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