Files
2026-08-07 17:19:49 +02:00

171 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""
db.py — Connexion SQLite + écriture des relevés de coûts.
Utilisé par collect.py (collecte journalière) et import_historical.py
(import ponctuel). Le schéma vit dans db/schema.sql.
"""
from __future__ import annotations
import os
import sqlite3
from pathlib import Path
DEFAULT_DB_PATH = "cost_dashboard.db"
SCHEMA_PATH = Path(__file__).parent.parent / "db" / "schema.sql"
def _resolve_db_path(db_path: str | None = None) -> str:
return db_path or os.environ.get("COST_DASHBOARD_DB") or DEFAULT_DB_PATH
def init_db(db_path: str | None = None) -> None:
"""Crée le fichier SQLite et applique le schéma s'il n'existe pas déjà."""
path = _resolve_db_path(db_path)
Path(path).parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path)
try:
conn.executescript(SCHEMA_PATH.read_text())
conn.commit()
finally:
conn.close()
def get_connection(db_path: str | None = None) -> sqlite3.Connection:
"""Ouvre une connexion SQLite (foreign keys activées, row factory par nom)."""
path = _resolve_db_path(db_path)
conn = sqlite3.connect(path)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def upsert_collection(
conn: sqlite3.Connection,
provider: str,
tenant_name: str,
granularity: str,
period_start: str,
period_end: str,
collected_at: str,
status: str,
data: dict | None = None,
error_message: str | None = None,
) -> int:
"""Insère ou remplace un relevé (provider, tenant_name, granularity,
period_start) et sa répartition par service. `data` est le dict retourné
par providers.get_aws_cost/get_azure_cost (+ convert_result) : total,
currency, by_service, original_total, original_currency, exchange_rate.
"""
data = data or {}
conn.execute(
"""
INSERT INTO collections (
provider, tenant_name, granularity, period_start, period_end,
collected_at, status, error_message,
total_amount, currency, original_total, original_currency, exchange_rate
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(provider, tenant_name, granularity, period_start) DO UPDATE SET
period_end = excluded.period_end,
collected_at = excluded.collected_at,
status = excluded.status,
error_message = excluded.error_message,
total_amount = excluded.total_amount,
currency = excluded.currency,
original_total = excluded.original_total,
original_currency = excluded.original_currency,
exchange_rate = excluded.exchange_rate
""",
(
provider, tenant_name, granularity, period_start, period_end,
collected_at, status, error_message,
data.get("total"), data.get("currency"),
data.get("original_total"), data.get("original_currency"), data.get("exchange_rate"),
),
)
# last_insert_rowid() n'est pas fiable ici : sur la branche ON CONFLICT DO
# UPDATE (pas de nouvelle ligne créée), SQLite ne le met à jour que si
# aucun autre INSERT n'a eu lieu sur la connexion depuis — ce qui n'est
# pas garanti (ex: service_costs d'un tenant précédent). On relit l'id
# explicitement via la clé unique.
collection_id = conn.execute(
"""
SELECT id FROM collections
WHERE provider = ? AND tenant_name = ? AND granularity = ? AND period_start = ?
""",
(provider, tenant_name, granularity, period_start),
).fetchone()["id"]
conn.execute("DELETE FROM service_costs WHERE collection_id = ?", (collection_id,))
by_service = data.get("by_service") or {}
if by_service:
conn.executemany(
"INSERT INTO service_costs (collection_id, service_name, amount) VALUES (?, ?, ?)",
[(collection_id, service, amount) for service, amount in by_service.items()],
)
conn.commit()
return collection_id
def set_resource_collection_status(
conn: sqlite3.Connection,
provider: str,
tenant_name: str,
status: str,
collected_at: str,
message: str | None = None,
) -> None:
conn.execute(
"""
INSERT INTO resource_collection_status (provider, tenant_name, status, message, collected_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(provider, tenant_name) DO UPDATE SET
status = excluded.status,
message = excluded.message,
collected_at = excluded.collected_at
""",
(provider, tenant_name, status, message, collected_at),
)
conn.commit()
def upsert_resource_costs(
conn: sqlite3.Connection,
provider: str,
tenant_name: str,
period_start: str,
period_end: str,
services: dict,
) -> None:
"""Remplace le cliché de coûts par ressource pour ce tenant (voir
resource_costs dans le schéma : c'est une photo glissante, pas un
historique — on supprime l'ancien cliché avant d'écrire le nouveau).
`services` : {service_name: [{resource_id, resource_name, amount}, ...]},
la forme renvoyée par providers.get_*_resource_costs.
"""
conn.execute(
"DELETE FROM resource_costs WHERE provider = ? AND tenant_name = ?",
(provider, tenant_name),
)
rows = [
(provider, tenant_name, service, r["resource_id"], r["resource_name"], period_start, period_end, r["amount"])
for service, resources in services.items()
for r in resources
]
if rows:
conn.executemany(
"""
INSERT INTO resource_costs (
provider, tenant_name, service_name, resource_id, resource_name,
period_start, period_end, amount
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
rows,
)
conn.commit()