mlforge / main.py
senthil2421
arch: refactor cloud_backend into lean discovery server by removing execution logic
99e3f1b
"""
main.py β€” FastAPI application entry point.
Wires together all modules, registers middleware/routes, manages lifespan.
"""
from __future__ import annotations
import os
import sys
# Ensure backend root is in sys.path to resolve 'backend.*' imports correctly
# when running from the 'backend' directory.
backend_root = os.path.dirname(os.path.abspath(__file__))
if backend_root not in sys.path:
sys.path.insert(0, backend_root)
import asyncio
from contextlib import asynccontextmanager
from typing import AsyncIterator
import traceback
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from api.routes import models as models_router
from api.routes import sync as sync_router
from api.routes import datasets as datasets_router
from config import settings
from database.connection import close_db, get_db
from middleware.logging_middleware import RequestLoggingMiddleware
from observability.logger import configure_logging, get_logger
# ── Logging bootstrap (must be first) ─────────────────────────────────────────
configure_logging()
log = get_logger("main")
# ── Lifespan ──────────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
# Startup
settings.ensure_dirs()
log.info("startup", host=settings.host, port=settings.port, version=settings.version)
await get_db() # Bootstrap DB / run migrations
log.info("database_ready", path=str(settings.db_path))
if settings.auto_sync_on_startup:
from registry.registry import count_models
current = await count_models()
if current == 0:
from api.routes.sync import _run_full_sync
log.info("auto_sync_startup_triggered")
asyncio.create_task(_run_full_sync())
yield # ← app runs
# Shutdown
await close_db()
log.info("shutdown")
# ── Application ───────────────────────────────────────────────────────────────
app = FastAPI(
title="MLForge Cloud Registry",
version=settings.version,
description="Global Model and Dataset Discovery Service β€” The Brain of MLForge.",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# Log full traceback for debugging 500s.
log.error(
"unhandled_exception",
path=request.url.path,
error=str(exc),
traceback=traceback.format_exc(),
)
return JSONResponse(
status_code=500,
content={"detail": "Internal Server Error", "error": str(exc)},
)
# ── Middleware ─────────────────────────────────────────────────────────────────
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for the cloud registry to support SDK/CLI/UI
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(RequestLoggingMiddleware)
# ── Routes ────────────────────────────────────────────────────────────────────
app.include_router(models_router.router)
app.include_router(sync_router.router)
app.include_router(datasets_router.router)
@app.get("/health", tags=["system"])
async def health() -> dict:
from registry.registry import count_models
from datasets.registry import count_datasets
n_models = await count_models()
n_datasets = await count_datasets()
return {
"status": "ok",
"service": "cloud_registry",
"version": settings.version,
"model_count": n_models,
"dataset_count": n_datasets,
}
# ── Dev runner ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host=settings.host,
port=settings.port,
reload=settings.debug,
log_config=None, # We use structlog
)