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)