#!/usr/bin/env python3 """ main.py — App FastAPI : pages statiques (page globale + détail tenant), API JSON pour les graphiques, et /health pour Kubernetes. """ from __future__ import annotations import sys from contextlib import asynccontextmanager from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from fastapi import FastAPI, HTTPException, Query from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles import queries FRONTEND_DIR = Path(__file__).parent.parent / "frontend" PROVIDERS = ("aws", "azure") @asynccontextmanager async def lifespan(app: FastAPI): queries.init_db() yield app = FastAPI(title="simple-cost-dashboard", lifespan=lifespan) app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR / "static")), name="static") def _read_page(filename: str) -> str: return (FRONTEND_DIR / filename).read_text() def _require_provider(provider: str) -> None: if provider not in PROVIDERS: raise HTTPException(status_code=404, detail=f"Provider inconnu : {provider}") @app.get("/", response_class=HTMLResponse) def index_page(): return _read_page("index.html") @app.get("/tenant/{provider}/{name}", response_class=HTMLResponse) def tenant_page(provider: str, name: str): _require_provider(provider) return _read_page("tenant.html") @app.get("/health") def health(): try: conn = queries.get_connection() try: conn.execute("SELECT 1") finally: conn.close() except Exception as e: raise HTTPException(status_code=503, detail=f"Base indisponible : {e}") return {"status": "ok"} @app.get("/api/overview") def api_overview(months: int = Query(12, ge=1, le=24)): conn = queries.get_connection() try: return queries.overview(conn, months=months) finally: conn.close() @app.get("/api/tenant/{provider}/{name}") def api_tenant_detail(provider: str, name: str, range: int = Query(30, alias="range")): _require_provider(provider) if range not in (7, 30, 365): raise HTTPException(status_code=400, detail="range doit être 7, 30 ou 365") conn = queries.get_connection() try: return queries.tenant_detail(conn, provider, name, range_days=range) finally: conn.close() @app.get("/api/tenant/{provider}/{name}/services") def api_tenant_services_selection( provider: str, name: str, days: str = Query(None), months: str = Query(None), ): _require_provider(provider) conn = queries.get_connection() try: if months: selected = [m for m in months.split(",") if m] return {"services": queries.services_for_months(conn, provider, name, selected)} if days: selected = [d for d in days.split(",") if d] return {"services": queries.services_for_days(conn, provider, name, selected)} raise HTTPException(status_code=400, detail="'days' ou 'months' requis") finally: conn.close() @app.get("/api/tenant/{provider}/{name}/resources/{service_name}") def api_resource_detail(provider: str, name: str, service_name: str): _require_provider(provider) conn = queries.get_connection() try: return queries.resource_detail(conn, provider, name, service_name) finally: conn.close()