init projet
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# ============================================================
|
||||
# config.py — Configuration & connexion SQL Server
|
||||
# Data Sentinel | COYAUD Anthony | 2026
|
||||
# ============================================================
|
||||
|
||||
import pyodbc
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
class Config:
|
||||
# Chaîne de connexion SQL Server
|
||||
DB_CONNECTION_STRING = (
|
||||
"Driver={ODBC Driver 17 for SQL Server};"
|
||||
"Server=LaptopCA\\SQLEXPRESS;"
|
||||
"Database=DataSentinel;"
|
||||
"Trusted_Connection=yes;"
|
||||
)
|
||||
|
||||
# Paramètres API
|
||||
API_TITLE = "Data Sentinel API"
|
||||
API_VERSION = "1.0.0"
|
||||
API_DESCRIPTION = "API de monitoring de la qualité des données — XEFI"
|
||||
|
||||
# Sécurité JWT
|
||||
SECRET_KEY = "data-sentinel-secret-change-in-prod"
|
||||
ALGORITHM = "HS256"
|
||||
TOKEN_EXPIRE_MINUTES = 60
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Connexion — retourne un curseur pyodbc via context manager
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
def get_connection() -> pyodbc.Connection:
|
||||
"""Ouvre une connexion SQL Server et la retourne."""
|
||||
return pyodbc.connect(Config.DB_CONNECTION_STRING)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_cursor():
|
||||
"""
|
||||
Context manager : ouvre connexion + curseur, commit ou rollback,
|
||||
ferme proprement.
|
||||
|
||||
Usage :
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute("SELECT ...")
|
||||
rows = cursor.fetchall()
|
||||
"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
yield cursor
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
@@ -0,0 +1,585 @@
|
||||
# ============================================================
|
||||
# main.py — API FastAPI Data Sentinel
|
||||
# COYAUD Anthony | 2026
|
||||
# V2 : une table dédiée par monitoring
|
||||
#
|
||||
# Lancement : uvicorn main:app --reload --port 8000
|
||||
# Swagger : http://localhost:8000/docs
|
||||
# ============================================================
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
|
||||
from config import Config, get_cursor
|
||||
|
||||
# ============================================================
|
||||
# Initialisation
|
||||
# ============================================================
|
||||
|
||||
app = FastAPI(
|
||||
title = Config.API_TITLE,
|
||||
version = Config.API_VERSION,
|
||||
description = Config.API_DESCRIPTION,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins = ["http://localhost:5173", "http://localhost:3000"],
|
||||
allow_methods = ["GET"],
|
||||
allow_headers = ["*"],
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Mapping id_monito → table SQL dédiée
|
||||
# Pour ajouter un monitoring : ajouter une entrée ici.
|
||||
# ============================================================
|
||||
|
||||
MONITO_TABLES: dict[int, str] = {
|
||||
1: "MONITO_TIERS_PAYEURS",
|
||||
2: "MONITO_PRELEVEMENT_SANS_RIB",
|
||||
3: "MONITO_DOM_TOM_TVA",
|
||||
4: "MONITO_ZONE_VENTES_CRM",
|
||||
5: "MONITO_MULTI_MODES_REGLEMENT",
|
||||
6: "MONITO_SANS_SIRET",
|
||||
7: "MONITO_SANS_CONTREPARTIE",
|
||||
}
|
||||
|
||||
# Colonnes de recherche textuelle par table (pour le filtre search)
|
||||
SEARCH_COLS: dict[str, list[str]] = {
|
||||
"MONITO_TIERS_PAYEURS": ["CT_Intitule", "origineClient", "STE"],
|
||||
"MONITO_PRELEVEMENT_SANS_RIB": ["CT_Intitule", "STE"],
|
||||
"MONITO_DOM_TOM_TVA": ["CT_Intitule", "agence", "CT_Pays"],
|
||||
"MONITO_ZONE_VENTES_CRM": ["name", "Agence", "xefi_sagedatabase"],
|
||||
"MONITO_MULTI_MODES_REGLEMENT": ["CT_Intitule", "Agence"],
|
||||
"MONITO_SANS_SIRET": ["ct_intitule", "agence", "ct_num"],
|
||||
"MONITO_SANS_CONTREPARTIE": ["agence", "EC_Piece", "CT_NumCont"],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers
|
||||
# ============================================================
|
||||
|
||||
def rows_to_list(cursor, rows) -> list[dict]:
|
||||
"""Convertit les lignes pyodbc en liste de dicts."""
|
||||
cols = [col[0] for col in cursor.description]
|
||||
return [dict(zip(cols, r)) for r in rows]
|
||||
|
||||
|
||||
def row_to_dict(cursor, row) -> dict:
|
||||
"""Convertit une ligne pyodbc en dict."""
|
||||
cols = [col[0] for col in cursor.description]
|
||||
return dict(zip(cols, row))
|
||||
|
||||
|
||||
def get_table_name(id_monito: int) -> str:
|
||||
"""Retourne le nom de la table dédiée. Lève 404 si inconnu."""
|
||||
table = MONITO_TABLES.get(id_monito)
|
||||
if not table:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Monitoring {id_monito} introuvable. IDs valides : {list(MONITO_TABLES.keys())}"
|
||||
)
|
||||
return table
|
||||
|
||||
|
||||
def serialize_row(row: dict) -> dict:
|
||||
"""Convertit les types non-JSON (date, Decimal) en types sérialisables."""
|
||||
result = {}
|
||||
for k, v in row.items():
|
||||
if v is None:
|
||||
result[k] = None
|
||||
elif hasattr(v, 'isoformat'):
|
||||
result[k] = v.isoformat()
|
||||
else:
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# RÉFÉRENTIELS
|
||||
# ============================================================
|
||||
|
||||
@app.get("/categories", tags=["Référentiels"])
|
||||
def get_categories():
|
||||
"""Toutes les catégories de monitoring."""
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id_categorie, intitule_categorie "
|
||||
"FROM CATEGORIE ORDER BY intitule_categorie"
|
||||
)
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
@app.get("/services", tags=["Référentiels"])
|
||||
def get_services():
|
||||
"""Tous les services."""
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute("SELECT id_service, nom_service FROM SERVICE ORDER BY nom_service")
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
@app.get("/contacts", tags=["Référentiels"])
|
||||
def get_contacts(
|
||||
id_service: Optional[int] = Query(None, description="Filtrer par service")
|
||||
):
|
||||
"""Contacts, filtrables par service."""
|
||||
with get_cursor() as cursor:
|
||||
if id_service:
|
||||
cursor.execute(
|
||||
"SELECT id_contact, id_service, intitule_contact, nom, prenom, mail "
|
||||
"FROM CONTACT WHERE id_service = ? ORDER BY nom",
|
||||
id_service
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"SELECT id_contact, id_service, intitule_contact, nom, prenom, mail "
|
||||
"FROM CONTACT ORDER BY nom"
|
||||
)
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# NOMENCLATURE
|
||||
# ============================================================
|
||||
|
||||
@app.get("/monitorings", tags=["Monitorings"])
|
||||
def get_monitorings(
|
||||
id_service : Optional[int] = Query(None, description="Filtrer par service"),
|
||||
id_categorie : Optional[int] = Query(None, description="Filtrer par catégorie"),
|
||||
):
|
||||
"""Liste des monitorings actifs avec table_source et bdd_source."""
|
||||
query = (
|
||||
"SELECT id_monito, monito_intitule, id_service, id_categorie, "
|
||||
"table_source, bdd_source "
|
||||
"FROM NOMENCLATURE_MONITO WHERE actif = 1"
|
||||
)
|
||||
params = []
|
||||
|
||||
if id_service:
|
||||
query += " AND id_service = ?"
|
||||
params.append(id_service)
|
||||
if id_categorie:
|
||||
query += " AND id_categorie = ?"
|
||||
params.append(id_categorie)
|
||||
|
||||
query += " ORDER BY id_monito"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
@app.get("/monitorings/{id_monito}", tags=["Monitorings"])
|
||||
def get_monitoring_by_id(id_monito: int):
|
||||
"""Détail d'un monitoring."""
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id_monito, monito_intitule, id_service, id_categorie, "
|
||||
"table_source, bdd_source "
|
||||
"FROM NOMENCLATURE_MONITO WHERE id_monito = ? AND actif = 1",
|
||||
id_monito
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail=f"Monitoring {id_monito} introuvable.")
|
||||
return row_to_dict(cursor, row)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DONNÉES DÉTAILLÉES — table dédiée par monitoring
|
||||
# ============================================================
|
||||
|
||||
@app.get("/monitorings/{id_monito}/details", tags=["Monitorings"])
|
||||
def get_monitoring_details(
|
||||
id_monito : int,
|
||||
search : Optional[str] = Query(
|
||||
None,
|
||||
description="Recherche sur colonnes texte (ct_intitule, agence, STE...)"
|
||||
),
|
||||
limit : int = Query(500, ge=1, le=5000, description="Lignes max — mettre 5000 pour export CSV complet"),
|
||||
offset : int = Query(0, ge=0, description="Offset pagination"),
|
||||
):
|
||||
"""
|
||||
Retourne toutes les lignes en erreur depuis la table MONITO_[NOM] dédiée.
|
||||
Toutes les colonnes métier sont exposées → utilisable pour export CSV côté frontend.
|
||||
"""
|
||||
table = get_table_name(id_monito)
|
||||
query = f"SELECT * FROM {table} WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if search and table in SEARCH_COLS:
|
||||
cols = SEARCH_COLS[table]
|
||||
conditions = " OR ".join([f"{col} LIKE ?" for col in cols])
|
||||
query += f" AND ({conditions})"
|
||||
params.extend([f"%{search}%"] * len(cols))
|
||||
|
||||
query += " ORDER BY date_extraction DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"
|
||||
params += [offset, limit]
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
rows = rows_to_list(cursor, cursor.fetchall())
|
||||
return [serialize_row(r) for r in rows]
|
||||
|
||||
|
||||
@app.get("/monitorings/{id_monito}/count", tags=["Monitorings"])
|
||||
def get_monitoring_count(id_monito: int):
|
||||
"""Nombre d'erreurs dans la table dédiée du monitoring."""
|
||||
table = get_table_name(id_monito)
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table}")
|
||||
return {
|
||||
"id_monito" : id_monito,
|
||||
"table" : table,
|
||||
"nb_erreurs": cursor.fetchone()[0],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/monitorings/{id_monito}/columns", tags=["Monitorings"])
|
||||
def get_monitoring_columns(id_monito: int):
|
||||
"""
|
||||
Retourne les colonnes de la table dédiée.
|
||||
Permet au frontend de générer dynamiquement les en-têtes du tableau.
|
||||
"""
|
||||
table = get_table_name(id_monito)
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT COLUMN_NAME, DATA_TYPE "
|
||||
"FROM INFORMATION_SCHEMA.COLUMNS "
|
||||
"WHERE TABLE_NAME = ? ORDER BY ORDINAL_POSITION",
|
||||
table
|
||||
)
|
||||
return {
|
||||
"id_monito": id_monito,
|
||||
"table" : table,
|
||||
"columns" : rows_to_list(cursor, cursor.fetchall()),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DASHBOARD — VUE_CONSO
|
||||
# ============================================================
|
||||
|
||||
@app.get("/dashboard", tags=["Dashboard"])
|
||||
def get_dashboard(
|
||||
service : Optional[str] = Query(None, description="Filtrer par service"),
|
||||
categorie : Optional[str] = Query(None, description="Filtrer par catégorie"),
|
||||
):
|
||||
"""Vue consolidée — source principale du dashboard."""
|
||||
query = (
|
||||
"SELECT id_monito, nom_monito, nb_erreurs, service, categorie, bdd_source "
|
||||
"FROM VUE_CONSO WHERE 1=1"
|
||||
)
|
||||
params = []
|
||||
|
||||
if service:
|
||||
query += " AND service = ?"
|
||||
params.append(service)
|
||||
if categorie:
|
||||
query += " AND categorie = ?"
|
||||
params.append(categorie)
|
||||
|
||||
query += " ORDER BY nb_erreurs DESC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
@app.get("/dashboard/summary", tags=["Dashboard"])
|
||||
def get_dashboard_summary():
|
||||
"""KPI globaux pour les 4 cartes du dashboard."""
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) AS nb_monitorings,
|
||||
SUM(nb_erreurs) AS total_erreurs,
|
||||
MAX(nb_erreurs) AS max_erreurs,
|
||||
SUM(CASE WHEN nb_erreurs = 0 THEN 1 ELSE 0 END) AS monitorings_ok,
|
||||
SUM(CASE WHEN nb_erreurs > 0 THEN 1 ELSE 0 END) AS monitorings_en_erreur
|
||||
FROM VUE_CONSO
|
||||
""")
|
||||
summary = row_to_dict(cursor, cursor.fetchone())
|
||||
|
||||
cursor.execute(
|
||||
"SELECT TOP 1 id_monito, nom_monito, nb_erreurs, service, bdd_source "
|
||||
"FROM VUE_CONSO ORDER BY nb_erreurs DESC"
|
||||
)
|
||||
top = cursor.fetchone()
|
||||
summary["monitoring_critique"] = row_to_dict(cursor, top) if top else None
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
# ============================================================
|
||||
# HISTORIQUE — TABLE_FINAL
|
||||
# ============================================================
|
||||
|
||||
@app.get("/historique", tags=["Historique"])
|
||||
def get_historique(
|
||||
id_monito : Optional[int] = Query(None),
|
||||
date_debut : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
date_fin : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
service : Optional[str] = Query(None),
|
||||
):
|
||||
"""Snapshots journaliers (TABLE_FINAL)."""
|
||||
query = (
|
||||
"SELECT id_monito, nom_monito, nb_erreurs, service, categorie, bdd_source, "
|
||||
"CONVERT(NVARCHAR, date_sauvegarde, 23) AS date_sauvegarde "
|
||||
"FROM TABLE_FINAL WHERE 1=1"
|
||||
)
|
||||
params = []
|
||||
|
||||
if id_monito:
|
||||
query += " AND id_monito = ?"
|
||||
params.append(id_monito)
|
||||
if date_debut:
|
||||
query += " AND date_sauvegarde >= ?"
|
||||
params.append(str(date_debut))
|
||||
if date_fin:
|
||||
query += " AND date_sauvegarde <= ?"
|
||||
params.append(str(date_fin))
|
||||
if service:
|
||||
query += " AND service = ?"
|
||||
params.append(service)
|
||||
|
||||
query += " ORDER BY date_sauvegarde ASC, id_monito ASC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
@app.get("/historique/{id_monito}/evolution", tags=["Historique"])
|
||||
def get_evolution(
|
||||
id_monito : int,
|
||||
date_debut : Optional[date] = Query(None),
|
||||
date_fin : Optional[date] = Query(None),
|
||||
):
|
||||
"""Évolution d'un monitoring — format optimisé Recharts."""
|
||||
get_table_name(id_monito) # valide l'existence du monitoring
|
||||
|
||||
query = (
|
||||
"SELECT CONVERT(NVARCHAR, date_sauvegarde, 23) AS date, nb_erreurs "
|
||||
"FROM TABLE_FINAL WHERE id_monito = ?"
|
||||
)
|
||||
params = [id_monito]
|
||||
|
||||
if date_debut:
|
||||
query += " AND date_sauvegarde >= ?"
|
||||
params.append(str(date_debut))
|
||||
if date_fin:
|
||||
query += " AND date_sauvegarde <= ?"
|
||||
params.append(str(date_fin))
|
||||
|
||||
query += " ORDER BY date_sauvegarde ASC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
data = [{"date": r[0], "nb_erreurs": r[1]} for r in cursor.fetchall()]
|
||||
|
||||
return {"id_monito": id_monito, "points": len(data), "evolution": data}
|
||||
|
||||
|
||||
@app.get("/historique/comparaison", tags=["Historique"])
|
||||
def get_comparaison(
|
||||
date_debut : Optional[date] = Query(None),
|
||||
date_fin : Optional[date] = Query(None),
|
||||
):
|
||||
"""
|
||||
Déprécié — utiliser GET /evolution/par-monitoring à la place.
|
||||
Conservé pour compatibilité ascendante.
|
||||
"""
|
||||
query = (
|
||||
"SELECT CONVERT(NVARCHAR, date_sauvegarde, 23) AS date, "
|
||||
"id_monito, nom_monito, nb_erreurs, service "
|
||||
"FROM TABLE_FINAL WHERE 1=1"
|
||||
)
|
||||
params = []
|
||||
|
||||
if date_debut:
|
||||
query += " AND date_sauvegarde >= ?"
|
||||
params.append(str(date_debut))
|
||||
if date_fin:
|
||||
query += " AND date_sauvegarde <= ?"
|
||||
params.append(str(date_fin))
|
||||
|
||||
query += " ORDER BY date_sauvegarde ASC, id_monito ASC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
return rows_to_list(cursor, cursor.fetchall())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ÉVOLUTION GLOBALE — VUE_TABLE_FINAL_CONSO
|
||||
# Vue qui agrège TABLE_FINAL en 3 niveaux :
|
||||
# GLOBAL → total toutes monitorings confondues (courbe principale)
|
||||
# MONITO → détail par monitoring (courbes individuelles)
|
||||
# SERVICE → regroupement par service (Contrat / Fournisseur)
|
||||
# ============================================================
|
||||
|
||||
@app.get("/evolution/global", tags=["Évolution globale"])
|
||||
def get_evolution_global(
|
||||
date_debut : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
date_fin : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
):
|
||||
"""
|
||||
Courbe globale : total des erreurs tous monitorings confondus, par jour.
|
||||
Source : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'GLOBAL'
|
||||
Format optimisé Recharts → [{date, nb_erreurs}]
|
||||
"""
|
||||
query = (
|
||||
"SELECT date_sauvegarde AS date, nb_erreurs "
|
||||
"FROM VUE_TABLE_FINAL_CONSO "
|
||||
"WHERE type_agregat = 'GLOBAL'"
|
||||
)
|
||||
params = []
|
||||
|
||||
if date_debut:
|
||||
query += " AND date_sauvegarde >= ?"
|
||||
params.append(str(date_debut))
|
||||
if date_fin:
|
||||
query += " AND date_sauvegarde <= ?"
|
||||
params.append(str(date_fin))
|
||||
|
||||
query += " ORDER BY date_sauvegarde ASC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
data = [{"date": r[0], "nb_erreurs": r[1]} for r in cursor.fetchall()]
|
||||
|
||||
return {
|
||||
"type" : "GLOBAL",
|
||||
"points" : len(data),
|
||||
"series" : data,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/evolution/par-monitoring", tags=["Évolution globale"])
|
||||
def get_evolution_par_monitoring(
|
||||
date_debut : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
date_fin : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
id_monito : Optional[int] = Query(None, description="Filtrer sur un seul monitoring"),
|
||||
):
|
||||
"""
|
||||
Évolution par monitoring — toutes les courbes individuelles.
|
||||
Source : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'MONITO'
|
||||
|
||||
Retourne un format pivot par monitoring :
|
||||
{
|
||||
"monitorings": [{"id": 1, "nom": "...", "serie": [{"date","nb_erreurs"}]}]
|
||||
}
|
||||
Utile pour un graphique multi-lignes Recharts (une ligne par monitoring).
|
||||
"""
|
||||
query = (
|
||||
"SELECT date_sauvegarde AS date, id_monito, nom_monito, nb_erreurs "
|
||||
"FROM VUE_TABLE_FINAL_CONSO "
|
||||
"WHERE type_agregat = 'MONITO'"
|
||||
)
|
||||
params = []
|
||||
|
||||
if date_debut:
|
||||
query += " AND date_sauvegarde >= ?"
|
||||
params.append(str(date_debut))
|
||||
if date_fin:
|
||||
query += " AND date_sauvegarde <= ?"
|
||||
params.append(str(date_fin))
|
||||
if id_monito:
|
||||
query += " AND id_monito = ?"
|
||||
params.append(id_monito)
|
||||
|
||||
query += " ORDER BY id_monito ASC, date_sauvegarde ASC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Pivot : regrouper par monitoring
|
||||
pivot: dict[int, dict] = {}
|
||||
for row in rows:
|
||||
date, mid, nom, nb = row[0], row[1], row[2], row[3]
|
||||
if mid not in pivot:
|
||||
pivot[mid] = {"id_monito": mid, "nom_monito": nom, "serie": []}
|
||||
pivot[mid]["serie"].append({"date": date, "nb_erreurs": nb})
|
||||
|
||||
return {
|
||||
"type" : "MONITO",
|
||||
"nb_series" : len(pivot),
|
||||
"monitorings" : list(pivot.values()),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/evolution/par-service", tags=["Évolution globale"])
|
||||
def get_evolution_par_service(
|
||||
date_debut : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
date_fin : Optional[date] = Query(None, description="YYYY-MM-DD"),
|
||||
):
|
||||
"""
|
||||
Évolution par service (Contrat / Fournisseur) — 1 courbe par service.
|
||||
Source : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'SERVICE'
|
||||
|
||||
Retourne :
|
||||
{
|
||||
"services": [{"service": "Contrat", "serie": [{"date","nb_erreurs"}]}]
|
||||
}
|
||||
"""
|
||||
query = (
|
||||
"SELECT date_sauvegarde AS date, nom_monito AS service, nb_erreurs "
|
||||
"FROM VUE_TABLE_FINAL_CONSO "
|
||||
"WHERE type_agregat = 'SERVICE'"
|
||||
)
|
||||
params = []
|
||||
|
||||
if date_debut:
|
||||
query += " AND date_sauvegarde >= ?"
|
||||
params.append(str(date_debut))
|
||||
if date_fin:
|
||||
query += " AND date_sauvegarde <= ?"
|
||||
params.append(str(date_fin))
|
||||
|
||||
query += " ORDER BY nom_monito ASC, date_sauvegarde ASC"
|
||||
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute(query, *params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Pivot par service
|
||||
pivot: dict[str, list] = {}
|
||||
for row in rows:
|
||||
date, service, nb = row[0], row[1], row[2]
|
||||
if service not in pivot:
|
||||
pivot[service] = []
|
||||
pivot[service].append({"date": date, "nb_erreurs": nb})
|
||||
|
||||
return {
|
||||
"type" : "SERVICE",
|
||||
"nb_series": len(pivot),
|
||||
"services" : [{"service": s, "serie": v} for s, v in pivot.items()],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SANTÉ
|
||||
# ============================================================
|
||||
|
||||
@app.get("/health", tags=["Système"])
|
||||
def health_check():
|
||||
"""Ping API + test connexion SQL Server."""
|
||||
try:
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.fetchone()
|
||||
db_status = "ok"
|
||||
except Exception as e:
|
||||
db_status = f"error: {str(e)}"
|
||||
|
||||
return {
|
||||
"api" : "ok",
|
||||
"database" : db_status,
|
||||
"version" : Config.API_VERSION,
|
||||
"nb_monitorings" : len(MONITO_TABLES),
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Configuration
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
def test_get_dashboard():
|
||||
print(f"--- Test de l'endpoint: {BASE_URL}/dashboard ---")
|
||||
|
||||
try:
|
||||
# 1. Envoi de la requête
|
||||
response = requests.get(f"{BASE_URL}/dashboard")
|
||||
|
||||
# 2. Vérification du code statut
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
if not data:
|
||||
print("L'API a répondu avec succès, mais le tableau est vide (VUE_CONSO ne contient rien).")
|
||||
return
|
||||
|
||||
# 3. Affichage formaté du tableau
|
||||
print(f"{'ID':<5} | {'NOM DU MONITORING':<40} | {'ERREURS':<8} | {'SERVICE':<15}")
|
||||
print("-" * 75)
|
||||
|
||||
for item in data:
|
||||
print(f"{item['id_monito']:<5} | {item['nom_monito'][:38]:<40} | {item['nb_erreurs']:<8} | {item['service']:<15}")
|
||||
|
||||
print(f"\nTotal de lignes récupérées : {len(data)}")
|
||||
|
||||
else:
|
||||
print(f"Erreur lors de la requête : {response.status_code}")
|
||||
print(response.text)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("Erreur : Impossible de se connecter à l'API. Est-ce qu'uvicorn est lancé ?")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Petit check de santé avant de tester les données
|
||||
print("Vérification de l'état de l'API...")
|
||||
health = requests.get(f"{BASE_URL}/health").json()
|
||||
print(f"Statut API : {health['api']} | Statut DB : {health['database']}\n")
|
||||
|
||||
if health['database'] == "ok":
|
||||
test_get_dashboard()
|
||||
else:
|
||||
print("Abandon du test : La base de données n'est pas accessible.")
|
||||
Reference in New Issue
Block a user