""" 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, ) # Middleware (order matters — outermost first) 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) # Routers 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"]) # UI 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