ffmol-rdkit / backend /app /main.py
hercules168's picture
Fix API routing before frontend fallback
3c2033a verified
Raw
History Blame Contribute Delete
2.73 kB
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.api.routes import create_router
from app.core.config import get_settings
from app.db.database import create_session_factory, init_db
from app.db.repository import Repository
def _configure_pipeline_api(app: FastAPI) -> None:
if getattr(app.state, "pipeline_api_configured", False):
return
settings = get_settings()
settings.data_dir.mkdir(parents=True, exist_ok=True)
engine, session_factory = create_session_factory(settings.database_url)
init_db(engine)
repo = Repository(session_factory)
app.include_router(create_router(repo))
_prioritize_api_routes(app)
app.state.pipeline_api_configured = True
def _prioritize_api_routes(app: FastAPI) -> None:
app.router.routes.sort(
key=lambda route: 0 if getattr(route, "path", "").startswith("/api") else 1
)
def _configure_frontend(app: FastAPI) -> None:
settings = get_settings()
frontend_dir = settings.frontend_dir
if frontend_dir is None:
return
index_html = frontend_dir / "index.html"
assets_dir = frontend_dir / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
if not index_html.exists():
return
@app.get("/")
def frontend_index() -> FileResponse:
return FileResponse(index_html)
@app.get("/{path:path}", include_in_schema=False)
def frontend_fallback(path: str) -> FileResponse:
candidate = _safe_frontend_file(frontend_dir, path)
if candidate is not None and candidate.exists() and candidate.is_file():
return FileResponse(candidate)
return FileResponse(index_html)
def _safe_frontend_file(frontend_dir: Path, path: str) -> Path | None:
if not path or path.startswith("api/"):
return None
root = frontend_dir.resolve()
candidate = (root / path).resolve()
if root == candidate or root in candidate.parents:
return candidate
return None
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
_configure_pipeline_api(app)
yield
def create_app(*, initialize: bool = True) -> FastAPI:
app = FastAPI(
title="Fast Follow Structure Generator",
lifespan=None if initialize else lifespan,
)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
if initialize:
_configure_pipeline_api(app)
_configure_frontend(app)
return app
app = create_app(initialize=False)