from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.util import get_remote_address from app.config import settings from app.routers.extract import router as extract_router from app.routers.sanitize import router as sanitize_router from app.services.sanitizer import preload_pipeline def _get_real_ip(request: Request) -> str: """Extract the real client IP — honours X-Forwarded-For set by HF Spaces / reverse proxy.""" forwarded_for = request.headers.get("X-Forwarded-For") if forwarded_for: # X-Forwarded-For: client, proxy1, proxy2 — take the leftmost (real client) return forwarded_for.split(",")[0].strip() return get_remote_address(request) limiter = Limiter(key_func=_get_real_ip, default_limits=["100/minute"]) @asynccontextmanager async def lifespan(app: FastAPI): # Pre-load default model at startup; other models are lazy-loaded on first use preload_pipeline(settings.default_model_name) yield app = FastAPI( title="Privacy Text Sanitizer API", version="1.0.0", lifespan=lifespan, ) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware( CORSMiddleware, allow_origins=settings.allowed_origins, allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["Content-Type"], ) app.include_router(sanitize_router, prefix="/api") app.include_router(extract_router, prefix="/api") @app.get("/health") async def health() -> dict[str, str]: return {"status": "ok"}