first (big) draft
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user