added redis, added convert function

also fixed logs, split code into multiple files
This commit is contained in:
2026-01-28 10:39:57 +03:00
parent 23e8b71776
commit 8cff37ce4a
7 changed files with 156 additions and 26 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
# postman
.postman
postman
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[codz] *.py[codz]

View File

@@ -6,4 +6,11 @@ services:
PORT: 1654 PORT: 1654
ports: ports:
- 1654:1654 - 1654:1654
tty: true depends_on:
- redis
restart: unless-stopped
redis:
image: "redis:alpine"
ports:
- "6379:6379"
restart: unless-stopped

View File

@@ -1,6 +1,6 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
# Install openbabel dependencies # Install openbabel and python
RUN apt-get update \ RUN apt-get update \
&& apt-get install --yes --quiet --no-install-recommends \ && apt-get install --yes --quiet --no-install-recommends \
openbabel \ openbabel \
@@ -17,4 +17,4 @@ RUN /usr/bin/python3 -m pip install --no-cache-dir -r requirements.txt
COPY src . COPY src .
CMD fastapi run app.py --port ${PORT} CMD uvicorn app:app --host 0.0.0.0 --port ${PORT} --log-level warning

View File

@@ -2,3 +2,4 @@ openbabel-wheel==3.1.1.22
fastapi[all]==0.121.3 fastapi[all]==0.121.3
fastapi-cache2==0.2.2 fastapi-cache2==0.2.2
redis==7.1.0 redis==7.1.0
pyscf==2.11.0

View File

@@ -2,39 +2,103 @@ from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from starlette.requests import Request from fastapi.exceptions import HTTPException
from starlette.responses import Response from fastapi.middleware.cors import CORSMiddleware
# from fastapi.requests import Request
from fastapi.responses import Response
from fastapi_cache import FastAPICache from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache from fastapi_cache.decorator import cache
from logging_config import logger
from redis import asyncio as aioredis
from openbabel import pybel from openbabel import pybel
from pyscf import gto # pyright: ignore
from redis import asyncio as aioredis
from request_response_models import BasicError, ConvertRequest, MolFileModel
@asynccontextmanager @asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]: async def lifespan(_: FastAPI) -> AsyncIterator[None]:
redis = aioredis.from_url("redis://localhost") redis = aioredis.from_url("redis://redis:6379")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache") FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
pybel.ob.obErrorLog.SetOutputLevel(0) # NOTE removes logging
yield yield
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
origins = [
"http://localhost",
"http://localhost:8001",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post(
"/convert",
responses={
200: {"description": "Coversion Successful", "model": MolFileModel},
400: {"description": "Item created", "model": BasicError},
},
)
def convert_molecule(req: ConvertRequest):
try:
# Rad the string and format from request
mol = pybel.readstring(req.format, req.text)
mol.addh()
if req.convert_3d:
mol.make3D()
if req.optimize_geometry:
mol.localopt()
logger.info(f"Converting from format {req.format}")
# To compute the Number of electrons and orbitals
atoms = [(atom.atomicnum, atom.coords) for atom in mol.atoms]
mol_pyscf = gto.Mole()
mol_pyscf.atom = atoms
mol_pyscf.basis = "sto-3g" # simple basis
mol_pyscf.charge = mol.charge # default net charge
mol_pyscf.spin = mol.spin - 1 # multiplicity - 1
mol_pyscf.build()
# Export the resulting molecule
mol.OBMol.SetTitle(
f"Charge={mol.charge} Multiplicity={mol.spin} Electrons={mol_pyscf.nelectron} Orbitals={mol_pyscf.nao_nr()}"
)
mol2string = mol.write("xyz")
return Response({"molfile": mol2string}, 200)
except Exception as e:
raise HTTPException(status_code=400, detail={"error": str(e)})
@app.get("/informats") @app.get("/informats")
@cache()
async def get_informats(): async def get_informats():
return pybel.informats return pybel.informats
@app.get("/") @app.get(
@cache(expire=60) "/health",
async def index(): responses={
return dict(hello="world") 200: {
"description": "Is the service running?",
"content": {"application/json": {"example": {"status": "healthy"}}},
},
},
)
@cache()
async def health_check():
return {"status": "healthy"}

38
src/logging_config.py Normal file
View File

@@ -0,0 +1,38 @@
import logging
import logging.config
import os
ROOT_LEVEL = os.environ.get("PROD", "INFO")
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
},
"handlers": {
"default": {
"formatter": "standard",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout", # Default is stderr
},
},
"loggers": {
"": { # root logger
"level": ROOT_LEVEL, # "INFO",
"handlers": ["default"],
"propagate": False,
},
"uvicorn.error": {
"level": "WARNING",
"handlers": ["default"],
},
"uvicorn.access": {
"level": "DEBUG",
"handlers": ["default"],
},
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)

View File

@@ -0,0 +1,16 @@
from pydantic import BaseModel
class ConvertRequest(BaseModel):
text: str
format: str # e.g. "mol", "smi", "sdf", "inchi", etc.
convert_3d: bool = False
optimize_geometry: bool = False
class MolFileModel(BaseModel):
molfile: str
class BasicError(BaseModel):
error: str