1- from fastapi import APIRouter , HTTPException , status , UploadFile
1+ from fastapi import APIRouter , HTTPException , status , UploadFile , File , Form
22from app .schemas import HealthCheck
3+ from pydantic import BaseModel , HttpUrl , Field
34import subprocess
5+ import tempfile
6+ import os
7+ import json
8+ from pathlib import Path
49
510router = APIRouter (
611 prefix = "/spectra" ,
914 responses = {404 : {"description" : "Not Found" }},
1015)
1116
17+ # Container name for nmr-cli (from docker-compose.yml)
18+ NMR_CLI_CONTAINER = "nmr-converter"
19+
20+
21+ class UrlParseRequest (BaseModel ):
22+ """Request model for parsing spectra from URL"""
23+ url : HttpUrl = Field (..., description = "URL of the spectra file" )
24+ capture_snapshot : bool = Field (
25+ False ,
26+ description = "Generate an image snapshot of the spectra"
27+ )
28+ auto_processing : bool = Field (
29+ False ,
30+ description = "Enable automatic processing of spectrum (FID → FT spectra)"
31+ )
32+ auto_detection : bool = Field (
33+ False ,
34+ description = "Enable ranges and zones automatic detection"
35+ )
36+
1237
1338@router .get ("/" , include_in_schema = False )
1439@router .get (
@@ -33,42 +58,205 @@ def get_health() -> HealthCheck:
3358 return HealthCheck (status = "OK" )
3459
3560
61+ def run_command (
62+ file_path : str = None ,
63+ url : str = None ,
64+ capture_snapshot : bool = False ,
65+ auto_processing : bool = False ,
66+ auto_detection : bool = False ,
67+ ) -> dict :
68+ """Execute nmr-cli command in Docker container"""
69+
70+ cmd = ["nmr-cli" , "parse-spectra" ]
71+
72+ if url :
73+ cmd .extend (["-u" , url ])
74+ elif file_path :
75+ cmd .extend (["-p" , file_path ])
76+
77+ if capture_snapshot :
78+ cmd .append ("-s" )
79+ if auto_processing :
80+ cmd .append ("-p" )
81+ if auto_detection :
82+ cmd .append ("-d" )
83+
84+ try :
85+ result = subprocess .run (
86+ ["docker" , "exec" , NMR_CLI_CONTAINER ] + cmd ,
87+ capture_output = True ,
88+ text = False ,
89+ timeout = 120
90+ )
91+ except subprocess .TimeoutExpired :
92+ raise HTTPException (
93+ status_code = 408 ,
94+ detail = "Processing timeout exceeded"
95+ )
96+ except FileNotFoundError :
97+ raise HTTPException (
98+ status_code = 500 ,
99+ detail = "Docker not found or nmr-converter container not running."
100+ )
101+
102+ if result .returncode != 0 :
103+ error_msg = result .stderr .decode (
104+ "utf-8" ) if result .stderr else "Unknown error"
105+ raise HTTPException (
106+ status_code = 422 ,
107+ detail = f"NMR CLI error: { error_msg } "
108+ )
109+
110+ # Parse output
111+ try :
112+ return json .loads (result .stdout .decode ("utf-8" ))
113+ except json .JSONDecodeError as e :
114+ raise HTTPException (
115+ status_code = 500 ,
116+ detail = f"Invalid JSON from NMR CLI: { e } "
117+ )
118+
119+
120+ def copy_file_to_container (local_path : str , container_path : str ) -> None :
121+ """Copy a file to the nmr-converter container."""
122+ try :
123+ subprocess .run (
124+ ["docker" , "cp" , local_path ,
125+ f"{ NMR_CLI_CONTAINER } :{ container_path } " ],
126+ check = True ,
127+ capture_output = True ,
128+ timeout = 30
129+ )
130+ except subprocess .CalledProcessError as e :
131+ error_msg = e .stderr .decode ("utf-8" ) if e .stderr else "Unknown error"
132+ raise HTTPException (
133+ status_code = 500 ,
134+ detail = f"Failed to copy file to container: { error_msg } "
135+ )
136+
137+
138+ def remove_file_from_container (container_path : str ) -> None :
139+ """Remove a file from the nmr-converter container."""
140+ try :
141+ subprocess .run (
142+ ["docker" , "exec" , NMR_CLI_CONTAINER , "rm" , "-f" , container_path ],
143+ capture_output = True ,
144+ timeout = 10
145+ )
146+ except Exception :
147+ pass
148+
149+
36150@router .post (
37- "/parse" ,
151+ "/parse/file " ,
38152 tags = ["spectra" ],
39- summary = "Parse the input spectra format and extract metadata " ,
40- response_description = "" ,
153+ summary = "Parse spectra from uploaded file " ,
154+ response_description = "Spectra data in JSON format " ,
41155 status_code = status .HTTP_200_OK ,
42156)
43- async def parse_spectra (file : UploadFile ):
157+ async def parse_spectra_from_file (
158+ file : UploadFile = File (..., description = "Upload a spectra file" ),
159+ capture_snapshot : bool = Form (
160+ False ,
161+ description = "Generate an image snapshot of the spectra"
162+ ),
163+ auto_processing : bool = Form (
164+ False ,
165+ description = "Enable automatic processing of spectrum (FID → FT spectra)"
166+ ),
167+ auto_detection : bool = Form (
168+ False ,
169+ description = "Enable ranges and zones automatic detection"
170+ ),
171+ ):
44172 """
45- ## Parse the spectra file and extract meta-data
46- Endpoint uses NMR-load-save to read the input spectra file (.jdx,.nmredata,.dx) and extracts metadata
173+ ## Parse spectra from uploaded file
174+
175+ Upload a spectra file along with processing options using multipart/form-data.
176+
177+ Processing Options:
178+ - `capture_snapshot (s)` : Capture snapshot of the spectra
179+ - `auto_processing (p)` : Enable automatic processing of spectrum (FID → FT spectra)
180+ - `auto_detection (d)` : Enable ranges and zones automatic detection
47181
48182 Returns:
49- data: spectra data in JSON format
183+ Spectra data in JSON format
50184 """
185+
186+ local_tmp_path = None
187+ container_tmp_path = None
188+
51189 try :
52- contents = file .file .read ()
53- file_path = "/tmp/" + file .filename
54- with open (file_path , "wb" ) as f :
55- f .write (contents )
56- p = subprocess .Popen (
57- "npx nmr-cli -p " + file_path , stdout = subprocess .PIPE , shell = True
190+ contents = await file .read ()
191+
192+ with tempfile .NamedTemporaryFile (
193+ delete = False ,
194+ suffix = Path (file .filename ).suffix
195+ ) as tmp_file :
196+ tmp_file .write (contents )
197+ local_tmp_path = tmp_file .name
198+
199+ container_tmp_path = f"/tmp/{ Path (local_tmp_path ).name } "
200+
201+ # Copy file to nmr-converter container
202+ copy_file_to_container (local_tmp_path , container_tmp_path )
203+
204+ # Run nmr-cli and get JSON output
205+ return run_command (
206+ file_path = container_tmp_path ,
207+ capture_snapshot = capture_snapshot ,
208+ auto_processing = auto_processing ,
209+ auto_detection = auto_detection ,
58210 )
59- ( output , err ) = p . communicate ()
60- p_status = p . wait ()
61- return output
211+
212+ except HTTPException :
213+ raise
62214 except Exception as e :
63215 raise HTTPException (
64216 status_code = 422 ,
65- detail = "Error parsing the structure "
66- + e .message
67- + ". Error: "
68- + err
69- + ". Status:"
70- + p_status ,
71- headers = {"X-Error" : "RDKit molecule input parse error" },
217+ detail = f"Error parsing the spectra file: { e } "
72218 )
73219 finally :
74- file .file .close ()
220+ if local_tmp_path and os .path .exists (local_tmp_path ):
221+ os .unlink (local_tmp_path )
222+ if container_tmp_path :
223+ remove_file_from_container (container_tmp_path )
224+ await file .close ()
225+
226+
227+ @router .post (
228+ "/parse/url" ,
229+ tags = ["spectra" ],
230+ summary = "Parse spectra from URL" ,
231+ response_description = "Spectra data in JSON format" ,
232+ status_code = status .HTTP_200_OK ,
233+ )
234+ async def parse_spectra_from_url (request : UrlParseRequest ):
235+ """
236+ Parse spectra from URL
237+
238+ Provide a URL to a spectra file along with processing options using JSON body.
239+
240+ Processing Options:
241+ - `capture_snapshot (s)` : Capture snapshot of the spectra
242+ - `auto_processing (p)` : Enable automatic processing of spectrum (FID → FT spectra)
243+ - `auto_detection (d)` : Enable ranges and zones automatic detection
244+
245+ Returns:
246+ Spectra data in JSON format
247+ """
248+ try :
249+ return run_command (
250+ url = str (request .url ),
251+ capture_snapshot = request .capture_snapshot ,
252+ auto_processing = request .auto_processing ,
253+ auto_detection = request .auto_detection ,
254+ )
255+
256+ except HTTPException :
257+ raise
258+ except Exception as e :
259+ raise HTTPException (
260+ status_code = 422 ,
261+ detail = f"Error parsing spectra from URL: { e } "
262+ )
0 commit comments