Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,62 @@
from prometheus_fastapi_instrumentator import Instrumentator
from app.schemas import HealthCheck

DESCRIPTION = """
## NMR Kit API

A Python-based microservice for **storing**, **parsing**, **converting**, and
**predicting** NMR (Nuclear Magnetic Resonance) spectra.

### Modules

| Module | Description |
|--------|-------------|
| **Chemistry** | Generate HOSE codes, label atoms via ALATIS |
| **Spectra** | Parse NMR spectra from files or URLs |
| **Converter** | Convert NMR raw data to NMRium JSON |
| **Predict** | Predict NMR spectra using nmrdb.org or nmrshift engines |
| **Registration** | Register and query molecules via lwreg |

### Links

* [Documentation](https://nfdi4chem.github.io/nmrkit)
* [Source Code](https://github.com/NFDI4Chem/nmrkit)
"""

tags_metadata = [
{
"name": "healthcheck",
"description": "Health check endpoints to verify service availability.",
},
{
"name": "chem",
"description": "Chemistry operations including HOSE code generation and atom labeling.",
},
{
"name": "spectra",
"description": "Parse NMR spectra from uploaded files or remote URLs.",
},
{
"name": "converter",
"description": "Convert NMR raw data into NMRium-compatible JSON format.",
},
{
"name": "predict",
"description": (
"Predict NMR spectra from molecular structures using "
"**nmrdb.org** or **nmrshift** prediction engines."
),
},
{
"name": "registration",
"description": "Register, query, and retrieve molecules using the lwreg registration system.",
},
]

app = FastAPI(
title=config.PROJECT_NAME,
description="Python-based microservice to store and predict spectra.",
version=config.VERSION,
description=DESCRIPTION,
terms_of_service="https://nfdi4chem.github.io/nmrkit",
contact={
"name": "Steinbeck Lab",
Expand All @@ -26,6 +79,7 @@
"name": "CC BY 4.0",
"url": "https://creativecommons.org/licenses/by/4.0/",
},
openapi_tags=tags_metadata,
)

app.include_router(registration.router)
Expand All @@ -42,6 +96,7 @@
version_format="{major}",
prefix_format="/v{major}",
enable_latest=True,
description=DESCRIPTION,
terms_of_service="https://nfdi4chem.github.io/nmrkit",
contact={
"name": "Steinbeck Lab",
Expand All @@ -52,6 +107,7 @@
"name": "CC BY 4.0",
"url": "https://creativecommons.org/licenses/by/4.0/",
},
openapi_tags=tags_metadata,
)

Instrumentator().instrument(app).expose(app)
Expand Down
121 changes: 79 additions & 42 deletions app/routers/chem.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from typing import Annotated
from psycopg2.errors import UniqueViolation
from app.modules.cdkmodules import getCDKHOSECodes
from fastapi import APIRouter, HTTPException, status, Query, Body
from fastapi import APIRouter, HTTPException, status, Query
from app.modules.rdkitmodules import getRDKitHOSECodes
from app.schemas import HealthCheck
from app.schemas.alatis import AlatisModel
import requests

