Mechanical only, and separated from the judgment calls that follow so the reviewable changes are not buried in a 98-file whitespace diff. 227 automatic fixes: 60 blank lines carrying whitespace, 60 unsorted import blocks, 34 Optional[X] to X | None, 28 unused imports, 16 deprecated typing imports, 12 datetime.timezone.utc to datetime.UTC, and assorted smaller modernisations. Then `ruff format` over src and tests: 98 files reformatted, 35 already conforming. No file among the unused-import findings defines __all__ or is an __init__.py, so nothing here removes a re-export. `make test`: 658 passed, unchanged from HEAD. Two things observed while verifying, neither addressed here: `pytest tests/` cannot collect — tests/e2e/test_orchestration_e2e.py uses an `e2e` marker that is not registered, and the config is strict about markers. This fails identically at HEAD, so it predates this change; `make test` passes because it ignores tests/e2e, tests/integration and tests/contracts. test_tatlock_tool_call_logging_calculator is flaky. It failed once in a full run with these changes and passed on the next, passes in isolation with them, and fails in isolation at HEAD. It is order- or timing-dependent, not a regression from this commit — established by running the full suite both ways rather than by reasoning about which change could have caused it. Co-Authored-By: Claude <noreply@anthropic.com>
188 lines
5.4 KiB
Python
188 lines
5.4 KiB
Python
"""
|
|
FastAPI application entry point.
|
|
Following best practices from github.com/zhanymkanov/fastapi-best-practices
|
|
|
|
Main responsibilities:
|
|
- Application configuration
|
|
- Middleware setup
|
|
- Exception handlers
|
|
- Router registration
|
|
- Lifecycle management
|
|
"""
|
|
|
|
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request, status
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from src.chat.router import router as chat_router
|
|
from src.core.config import config
|
|
from src.core.exceptions import AppException
|
|
from src.core.logging_config import get_logger
|
|
from src.core.router import router as core_router
|
|
from src.core.startup import initialize_application
|
|
from src.core.tracing_router import router as tracing_router
|
|
from src.models.router import router as models_router
|
|
from src.responses.router import router as responses_router
|
|
|
|
# Get structured logger
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
"""
|
|
Application lifespan manager.
|
|
|
|
Handles startup and shutdown logic.
|
|
"""
|
|
# Startup
|
|
logger.info(
|
|
"application_starting",
|
|
app_name=config.APP_NAME,
|
|
version=config.APP_VERSION,
|
|
environment=config.ENVIRONMENT.value,
|
|
prefer_cloud=config.PREFER_CLOUD_BACKEND,
|
|
anthropic_model=config.ANTHROPIC_MODEL,
|
|
ollama_host=str(config.OLLAMA_HOST),
|
|
ollama_model=config.OLLAMA_DEFAULT_MODEL,
|
|
redis_url=config.redis_memory_url,
|
|
log_format=config.log_format,
|
|
)
|
|
|
|
# Initialize application (check Claude health, register household members, etc.)
|
|
await initialize_application()
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
logger.info("application_shutdown")
|
|
|
|
|
|
def create_application() -> FastAPI:
|
|
"""
|
|
Application factory.
|
|
|
|
Creates and configures the FastAPI application.
|
|
Following best practice of using factory pattern.
|
|
"""
|
|
# Create app
|
|
application = FastAPI(
|
|
title=config.APP_NAME,
|
|
version=config.APP_VERSION,
|
|
lifespan=lifespan,
|
|
debug=config.DEBUG,
|
|
)
|
|
|
|
# Add middleware
|
|
application.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=config.CORS_ORIGINS,
|
|
allow_credentials=config.CORS_ALLOW_CREDENTIALS,
|
|
allow_methods=config.CORS_ALLOW_METHODS,
|
|
allow_headers=config.CORS_ALLOW_HEADERS,
|
|
)
|
|
|
|
# Register exception handlers
|
|
register_exception_handlers(application)
|
|
|
|
# Include routers
|
|
application.include_router(core_router) # Health and root endpoints
|
|
application.include_router(chat_router, prefix=config.API_PREFIX)
|
|
application.include_router(models_router, prefix=config.API_PREFIX)
|
|
application.include_router(responses_router, prefix=config.API_PREFIX) # Responses API
|
|
|
|
# Conditionally include tracing router (only in debug mode)
|
|
if config.DEBUG:
|
|
application.include_router(tracing_router)
|
|
logger.info("tracing_router_enabled")
|
|
|
|
return application
|
|
|
|
|
|
def register_exception_handlers(application: FastAPI) -> None:
|
|
"""
|
|
Register global exception handlers.
|
|
|
|
Provides consistent error responses compatible with OpenAI API.
|
|
"""
|
|
|
|
@application.exception_handler(AppException)
|
|
async def app_exception_handler(
|
|
request: Request,
|
|
exc: AppException,
|
|
) -> JSONResponse:
|
|
"""Handle custom application exceptions."""
|
|
logger.error(
|
|
"application_exception",
|
|
error_message=exc.message,
|
|
error_type=exc.__class__.__name__,
|
|
status_code=exc.status_code,
|
|
details=exc.details,
|
|
path=request.url.path,
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"error": {
|
|
"message": exc.message,
|
|
"type": exc.__class__.__name__,
|
|
"details": exc.details,
|
|
}
|
|
},
|
|
)
|
|
|
|
@application.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(
|
|
request: Request,
|
|
exc: RequestValidationError,
|
|
) -> JSONResponse:
|
|
"""Handle Pydantic validation errors."""
|
|
logger.error(
|
|
"validation_error",
|
|
errors=exc.errors(),
|
|
path=request.url.path,
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
content={
|
|
"error": {
|
|
"message": "Invalid request",
|
|
"type": "invalid_request_error",
|
|
"details": exc.errors(),
|
|
}
|
|
},
|
|
)
|
|
|
|
@application.exception_handler(Exception)
|
|
async def general_exception_handler(
|
|
request: Request,
|
|
exc: Exception,
|
|
) -> JSONResponse:
|
|
"""Handle unexpected exceptions."""
|
|
logger.exception(
|
|
"unexpected_error",
|
|
error_type=type(exc).__name__,
|
|
error_message=str(exc),
|
|
path=request.url.path,
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"error": {
|
|
"message": "An internal error occurred",
|
|
"type": "internal_server_error",
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
# Create application instance
|
|
app = create_application()
|