""" PaperJet backend — FastAPI application factory. This is the main entry point. The lifespan handler initializes the database and ensures required directories exist on startup. """ from contextlib import asynccontextmanager from collections.abc import AsyncGenerator from typing import Any from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from app.api.v1 import router as v1_router from app.config import settings from app.db import Base, engine @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Application lifespan: startup and shutdown tasks.""" # Create all tables (Alembic handles migrations in production, # but this ensures tables exist for development/first run) Base.metadata.create_all(bind=engine) # Ensure storage directories exist settings.PDF_STORAGE_PATH.mkdir(parents=True, exist_ok=True) settings.THUMBNAILS_PATH.mkdir(parents=True, exist_ok=True) settings.DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True) yield # Shutdown: dispose of the engine engine.dispose() app = FastAPI( title="PaperJet", description="Self-hosted PDF editor API", version=settings.APP_VERSION, docs_url="/api/v1/docs" if settings.DEBUG else None, redoc_url="/api/v1/redoc" if settings.DEBUG else None, openapi_url="/api/v1/openapi.json", lifespan=lifespan, ) # --- Error handlers --- @app.exception_handler(404) async def not_found_handler(request: Request, exc: Any) -> JSONResponse: """Consistent 404 error envelope.""" return JSONResponse( status_code=404, content={"error": {"code": "not_found", "message": "Resource not found"}}, ) @app.exception_handler(422) async def validation_error_handler(request: Request, exc: Any) -> JSONResponse: """Consistent validation error envelope.""" return JSONResponse( status_code=422, content={ "error": { "code": "validation_error", "message": "Request validation failed", "details": exc.errors() if hasattr(exc, "errors") else str(exc), } }, ) @app.exception_handler(500) async def internal_error_handler(request: Request, exc: Any) -> JSONResponse: """Consistent 500 error envelope.""" return JSONResponse( status_code=500, content={"error": {"code": "internal_error", "message": "Internal server error"}}, ) # --- Mount routers --- app.include_router(v1_router)