| import logging
|
| import asyncio
|
| from routers import health, predict, home
|
| from contextlib import asynccontextmanager
|
|
|
| from fastapi import FastAPI
|
|
|
|
|
| from models.artifacts_loader import get_artifacts
|
| from core.config import settings
|
| from core.logging import setup_logging
|
| from core.exceptions import (
|
| ModelPredictionError,
|
| ArtifactLoadError,
|
| generic_exception_handler,
|
| model_prediction_exception_handler,
|
| )
|
|
|
|
|
| setup_logging()
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
| @asynccontextmanager
|
| async def lifespan(app: FastAPI):
|
| """
|
| Lifespan context manager — non-blocking startup.
|
|
|
| Model loading is fired as a background task so the server yields
|
| immediately and can answer /health/liveness probes right away.
|
| Traffic is withheld naturally by /health/readiness (returns 503)
|
| until `artifacts.is_loaded` becomes True.
|
| """
|
| logger.info(f"Starting {settings.PROJECT_NAME} v{settings.VERSION}")
|
|
|
| async def _load():
|
| try:
|
| await asyncio.to_thread(get_artifacts().load_artifacts)
|
| except ArtifactLoadError as e:
|
| logger.error(f"Failed to load artifacts: {e.detail}")
|
| except Exception as e:
|
| logger.critical(f"Unhandled error loading artifacts: {e}")
|
|
|
|
|
| asyncio.create_task(_load())
|
|
|
| yield
|
|
|
|
|
| logger.info("Shutting down API...")
|
| get_artifacts().clear()
|
|
|
|
|
|
|
|
|
|
|
| app = FastAPI(
|
| title=settings.PROJECT_NAME,
|
| description=settings.DESCRIPTION,
|
| version=settings.VERSION,
|
| lifespan=lifespan,
|
| )
|
|
|
|
|
| app.add_exception_handler(Exception, generic_exception_handler)
|
| app.add_exception_handler(ModelPredictionError, model_prediction_exception_handler)
|
|
|
|
|
|
|
|
|
| API_V1_PREFIX = "/api/v1"
|
|
|
| app.include_router(home.router)
|
| app.include_router(health.router, prefix=API_V1_PREFIX)
|
| app.include_router(predict.router, prefix=API_V1_PREFIX) |