smartvision-ai / api /main.py
Priya Monisha
fix: pass ruff lint+format, clean README, remove stray script
fa0ce5b
Raw History Blame Contribute Delete
4.22 kB
# filename: api/main.py
# purpose: FastAPI application β€” lifespan startup/shutdown + route registration.
#
# Rules applied:
# Rule 32: lifespan context manager (not deprecated @app.on_event)
# Rule 31: asyncio.get_running_loop() β€” event loop is already running inside async function
# Rule 23: Redis failure handled gracefully in RedisCache.__init__ (1s timeout)
# Rule 4: src/ never imports from api/ β€” dependency flows api/ β†’ src/ only
from __future__ import annotations
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI, Request, Response
import config as cfg
from api.prometheus_metrics import http_errors, models_loaded
from api.routes import classify, detect, drift, ensemble, health, metrics
from src.data.augmentor import get_eval_transforms
from src.inference.model_loader import load_all_models
from src.inference.redis_cache import RedisCache
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# Initialise state flags before model loading so /health can respond immediately
app.state.models_ready = False
app.state.models = {}
app.state.model_hashes = {}
# Rule 31: get_running_loop() β€” loop is already running in an async context.
# load_all_models is blocking (torch.load, HF Hub HTTP); run in thread pool so
# the event loop stays responsive during startup (/health returns 503 while loading).
loop = asyncio.get_running_loop()
try:
models, hashes = await loop.run_in_executor(
None, lambda: load_all_models(device="cpu")
)
app.state.models = models
app.state.model_hashes = hashes
app.state.models_ready = True
models_loaded.set(1)
logger.info(f"Models ready: {list(models.keys())}")
except Exception as e:
logger.error(f"Model loading failed: {e}")
# models_ready stays False β€” /health returns 503 on every request
# Redis β€” 1s socket_connect_timeout; disables cache gracefully if Redis is down
app.state.redis = RedisCache(
host=cfg.REDIS_HOST, port=cfg.REDIS_PORT, password=cfg.REDIS_PASSWORD
)
# Build eval transform once at startup β€” not per-request (avoids per-call allocation)
app.state.eval_transform = get_eval_transforms(image_size=cfg.IMAGE_SIZE)
# Drift detector β€” initialized after Redis so redis_client is available for buffer restore
from src.monitoring.drift_detector import DriftDetector
try:
app.state.drift_detector = DriftDetector(
baseline_path=cfg.DRIFT_BASELINE_PATH,
redis_client=app.state.redis,
min_samples=cfg.KS_MIN_LIVE_SAMPLES,
alert_threshold=cfg.KS_DRIFT_ALERT_THRESHOLD,
)
logger.info("DriftDetector initialized: 22-class KS monitoring active")
except FileNotFoundError as e:
logger.error(f"DriftDetector init failed (baseline missing): {e}")
app.state.drift_detector = None
yield
# Shutdown cleanup
app.state.models = {}
app.state.model_hashes = {}
app.state.models_ready = False
logger.info("SmartVision API shutdown complete.")
app = FastAPI(
title="SmartVision AI",
description=(
"Multi-class object recognition (ResNet50, MobileNetV2) "
"and detection (YOLOv8n) API."
),
version="1.0.0",
lifespan=lifespan,
)
@app.middleware("http")
async def count_http_errors(request: Request, call_next) -> Response:
"""Increment http_errors counter for any 4xx/5xx response."""
response = await call_next(request)
if response.status_code >= 400:
endpoint = request.url.path
http_errors.labels(
status_code=str(response.status_code),
endpoint=endpoint,
).inc()
return response
app.include_router(health.router)
app.include_router(classify.router)
app.include_router(ensemble.router)
app.include_router(detect.router)
app.include_router(drift.router)
app.include_router(metrics.router)