import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from minio import Minio from sqlalchemy import text from api.documents import router as documents_router from api.settings import router as settings_router from api.topics import router as topics_router from config import settings from db.session import AsyncSessionLocal, engine @asynccontextmanager async def lifespan(app: FastAPI): """FastAPI lifespan: create MinIO bucket at startup, dispose engine at shutdown. D-07: bucket auto-create ensures the docuvault bucket exists on every reboot. MinIO client stored on app.state.minio for use in the /health endpoint. """ # MinIO bucket initialization (RESEARCH.md Pattern 4) minio_client = Minio( settings.minio_endpoint, access_key=settings.minio_access_key, secret_key=settings.minio_secret_key, secure=False, ) exists = await asyncio.to_thread(minio_client.bucket_exists, settings.minio_bucket) if not exists: await asyncio.to_thread(minio_client.make_bucket, settings.minio_bucket) app.state.minio = minio_client yield # Shutdown: close all pooled connections await engine.dispose() app = FastAPI(title="Document Scanner API", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], # Phase 1: locked down in Phase 2 after auth lands allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") async def health(request: Request): """Extended health probe: reports PostgreSQL and MinIO connectivity (D-07). Always returns HTTP 200 — 'degraded' status signals a partial outage without causing load-balancer retries. Note (T-01-05-03): error strings expose Python exception class names — acceptable for an internal/dev endpoint in Phase 1. Phase 2 will trim to 'error' or 'unhealthy' once the endpoint is internet-facing. """ checks: dict = {} # PostgreSQL probe try: async with AsyncSessionLocal() as session: await session.execute(text("SELECT 1")) checks["postgres"] = "ok" except Exception as e: checks["postgres"] = f"error: {type(e).__name__}: {e}" # MinIO probe try: ok = await asyncio.to_thread( request.app.state.minio.bucket_exists, settings.minio_bucket ) checks["minio"] = "ok" if ok else "error: bucket missing" except Exception as e: checks["minio"] = f"error: {type(e).__name__}: {e}" status = "ok" if all(v == "ok" for v in checks.values()) else "degraded" return {"status": status, "checks": checks} app.include_router(documents_router) app.include_router(topics_router) app.include_router(settings_router)