diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index b60a852..67c8fb2 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -1,3 +1,14 @@ + +# This worklflow will perform following actions when the code is pushed to development branch: +# - Test linting with pylint. +# - Fetch Latest release. +# - Build the latest docker image in development which needs test to pass first. +# - Push the docker image to Github Artifact Registry-Dev. +# +# Maintainers: +# - name: Nisha Sharma +# - email: nisha.sharma@uni-jena.de + name : Dev Build, Test and Publish on: diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 2e8a5b8..197746a 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -1,3 +1,13 @@ + +# This worklflow will perform following actions when a release is published: +# - Fetch Latest release. +# - Build the latest docker image in production. +# - Push the docker image to Github Artifact Registry-Prod. +# +# Maintainers: +# - name: Nisha Sharma +# - email: nisha.sharma@uni-jena.de + name : Prod Build, Test and Publish on: @@ -11,40 +21,9 @@ env: APP_IMAGE: chem-py-microservice jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip3 install --upgrade setuptools pip - pip3 install --no-cache-dir -r requirements.txt - python3 -m pip uninstall -y imantics - pip3 install imantics==0.1.12 - pip3 install --no-deps decimer-segmentation - pip3 install --no-deps decimer>=2.2.0 - pip3 install --no-deps STOUT-pypi>=2.0.5 - pip install flake8 pytest - - name: Analysing the code with pylint - run: | - flake8 --ignore E501,W503 $(git ls-files '*.py') - - name: Run test - run: | - pytest -p no:warnings - setup-build-publish-prod: name: Build & publish to prod registry - # if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - needs: test steps: - name: Checkout uses: actions/checkout@v2 @@ -77,7 +56,7 @@ jobs: - name: Build docker image run: |- docker build --build-arg RELEASE_VERSION=${{ steps.fetch-latest-release.outputs.tag_name }} --tag "europe-west3-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_NAME/$APP_IMAGE:latest" . - . + # Push the Docker image to Google Container Registry - name: Publish image to Google Artifact Registry run: |- diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8307264..02de48a 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,3 +1,13 @@ + +# This worklflow will perform following actions when the code is pushed to main branch. +# - Test linting with pylint. +# - Test the code with pytest. +# - Trigger release-please action to create release which needs test to pass first. +# +# Maintainers: +# - name: Nisha Sharma +# - email: nisha.sharma@uni-jena.de + name: release-please-action on: @@ -6,8 +16,38 @@ on: - main jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip3 install --upgrade setuptools pip + pip3 install --no-cache-dir -r requirements.txt + python3 -m pip uninstall -y imantics + pip3 install imantics==0.1.12 + pip3 install --no-deps decimer-segmentation + pip3 install --no-deps decimer>=2.2.0 + pip3 install --no-deps STOUT-pypi>=2.0.5 + pip install flake8 pytest + - name: Analysing the code with pylint + run: | + flake8 --ignore E501,W503 $(git ls-files '*.py') + - name: Run test + run: | + pytest -p no:warnings + release-please: runs-on: ubuntu-latest + needs: test steps: - uses: google-github-actions/release-please-action@v3 with: diff --git a/app/main.py b/app/main.py index 7be498e..4ca99d9 100644 --- a/app/main.py +++ b/app/main.py @@ -35,7 +35,7 @@ def custom_openapi(): return app.openapi_schema openapi_schema = get_openapi( title="Cheminf Micro Services", - version=os.getenv("RELEASE_VERSION", "pre-release"), + version=os.getenv("RELEASE_VERSION", "latest"), description="This set of essential and valuable microservices is designed to be accessed via API calls to support cheminformatics. Generally, it is designed to work with SMILES-based inputs and could be used to translate between different machine-readable representations, get Natural Product (NP) likeliness scores, visualize chemical structures, and generate descriptors. In addition, the microservices also host an instance of STOUT and another instance of DECIMER (two deep learning models for IUPAC name generation and optical chemical structure recognition, respectively).", routes=app.routes, ) diff --git a/app/modules/cdkmodules.py b/app/modules/cdkmodules.py index f0d9eec..f0ab4ee 100644 --- a/app/modules/cdkmodules.py +++ b/app/modules/cdkmodules.py @@ -112,6 +112,33 @@ def getCDKSDGMol(smiles: str): return mol_str +def getAromaticRingCount(mol): + """This function is adapted from CDK to + calculate the number of Aromatic Rings + present in a given molecule. + Args (mol): CDK mol object as input. + Returns (int): Number if aromatic rings present + """ + Cycles = JClass(cdk_base + ".graph.Cycles") + ElectronDonation = JClass(cdk_base + ".aromaticity.ElectronDonation") + + Aromaticity = JClass(cdk_base + ".aromaticity.Aromaticity")( + ElectronDonation.daylight(), Cycles.cdkAromaticSet() + ) + Aromaticity.apply(mol) + MCBRings = Cycles.mcb(mol).toRingSet() + NumberOfAromaticRings = 0 + for RingContainer in MCBRings.atomContainers(): + AreAllRingBondsAromatic = True + for Bond in RingContainer.bonds(): + if not Bond.isAromatic(): + AreAllRingBondsAromatic = False + break + if AreAllRingBondsAromatic: + NumberOfAromaticRings += 1 + return NumberOfAromaticRings + + def getCDKDescriptors(smiles: str): """Take an input SMILES and generate a selected set of molecular descriptors generated using CDK as a list. @@ -172,7 +199,7 @@ def getCDKDescriptors(smiles: str): .calculate(Mol) .getValue() ) - AromaticRings = None + AromaticRings = getAromaticRingCount(Mol) QEDWeighted = None FormalCharge = JClass( cdk_base + ".tools.manipulator.AtomContainerManipulator" @@ -198,7 +225,7 @@ def getCDKDescriptors(smiles: str): str(HBondAcceptorCountDescriptor), str(HBondDonorCountDescriptor), str(RuleOfFiveDescriptor), - str(AromaticRings), + AromaticRings, str(QEDWeighted), FormalCharge, "{:.2f}".format(float(str(FractionalCSP3Descriptor))), diff --git a/app/routers/chem.py b/app/routers/chem.py index 121e473..1563512 100644 --- a/app/routers/chem.py +++ b/app/routers/chem.py @@ -1,5 +1,6 @@ -from fastapi import Request, APIRouter +from fastapi import Body, Request, APIRouter from typing import Optional +from typing_extensions import Annotated from rdkit import Chem from rdkit.Chem.EnumerateStereoisomers import ( EnumerateStereoisomers, @@ -49,14 +50,12 @@ async def SMILES_stereoisomers(smiles: str): @router.post("/standardize") -async def standardize_mol(request: Request): +async def standardize_mol(mol: Annotated[str, Body(embed=True)]): """ Standardize molblock using the ChEMBL curation pipeline routine: - **mol**: required """ - body = await request.json() - mol = body["mol"] if mol: standardized_mol = standardizer.standardize_molblock(mol) rdkit_mol = Chem.MolFromMolBlock(standardized_mol) diff --git a/app/routers/decimer.py b/app/routers/decimer.py index 366da45..67c7d98 100644 --- a/app/routers/decimer.py +++ b/app/routers/decimer.py @@ -4,7 +4,8 @@ from fastapi.responses import JSONResponse from urllib.request import urlopen from urllib.parse import urlsplit -from fastapi import Request, APIRouter +from fastapi import Body, APIRouter +from typing_extensions import Annotated from app.modules.decimermodules import getPredictedSegments router = APIRouter( @@ -21,25 +22,20 @@ async def decimer_index(): @router.post("/process") -async def extract_chemicalinfo(request: Request): - body = await request.json() - image_path = body["path"] - reference = body["reference"] - split = urlsplit(image_path) +async def extract_chemicalinfo(path: Annotated[str, Body(embed=True)], reference: Annotated[str, Body(embed=True)], img: Annotated[str, Body(embed=True)]): + split = urlsplit(path) filename = "/tmp/" + split.path.split("/")[-1] - if "img" in body: - imgDataURI = body["img"] - if imgDataURI: - response = urlopen(imgDataURI) - 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(".")} - ) + if img: + 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(".")} + ) else: - response = requests.get(image_path) + response = requests.get(path) if response.status_code == 200: with open(filename, "wb") as f: f.write(response.content)