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