From 66e5e5e9319eb05872968ffa6d06f5b0949f4929 Mon Sep 17 00:00:00 2001 From: Chandu Nainala Date: Mon, 4 Sep 2023 15:55:40 +0200 Subject: [PATCH] feat: added file upload option to ocsr routes (fixes #328) (#365) * feat: added file upload option to ocsr routes (fixes #365 --------- Co-authored-by: NishaSharma14 Co-authored-by: Nisha Sharma <54287612+NishaSharma14@users.noreply.github.com> --- README.md | 2 +- app/modules/decimer.py | 19 ++++++++ app/routers/ocsr.py | 108 ++++++++++++++++++++++++++++++----------- requirements.txt | 1 + 4 files changed, 102 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ad2fae7..3c4b17d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

CMS Logo

+

CMS Logo

[![License](https://img.shields.io/badge/License-MIT%202.0-blue.svg)](https://opensource.org/licenses/MIT) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-blue.svg)](https://GitHub.com/Steinbeck-Lab/cheminformatics-python-microservice/graphs/commit-activity) diff --git a/app/modules/decimer.py b/app/modules/decimer.py index 1821338..10f4967 100644 --- a/app/modules/decimer.py +++ b/app/modules/decimer.py @@ -100,3 +100,22 @@ def getPredictedSegments(path: str) -> str: smiles_predicted.append(smiles) os.remove(segment_path) return ".".join(smiles_predicted) + + +def getPredictedSegmentsFromFile(content: any, filename: str) -> tuple: + """ + Takes an image file path and returns a set of paths and image names of segmented images. + + Args: + input_path (str): the path of an image. + + Returns: + image_name (str): image file name. + segments (list): a set of segmented images. + """ + + with open(filename, "wb") as f: + f.write(content) + smiles = getPredictedSegments(filename) + os.remove(filename) + return smiles diff --git a/app/routers/ocsr.py b/app/routers/ocsr.py index f7d8a4f..d6b913b 100644 --- a/app/routers/ocsr.py +++ b/app/routers/ocsr.py @@ -1,14 +1,14 @@ -import os import requests - +import uuid from fastapi.responses import JSONResponse from urllib.request import urlopen from urllib.parse import urlsplit from fastapi import Body, APIRouter, status, HTTPException -from app.modules.decimer import getPredictedSegments +from app.modules.decimer import getPredictedSegmentsFromFile from app.schemas import HealthCheck from app.schemas.error import ErrorResponse, BadRequestModel, NotFoundModel from app.schemas.ocsr_schema import ExtractChemicalInfoResponse +from fastapi import UploadFile router = APIRouter( prefix="/ocsr", @@ -59,63 +59,117 @@ def get_health() -> HealthCheck: 422: {"description": "Unprocessable Entity", "model": ErrorResponse}, }, ) -async def extract_chemicalInfo( +async def Extract_ChemicalInfo_From_File( path: str = Body( None, embed=True, description="URL or local file path to the chemical structure depiction image.", + openapi_examples={ + "example1": { + "summary": "Cheminformatics - Article example image", + "value": "https://static-content.springer.com/image/art%3A10.1186%2Fs13321-023-00744-6/MediaObjects/13321_2023_744_Figa_HTML.png", + }, + "example2": { + "summary": "Cheminformatics - Article example image", + "value": "https://static-content.springer.com/image/art%3A10.1186%2Fs13321-023-00743-7/MediaObjects/13321_2023_743_Figa_HTML.png", + }, + }, ), reference: str = Body(None, embed=True, description="Reference information."), img: str = Body( - None, embed=True, description="URL of the chemical structure depiction image." + None, + embed=True, + description="Bytes content of the chemical structure depiction image.", ), ): """ Detect, segment and convert a chemical structure depiction into a SMILES string using the DECIMER modules. Parameters: - - **Images**: required (str): URL or local file path to the chemical structure depiction image. + - **path**: optional if img is provided (str): URL or local file path to the chemical structure depiction image. + - **reference**: optional (str): URL or local file path to the chemical structure depiction image. + - **img**: optional if a valid path is provided (str): URL or local file path to the chemical structure depiction image. Returns: - JSONResponse: A JSON response containing the extracted SMILES and the reference (if provided). Raises: - HTTPException: If the 'path' parameter is not provided or if it is an invalid URL or file path. - - HTTPException: If the 'img' parameter is provided, but the URL is not accessible. + - HTTPException: If the 'img' parameter is provided, but the content is not accessible. """ - split = urlsplit(path) - filename = "/tmp/" + split.path.split("/")[-1] if img: try: + filename = "/tmp/" + str(uuid.uuid4()) response = urlopen(img) - with open(filename, "wb") as f: - f.write(response.file.read()) - smiles = getPredictedSegments(filename) - os.remove(filename) - return JSONResponse( - content={"reference": reference, "smiles": smiles.split(".")} - ) + smiles = getPredictedSegmentsFromFile(response.file.read(), filename) + return JSONResponse( + content={"reference": reference, "smiles": smiles.split(".")} + ) except Exception as e: raise HTTPException( - status_code=400, detail="Error accessing image URL: " + str(e) + status_code=400, detail="Error accessing image content: " + str(e) ) else: try: + split = urlsplit(path) + filename = "/tmp/" + split.path.split("/")[-1] response = requests.get(path) if response.status_code == 200: - with open(filename, "wb") as f: - f.write(response.content) - smiles = getPredictedSegments(filename) - os.remove(filename) - return JSONResponse( - content={"reference": reference, "smiles": smiles.split(".")} - ) - else: + smiles = getPredictedSegmentsFromFile(response.content, filename) + return JSONResponse( + content={"reference": reference, "smiles": smiles.split(".")} + ) + except Exception as e: + raise HTTPException( + status_code=400, detail="Invalid URL or file path: " + str(e) + ) + + +@router.post( + "/process-upload", + summary="Detect, segment and convert a chemical structure depiction in the uploaded file into a SMILES string using the DECIMER", + responses={ + 200: { + "description": "Successful response", + "model": ExtractChemicalInfoResponse, + }, + 400: {"description": "Bad Request", "model": BadRequestModel}, + 404: {"description": "Not Found", "model": NotFoundModel}, + 422: {"description": "Unprocessable Entity", "model": ErrorResponse}, + }, +) +async def Extract_ChemicalInfo(file: UploadFile): + """ + Detect, segment and convert a chemical structure depiction in the uploaded image file into a SMILES string using the DECIMER modules. + + Parameters: + - **file**: required (File): + + Returns: + - JSONResponse: A JSON response containing the extracted SMILES and the reference (if provided). + + Raises: + - HTTPException: If the 'path' parameter is not provided or if it is an invalid URL or file path. + - HTTPException: If the 'img' parameter is provided, but the URL is not accessible. + """ + + if file: + filename = file.filename + try: + contents = file.file.read() + try: + smiles = getPredictedSegmentsFromFile(contents, filename) + return JSONResponse( + content={"reference": None, "smiles": smiles.split(".")} + ) + except Exception as e: raise HTTPException( - status_code=400, detail="Error accessing provided URL" + status_code=400, detail="Error accessing image URL: " + str(e) ) except Exception as e: raise HTTPException( - status_code=400, detail="Invalid URL or file path: " + str(e) + status_code=400, detail="Error accessing image URL: " + str(e) ) + finally: + file.file.close() diff --git a/requirements.txt b/requirements.txt index 69ff076..7ac4f53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,4 @@ pillow-heif==0.10.0 selfies>=2.1.1 httpx>=0.24.1 keras_preprocessing==1.1.2 +python-multipart \ No newline at end of file