added redis, added convert function
also fixed logs, split code into multiple files
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# postman
|
||||||
|
.postman
|
||||||
|
postman
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[codz]
|
*.py[codz]
|
||||||
@@ -182,9 +186,9 @@ cython_debug/
|
|||||||
.abstra/
|
.abstra/
|
||||||
|
|
||||||
# Visual Studio Code
|
# Visual Studio Code
|
||||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
# you could uncomment the following to ignore the entire vscode folder
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
14
dockerfile
14
dockerfile
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
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 \
|
||||||
python3 \
|
python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /var/local
|
WORKDIR /var/local
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
openbabel-wheel==3.1.1.22
|
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
|
||||||
|
|||||||
94
src/app.py
94
src/app.py
@@ -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
38
src/logging_config.py
Normal 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__)
|
||||||
16
src/request_response_models.py
Normal file
16
src/request_response_models.py
Normal 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
|
||||||
Reference in New Issue
Block a user