File size: 3,217 Bytes
aac350d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23d337e
 
 
 
 
 
aac350d
7e25f7a
aac350d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23d337e
aac350d
 
 
 
 
 
 
23d337e
 
aac350d
23d337e
aac350d
 
 
 
 
 
 
 
 
23d337e
7e25f7a
aac350d
 
 
23d337e
aac350d
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"""
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