router = APIRouter(
prefix="/chem",
Expand Down Expand Up @@ -41,23 +39,91 @@ def get_health() -> HealthCheck:
@router.get(
"/hosecode",
tags=["chem"],
summary="Generates HOSE codes of molecule",
summary="Generate HOSE codes for a molecule",
description=(
"Generate **Hierarchically Ordered Spherical Environment (HOSE)** codes "
"for every atom in the given molecule. HOSE codes encode the local chemical "
"environment around each atom up to a configurable number of spheres.\n\n"
"Supports two cheminformatics frameworks:\n"
"- **CDK** (Chemistry Development Kit) — default, supports stereo\n"
"- **RDKit** — alternative implementation"
),
response_model=list[str],
response_description="Returns an array of hose codes generated",
response_description="Array of HOSE code strings, one per atom in the molecule",
status_code=status.HTTP_200_OK,
responses={
200: {
"description": "Successfully generated HOSE codes",
"content": {
"application/json": {
"example": [
"C(CC,CC,&)",
"C(CC,C&,&)",
"C(CC,CC,&)",
"C(CCC,CC&,&)",
"C(CC,CC,CC)",
"C(CC,CC,CC)",
]
}
},
},
409: {"description": "Molecule already exists (unique constraint violation)"},
422: {"description": "Error parsing the molecular structure"},
},
)
async def HOSE_Codes(
smiles: Annotated[str, Query(examples=["CCCC1CC1"])],
framework: Annotated[str, Query(enum=["cdk", "rdkit"])] = "cdk",
spheres: Annotated[int, Query()] = 3,
usestereo: Annotated[bool, Query()] = False,
smiles: Annotated[
str,
Query(
description="SMILES string representing the molecular structure",
example="CCCC1CC1",
examples=[
"CCCC1CC1",
"c1ccccc1",
"CC(=O)O",
"CCO",
"C1CCCCC1",
"CC(=O)Oc1ccccc1C(=O)O",
],
),
],
framework: Annotated[
str,
Query(
enum=["cdk", "rdkit"],
description="Cheminformatics framework to use for HOSE code generation",
),
] = "cdk",
spheres: Annotated[
int,
Query(
description="Number of spheres (bond distance) to consider around each atom",
ge=1,
le=10,
),
] = 3,
usestereo: Annotated[
bool,
Query(
description="Whether to include stereochemistry information in HOSE codes (CDK only)",
),
] = False,
) -> list[str]:
"""
## Generates HOSE codes for a given molecule
Endpoint to generate HOSE codes based on each atom in the given molecule.
## Generate HOSE codes for a given molecule

Returns:
HOSE Codes: An array of hose codes generated
Generates HOSE (Hierarchically Ordered Spherical Environment) codes based on
each atom in the given molecule. These codes are widely used in NMR chemical
shift prediction.

### Parameters
- **smiles**: A valid SMILES string (e.g. `CCCC1CC1`)
- **framework**: Choose `cdk` (default) or `rdkit`
- **spheres**: Number of bond spheres to encode (default: 3)
- **usestereo**: Include stereochemistry in codes (CDK only, default: false)

### Returns
An array of HOSE code strings, one for each atom in the molecule.
"""
try:
if framework == "cdk":
Expand All @@ -78,32 +144,3 @@ async def HOSE_Codes(
detail="Error parsing the structure " + e.message,
headers={"X-Error": "RDKit molecule input parse error"},
)


@router.post(
"/label-atoms",
tags=["chem"],
summary="Label atoms using ALATIS naming system",
response_model=AlatisModel,
response_description="",
status_code=status.HTTP_200_OK,
)
async def label_atoms(data: Annotated[str, Body(embed=False, media_type="text/plain")]):
"""
## Generates atom labels for a given molecule

Returns:
JSON with various representations
"""
try:
url = "http://alatis.nmrfam.wisc.edu/upload"
payload = {"input_text": data, "format": "format_", "response_type": "json"}
response = requests.request("POST", url, data=payload)
response.raise_for_status() # Raise an error for bad status codes
return response.json()
except Exception as e:
raise HTTPException(
status_code=422,
detail=f"Error parsing the structure: {str(e)}",
headers={"X-Error": "RDKit molecule input parse error"},
)
41 changes: 33 additions & 8 deletions app/routers/converter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import subprocess
from fastapi import APIRouter, HTTPException, status, Response
from fastapi import APIRouter, HTTPException, status, Response, Query
from app.schemas import HealthCheck

router = APIRouter(
Expand Down Expand Up @@ -36,17 +36,42 @@ def get_health() -> HealthCheck:
@router.get(
"/spectra",
tags=["converter"],
summary="Load and convert NMR raw data",
# response_model=List[int],
response_description="Load and convert NMR raw data",
summary="Convert NMR raw data to NMRium JSON",
description=(
"Fetch NMR raw data from a remote URL and convert it into "
"[NMRium](https://www.nmrium.org/)-compatible JSON format. "
"The conversion is performed by the **nmr-cli** tool running "
"inside a Docker container.\n\n"
"Supported input formats include Bruker, JCAMP-DX, and other "
"formats recognized by nmr-cli."
),
response_description="NMRium-compatible JSON representation of the NMR data",
status_code=status.HTTP_200_OK,
responses={
200: {
"description": "Successfully converted NMR data to NMRium JSON",
"content": {"application/json": {}},
},
500: {"description": "Conversion failed or Docker container not available"},
},
)
async def nmr_load_save(url: str):
async def nmr_load_save(
url: str = Query(
...,
description="URL pointing to the NMR raw data file to convert",
examples=["https://example.com/nmr-data/sample.zip"],
),
):
"""
## Return nmrium json
## Convert NMR raw data to NMRium JSON

Returns:
Return nmrium json
Fetches NMR raw data from the provided URL and converts it into NMRium JSON format.

### Parameters
- **url**: A publicly accessible URL pointing to the NMR raw data

### Returns
NMRium-compatible JSON object containing the converted spectra data.
"""
process = subprocess.Popen(
["docker exec nmr-converter nmr-cli -u " + url],
Expand Down
Loading