Paperjet/backend/app/main.py
Elijah c545d4b17d
Some checks failed
Automated Container Build / build-and-push (push) Failing after 3s
CI / Backend (Python) (push) Failing after 28s
CI / Frontend (TypeScript) (push) Failing after 5m8s
Initial commit with Phase 0 Scaffolding
2026-06-10 18:28:17 -07:00

85 lines
2.5 KiB
Python

"""
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)