kernelmind/Resume-Screener-API
0
1"""2Resume Screener API - Main Application Entry Point.3 4This is the FastAPI application factory with:5- Lifespan event for model preloading (not reloaded per request)6- SlowAPI rate limiting (prevents API abuse)7- Custom exception handlers (clean error responses)8- CORS middleware (if needed for web frontends)9 10Run with: uvicorn src.main:app --reload11"""12 13from contextlib import asynccontextmanager14from typing import AsyncGenerator15 16from fastapi import FastAPI, Request17from fastapi.middleware.cors import CORSMiddleware18from fastapi.responses import JSONResponse, FileResponse19from fastapi.staticfiles import StaticFiles20import os21from slowapi import Limiter, _rate_limit_exceeded_handler22from slowapi.errors import RateLimitExceeded23from slowapi.util import get_remote_address24 25from src.api.routes import screen_router, rank_router26from src.auth.routes import router as auth_router27from src.core.config import get_settings28from src.core.exceptions import (29 ResumeScreenerError,30 InvalidFileError,31 GeminiAPIError,32 resume_screener_error_handler,33 invalid_file_error_handler,34 gemini_api_error_handler,35 unhandled_exception_handler,36)37from src.core.logging import setup_logging, logger38from src.services.embedding_service import initialize_embedding_model39from src.database import init_db40 41 42# Initialize rate limiter43limiter = Limiter(key_func=get_remote_address)44 45 46@asynccontextmanager47async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:48 """49 Application lifespan handler.50 51 Runs once at startup (before any requests) and once at shutdown.52 We use this to:53 1. Configure logging54 2. Load the embedding model INTO MEMORY (avoid per-request loading)55 56 Why lifespan instead of @app.on_event("startup")?57 - on_event is deprecated in FastAPI 0.100+58 - lifespan gives us proper async context management59 - Cleaner resource cleanup on shutdown60 """61 # === STARTUP ===62 settings = get_settings()63 64 # Configure logging first65 setup_logging(level=settings.log_level)66 logger.info("Resume Screener API starting up...")67 68 # Load embedding model into memory69 # This takes 2-5 seconds but only happens ONCE at startup70 logger.info("Loading embedding model (this may take a few seconds)...")71 try:72 initialize_embedding_model()73 logger.info("Embedding model loaded successfully")74 except Exception as e:75 logger.error(f"Failed to load embedding model: {e}")76 77 # Initialize database78 logger.info("Initializing user database...")79 init_db()80 logger.info("Database ready")81 82 logger.info("Resume Screener API ready for requests")83 84 yield # Application runs here85 86 # === SHUTDOWN ===87 logger.info("Resume Screener API shutting down...")88 89 90# Create FastAPI app91app = FastAPI(92 title="Resume Screener API",93 description="""94 Production-grade Resume Screening & Ranking API.95 96 Upload a PDF resume and job description to get:97 - Similarity score (0-1)98 - Extracted skills and experience99 - Gap analysis showing what the candidate is missing100 """,101 version="1.0.0",102 lifespan=lifespan,103 docs_url="/docs",104 redoc_url="/redoc",105)106 107# === MIDDLEWARE ===108 109# Rate limiting110app.state.limiter = limiter111app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)112 113# CORS - fully permissive for development114app.add_middleware(115 CORSMiddleware,116 allow_origins=["*"],117 allow_credentials=False, # Disabled to allow wildcard origins118 allow_methods=["*"],119 allow_headers=["*"],120)121 122 123# === EXCEPTION HANDLERS ===124# Order matters - more specific handlers first125 126app.add_exception_handler(InvalidFileError, invalid_file_error_handler)127app.add_exception_handler(GeminiAPIError, gemini_api_error_handler)128app.add_exception_handler(ResumeScreenerError, resume_screener_error_handler)129app.add_exception_handler(Exception, unhandled_exception_handler)130 131 132# === ROUTES ===133 134app.include_router(auth_router) # Auth routes (public)135app.include_router(screen_router)136app.include_router(rank_router)137 138 139# Root endpoint for quick verification140@app.get("/api/v1/health", tags=["health"])141async def health() -> dict:142 """Health check endpoint."""143 return {144 "status": "healthy",145 "service": "Resume Screener API",146 }147 148 149# Mount the frontend static files150# In Docker, the frontend will be built to /app/frontend-next/out151static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend-next", "out")152 153if os.path.exists(static_dir):154 app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")155 156 @app.exception_handler(404)157 async def not_found_handler(request: Request, exc: Exception):158 """Handle 404s by serving index.html (required for SPAs)."""159 return FileResponse(os.path.join(static_dir, "index.html"))160else:161 @app.get("/", tags=["root"])162 async def root() -> dict:163 """Root endpoint with API info (when frontend is not built)."""164 return {165 "service": "Resume Screener API",166 "version": "1.0.0",167 "docs": "/docs",168 "health": "/api/v1/health",169 }170 