Skip to content

Commit ba7faea

Browse files
authored
Merge pull request #560 from Steinbeck-Lab/development
feat: SMARTS convertion
2 parents 29ebd8b + 154b9b0 commit ba7faea

18 files changed

+948
-257
lines changed

.github/workflows/dev-build.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# This worklflow will perform following actions when the code is pushed to development branch:
32
# - Test linting with pylint.
43
# - Fetch Latest release.
@@ -43,6 +42,16 @@ jobs:
4342
push: true
4443
build-args: |
4544
RELEASE_VERSION=dev-latest
46-
tags: ${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:dev-latest
47-
username: ${{ env.DOCKER_HUB_USERNAME }}
48-
password: ${{ env.DOCKER_HUB_PASSWORD }}
45+
tags: ${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:api-dev
46+
47+
- name: Build and push frontend Docker image
48+
uses: docker/[email protected]
49+
with:
50+
context: ./frontend
51+
file: ./frontend/Dockerfile
52+
push: true
53+
# REACT_APP_API_URL will use the default from frontend/Dockerfile (https://dev.api.naturalproducts.net/latest)
54+
# If a specific one is needed for this dev build, add build-args here.
55+
# build-args: |
56+
# REACT_APP_API_URL=your-dev-frontend-api-url
57+
tags: ${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:app-dev

.github/workflows/prod-build.yml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# This worklflow will perform following actions when a release is published:
32
# - Fetch Latest release.
43
# - Build the latest docker image in production.
@@ -57,8 +56,6 @@ jobs:
5756
tags: |
5857
${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:${{ steps.fetch-latest-release.outputs.tag_name }}
5958
${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:latest
60-
username: ${{ env.DOCKER_HUB_USERNAME }}
61-
password: ${{ env.DOCKER_HUB_PASSWORD }}
6259
6360
- name: Build and push lite Docker image
6461
uses: docker/build-push-action@v4
@@ -71,5 +68,15 @@ jobs:
7168
tags: |
7269
${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:${{ steps.fetch-latest-release.outputs.tag_name }}-lite
7370
${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:latest-lite
74-
username: ${{ env.DOCKER_HUB_USERNAME }}
75-
password: ${{ env.DOCKER_HUB_PASSWORD }}
71+
72+
- name: Build and push frontend Docker image
73+
uses: docker/build-push-action@v4
74+
with:
75+
context: ./frontend
76+
file: ./frontend/Dockerfile
77+
push: true
78+
build-args: |
79+
REACT_APP_API_URL=/api/latest # For prod Traefik setup
80+
tags: |
81+
${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:app-${{ steps.fetch-latest-release.outputs.tag_name }}
82+
${{ env.REPOSITORY_NAMESPACE }}/${{ env.REPOSITORY_NAME }}:app

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ jobs:
3131
python -m pip install --upgrade pip
3232
pip3 install --upgrade setuptools pip
3333
pip3 install --no-cache-dir -r requirements.txt
34-
pip3 install --no-deps decimer-segmentation==1.1.3
35-
pip3 install --no-deps decimer==2.3.0
34+
pip install git+https://github.com/Kohulan/DECIMER-Image-Segmentation.git@bbox --no-deps
35+
pip3 install --no-deps decimer
3636
pip3 install --no-deps STOUT-pypi==2.0.5
3737
pip install flake8 pytest
3838
pip install pytest-cov

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM continuumio/miniconda3:24.1.2-0 AS cheminf-python-ms
22

33
ENV PYTHON_VERSION=3.11 \
44
INCLUDE_OCSR=true \
5-
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/ \
5+
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-arm64/ \
66
# Add default number of workers
77
WORKERS=2 \
88
# Add other Python configurations

Dockerfile.lite

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM continuumio/miniconda3:24.1.2-0 AS cheminf-python-ms
22

33
ENV PYTHON_VERSION=3.11 \
44
INCLUDE_OCSR=false \
5-
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/ \
5+
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-arm64/ \
66
# Add default number of workers
77
WORKERS=1 \
88
# Add other Python configurations

app/routers/chem.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,11 @@ async def all_filter_molecules(
977977
title="NPlikenessScore",
978978
description="Calculate NPlikenessScore in the range (e.g., 0-10)",
979979
),
980+
filterOperator: Literal["AND", "OR"] = Query(
981+
"OR",
982+
title="Filter Operator",
983+
description="Logic for combining filter results: AND requires all selected filters to pass, OR requires at least one filter to pass",
984+
),
980985
):
981986
all_smiles = []
982987
for item in io.StringIO(smiles_list):
@@ -1051,12 +1056,30 @@ async def all_filter_molecules(
10511056
else:
10521057
results.append("False")
10531058

1054-
output_list = [
1055-
x if x != "True" and x != "False" else ("T" if x == "True" else "F")
1056-
for x in results
1057-
]
1058-
final_results = ", ".join(output_list).replace(":,", " : ")
1059-
all_smiles.append(final_results)
1059+
# Filter results based on AND/OR operator
1060+
# First element is the SMILES, so we start checking from index 1
1061+
filter_results = (
1062+
[r == "True" for r in results[1:]] if len(results) > 1 else []
1063+
)
1064+
1065+
# Apply filter operator logic
1066+
passes_filters = False
1067+
if filter_results:
1068+
if filterOperator == "AND":
1069+
passes_filters = all(filter_results) # All filters must pass
1070+
else: # OR
1071+
passes_filters = any(
1072+
filter_results
1073+
) # At least one filter must pass
1074+
1075+
# Only include SMILES that pass the filter operator logic
1076+
if not filter_results or passes_filters:
1077+
output_list = [
1078+
x if x != "True" and x != "False" else ("T" if x == "True" else "F")
1079+
for x in results
1080+
]
1081+
final_results = ", ".join(output_list).replace(":,", " : ")
1082+
all_smiles.append(final_results)
10601083

10611084
return all_smiles
10621085

app/routers/converters.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from app.schemas.converters_schema import GenerateSMILESResponse
3838
from app.schemas.converters_schema import ThreeDCoordinatesResponse
3939
from app.schemas.converters_schema import TwoDCoordinatesResponse
40+
from app.schemas.converters_schema import GenerateSMARTSResponse
4041
from app.schemas.error import BadRequestModel
4142
from app.schemas.error import ErrorResponse
4243
from app.schemas.error import NotFoundModel
@@ -702,3 +703,45 @@ async def smiles_convert_to_formats(
702703
status_code=422,
703704
detail="Error processing request: " + str(e),
704705
)
706+
707+
708+
@router.get(
709+
"/smarts",
710+
summary="Generate SMARTS from a given SMILES",
711+
responses={
712+
200: {
713+
"description": "Successful response",
714+
"model": GenerateSMARTSResponse,
715+
},
716+
400: {"description": "Bad Request", "model": BadRequestModel},
717+
404: {"description": "Not Found", "model": NotFoundModel},
718+
422: {"description": "Unprocessable Entity", "model": ErrorResponse},
719+
},
720+
)
721+
async def smiles_to_smarts(
722+
smiles: str = Query(
723+
title="SMILES",
724+
description="SMILES representation of the molecule",
725+
openapi_examples={
726+
"example1": {
727+
"summary": "Example: Caffeine",
728+
"value": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C",
729+
},
730+
"example2": {
731+
"summary": "Example: Topiramate-13C6",
732+
"value": "CC1(C)OC2COC3(COS(N)(=O)=O)OC(C)(C)OC3C2O1",
733+
},
734+
},
735+
),
736+
toolkit: Literal["rdkit"] = Query(
737+
default="rdkit",
738+
description="Cheminformatics toolkit used in the backend",
739+
),
740+
):
741+
742+
if toolkit == "rdkit":
743+
mol = parse_input(smiles, "rdkit", False)
744+
if mol:
745+
smarts = Chem.MolToSmarts(mol)
746+
if smarts:
747+
return str(smarts)

app/schemas/converters_schema.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class GenerateSMILESResponse(BaseModel):
9090

9191
smiles: str = Field(
9292
...,
93-
title="SMILES",
93+
title="SMARTS",
9494
description="The generated SMILES string corresponding to the input text.",
9595
)
9696

@@ -271,10 +271,10 @@ class GenerateSELFIESResponse(BaseModel):
271271
"""Represents a response containing a generated SELFIES string.
272272
273273
Properties:
274-
- iupac (str): The generated SELFIES string.
274+
- selfies (str): The generated SELFIES string.
275275
"""
276276

277-
iupac: str = Field(
277+
selfies: str = Field(
278278
...,
279279
title="SMILES",
280280
description="The generated SELFIES string corresponding to the input SMILES.",
@@ -298,6 +298,37 @@ class Config:
298298
}
299299

300300

301+
class GenerateSMARTSResponse(BaseModel):
302+
"""Represents a response containing a generated SMARTS string.
303+
304+
Properties:
305+
- smarts (str): The generated SMARTS string.
306+
"""
307+
308+
smarts: str = Field(
309+
...,
310+
title="SMILES",
311+
description="The generated SMARTS string corresponding to the input SMILES.",
312+
)
313+
314+
class Config:
315+
"""Pydantic model configuration.
316+
317+
JSON Schema Extra:
318+
- Includes examples of the response structure.
319+
"""
320+
321+
json_schema_extra = {
322+
"examples": [
323+
{
324+
"input": "CN1C(=O)C2=C(N=CN2C)N(C)C1=O",
325+
"message": "Success",
326+
"output": "[#6]-[#7]1:[#6]:[#7]:[#6]2:[#6]:1:[#6](=[#8]):[#7](:[#6](=[#8]):[#7]:2-[#6])-[#6]",
327+
},
328+
],
329+
}
330+
331+
301332
class GenerateFormatsResponse(BaseModel):
302333
"""Represents a response containing a generated SELFIES string.
303334

docker-compose.lite.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
services:
2+
api:
3+
build:
4+
context: ./
5+
dockerfile: Dockerfile.lite
6+
container_name: cheminformatics-microservice-lite
7+
environment:
8+
- HOMEPAGE_URL=https://docs.api.naturalproducts.net
9+
- RELEASE_VERSION=v2.6.0
10+
- WORKERS=2 # Make workers configurable
11+
- INCLUDE_OCSR=false
12+
ports:
13+
- "80:80"
14+
healthcheck:
15+
test: curl -f http://localhost:80/latest/chem/health || exit 1
16+
interval: 90s
17+
timeout: 10s
18+
retries: 20
19+
start_period: 60s
20+
restart: unless-stopped
21+
security_opt:
22+
- no-new-privileges:true
23+
networks:
24+
- cm_fastapi
25+
26+
prometheus:
27+
image: prom/prometheus:latest # Specify version for better stability
28+
container_name: prometheus
29+
ports:
30+
- "9090:9090"
31+
volumes:
32+
- ./prometheus_data/prometheus.yml:/etc/prometheus/prometheus.yml:ro
33+
- prometheus_data:/prometheus
34+
command:
35+
- '--config.file=/etc/prometheus/prometheus.yml'
36+
- '--storage.tsdb.path=/prometheus'
37+
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
38+
- '--web.console.templates=/usr/share/prometheus/consoles'
39+
restart: unless-stopped
40+
networks:
41+
- cm_fastapi
42+
43+
grafana:
44+
image: grafana/grafana:latest
45+
container_name: grafana
46+
user: "472" # Grafana's official user ID
47+
ports:
48+
- "3000:3000"
49+
volumes:
50+
- grafana_data:/var/lib/grafana
51+
environment:
52+
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin} # Configurable password
53+
- GF_USERS_ALLOW_SIGN_UP=false
54+
restart: unless-stopped
55+
depends_on:
56+
- prometheus
57+
networks:
58+
- cm_fastapi
59+
60+
volumes:
61+
prometheus_data:
62+
grafana_data:
63+
64+
networks:
65+
cm_fastapi:
66+
name: cm_fastapi
67+
driver: bridge

0 commit comments

Comments
 (0)