| """ |
| FastAPI app factory. |
| |
| Constructs the ServiceContainer (composition root), mounts middleware, |
| registers routes, and serves the UI. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from contextlib import asynccontextmanager |
| from pathlib import Path |
|
|
| from fastapi import FastAPI |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.staticfiles import StaticFiles |
| from loguru import logger |
|
|
| from api.container import build_container |
| from api.middleware import ( |
| RequestContextMiddleware, |
| RateLimitMiddleware, |
| RequestSizeLimitMiddleware, |
| GlobalExceptionMiddleware, |
| ) |
| from api.routes import ( |
| health, stats, providers, cache, jobs, faces, search, export, analysis, osint, |
| ) |
| from config.settings import Settings, UI_DIR |
| from utils.logging import setup_logging |
|
|
|
|
| def create_app(settings: Settings | None = None) -> FastAPI: |
| """Application factory — composes the entire dependency graph.""" |
| settings = settings or Settings() |
| setup_logging(settings) |
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| logger.info(f"Starting {settings.app_name} v{settings.app_version}") |
| container = build_container(settings) |
| app.state.container = container |
| app.state.settings = settings |
| logger.info(f"Registered providers: {container.registry.list_names()}") |
| yield |
| logger.info("Shutting down") |
| container.database.close() |
|
|
| app = FastAPI( |
| title=settings.app_name, |
| version=settings.app_version, |
| description="Multi-provider face intelligence platform", |
| lifespan=lifespan, |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=settings.cors_origins, |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
| app.add_middleware(GlobalExceptionMiddleware) |
| app.add_middleware(RequestSizeLimitMiddleware, max_bytes=settings.max_request_body_bytes) |
| app.add_middleware(RateLimitMiddleware, requests_per_minute=settings.rate_limit_per_minute) |
| app.add_middleware(RequestContextMiddleware) |
|
|
| |
| app.include_router(health.router, prefix="/health", tags=["health"]) |
| app.include_router(stats.router, prefix="/stats", tags=["stats"]) |
| app.include_router(providers.router, prefix="/providers", tags=["providers"]) |
| app.include_router(cache.router, prefix="/cache", tags=["cache"]) |
| app.include_router(jobs.router, prefix="/jobs", tags=["jobs"]) |
| app.include_router(faces.router, prefix="/faces", tags=["faces"]) |
| app.include_router(search.router, prefix="/search", tags=["search"]) |
| app.include_router(analysis.router, prefix="/analysis", tags=["analysis"]) |
| app.include_router(osint.router, prefix="/osint", tags=["osint"]) |
| app.include_router(export.router, prefix="/export", tags=["export"]) |
|
|
| |
| if UI_DIR.exists() and (UI_DIR / "index.html").exists(): |
| app.mount("/", StaticFiles(directory=str(UI_DIR), html=True), name="ui") |
| else: |
| @app.get("/") |
| async def root(): |
| return {"app": settings.app_name, "version": settings.app_version, |
| "docs": "/docs"} |
|
|
| return app |
|
|