added working convertion + logging

This commit is contained in:
2026-03-16 22:21:59 +03:00
parent 73a6cbcbc7
commit 12a64e9f2f
6 changed files with 395 additions and 395 deletions

422
.gitignore vendored
View File

@@ -1,211 +1,211 @@
# postman # postman
.postman .postman
postman postman
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[codz] *.py[codz]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py.cover *.py.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/ cover/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache .webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version # .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# UV # UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
#uv.lock #uv.lock
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock #poetry.lock
#poetry.toml #poetry.toml
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock #pdm.lock
#pdm.toml #pdm.toml
.pdm-python .pdm-python
.pdm-build/ .pdm-build/
# pixi # pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock #pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control. # in the .venv directory. It is recommended not to include this directory in version control.
.pixi .pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
# Environments # Environments
.env .env
.envrc .envrc
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation # mkdocs documentation
/site /site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
dmypy.json dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer # pytype static type analyzer
.pytype/ .pytype/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# Abstra # Abstra
# Abstra is an AI-powered process automation framework. # Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings. # Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs # Learn more at https://abstra.io/docs
.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/
# Ruff stuff: # Ruff stuff:
.ruff_cache/ .ruff_cache/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
# Cursor # Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files # refer to https://docs.cursor.com/context/ignore-files
.cursorignore .cursorignore
.cursorindexingignore .cursorindexingignore
# Marimo # Marimo
marimo/_static/ marimo/_static/
marimo/_lsp/ marimo/_lsp/
__marimo__/ __marimo__/

View File

@@ -1,20 +1,20 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
# Install openbabel and python # 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
COPY requirements.txt . COPY requirements.txt .
# Install dependencies # Install dependencies
RUN /usr/bin/python3 -m pip install --no-cache-dir -r requirements.txt RUN /usr/bin/python3 -m pip install --no-cache-dir -r requirements.txt
COPY src . COPY src .
CMD uvicorn app:app --host 0.0.0.0 --port ${PORT} --log-level warning CMD uvicorn app:app --host 0.0.0.0 --port ${PORT} --log-level warning

View File

@@ -1,5 +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 pyscf==2.11.0

View File

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

View File

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

View File

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