import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from backend.bootstrap.container import build_container from backend.bootstrap.logging import configure_logging from backend.bootstrap.settings import Settings from backend.database.migrations import MIGRATIONS, MigrationRunner from backend.http.errors import install_error_handlers from backend.http.request_context import install_request_context from backend.http.router import api_router from backend.http.security import install_security_headers from backend.jobs.operations import run_operations_scheduler logger = logging.getLogger(__name__) def create_application(settings: Settings | None = None) -> FastAPI: runtime = settings or Settings.from_environment() container = build_container(runtime) @asynccontextmanager async def lifespan(_application: FastAPI): runtime.data_directory.mkdir(parents=True, exist_ok=True) configure_logging(runtime.log_level, runtime.log_file) MigrationRunner(container.database).upgrade(MIGRATIONS) database_status = container.database_status.get() logger.info( "Application started", extra={ "event": "application.started", "context": { "environment": runtime.environment, "schema_version": database_status.schema_version, }, }, ) stop = asyncio.Event() task = None if runtime.environment != "test": task = asyncio.create_task(run_operations_scheduler(container.operations, stop)) try: yield finally: if task: stop.set() await task application = FastAPI( title="小白复盘", version="0.1.0", docs_url="/api/docs" if runtime.debug else None, redoc_url=None, lifespan=lifespan, ) application.state.settings = runtime application.state.container = container install_request_context(application) install_security_headers(application) install_error_handlers(application) application.include_router(api_router, prefix="/api") if runtime.frontend_dist_directory.joinpath("index.html").is_file(): frontend_root = runtime.frontend_dist_directory.resolve() @application.get("/{requested_path:path}", include_in_schema=False) def frontend(requested_path: str) -> FileResponse: if requested_path == "api" or requested_path.startswith("api/"): raise HTTPException(status_code=404) candidate = frontend_root.joinpath(requested_path).resolve() if candidate.is_relative_to(frontend_root) and candidate.is_file(): response = FileResponse(candidate) if candidate.parent.name == "assets": response.headers["Cache-Control"] = "public,max-age=31536000,immutable" return response return FileResponse(frontend_root / "index.html", headers={"Cache-Control": "no-cache"}) return application