Files
ai-tax-agent/libs/app_factory.py
harkon db61b05c80
Some checks failed
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
full ingestion -> OCR -> extraction flow is now working correctly.
2025-11-26 15:46:59 +00:00

132 lines
4.3 KiB
Python

"""Factory for creating FastAPI applications with consistent setup."""
# FILE: libs/app_factory.py
from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from libs.config import BaseAppSettings, get_default_settings
from libs.observability import setup_observability
from libs.schemas import ErrorResponse
from libs.security import get_current_user, get_tenant_id
from libs.security.middleware import TrustedProxyMiddleware
def create_trusted_proxy_middleware(
internal_cidrs: list[str], disable_auth: bool = False
) -> TrustedProxyMiddleware:
"""Create a TrustedProxyMiddleware instance with the given internal CIDRs."""
# This is a factory function that will be called by FastAPI's add_middleware
# We return a partial function that creates the middleware
def middleware_factory(app: Any) -> TrustedProxyMiddleware:
return TrustedProxyMiddleware(app, internal_cidrs, disable_auth)
return middleware_factory # type: ignore
def create_app( # pylint: disable=too-many-arguments,too-many-positional-arguments
service_name: str,
title: str,
description: str,
version: str = "1.0.0",
settings_class: type[BaseAppSettings] = BaseAppSettings,
custom_settings: dict[str, Any] | None = None,
startup_hooks: list[Callable[[], Awaitable[None]]] | None = None,
shutdown_hooks: list[Callable[[], Awaitable[None]]] | None = None,
) -> tuple[FastAPI, BaseAppSettings]:
"""Create a FastAPI application with standard configuration"""
# Create settings
settings_kwargs = {"service_name": service_name}
if custom_settings:
settings_kwargs.update(custom_settings)
settings = get_default_settings(**settings_kwargs)
if settings_class != BaseAppSettings:
# Use custom settings class
settings = settings_class(**settings_kwargs) # type: ignore
# Create lifespan context manager
@asynccontextmanager
async def lifespan(
app: FastAPI,
) -> AsyncIterator[None]: # pylint: disable=unused-argument
# Startup
setup_observability(settings)
if startup_hooks:
for hook in startup_hooks:
await hook()
yield
# Shutdown
if shutdown_hooks:
for hook in shutdown_hooks:
await hook()
# Create FastAPI app
app = FastAPI(
title=title, description=description, version=version, lifespan=lifespan
)
# Add middleware
app.add_middleware(
TrustedProxyMiddleware,
internal_cidrs=settings.internal_cidrs,
disable_auth=getattr(settings, "disable_auth", False),
)
# Add exception handlers
@app.exception_handler(HTTPException)
async def http_exception_handler(
request: Request, exc: HTTPException
) -> JSONResponse:
"""Handle HTTP exceptions with RFC7807 format"""
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
type=f"https://httpstatuses.com/{exc.status_code}",
title=exc.detail,
status=exc.status_code,
detail=exc.detail,
instance=str(request.url),
trace_id=getattr(request.state, "trace_id", None),
).model_dump(),
)
# Add health endpoints
@app.get("/healthz")
async def health_check() -> dict[str, str]:
"""Health check endpoint"""
return {
"status": "healthy",
"service": settings.service_name,
"version": version,
}
@app.get("/readyz")
async def readiness_check() -> dict[str, str]:
"""Readiness check endpoint"""
return {"status": "ready", "service": settings.service_name, "version": version}
@app.get("/livez")
async def liveness_check() -> dict[str, str]:
"""Liveness check endpoint"""
return {"status": "alive", "service": settings.service_name, "version": version}
return app, settings
# Dependency factories
def get_user_dependency() -> Any:
"""Get user dependency function"""
return get_current_user()
def get_tenant_dependency() -> Any:
"""Get tenant dependency function"""
return get_tenant_id()