first (big) draft

This commit is contained in:
2026-08-07 17:19:49 +02:00
parent 2974a3a4cd
commit 9e78dfa4f2
24 changed files with 3975 additions and 1 deletions
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
tenant_detail.py — Détail des coûts par famille de service (S3, EC2, ECS,
Fargate, etc.) pour UN tenant spécifique, converti en EUR par défaut.
Ce script réutilise providers.py (accès AWS/Azure) et default_period() de
cost_report.py : il doit rester dans le même dossier que ces deux modules.
Utilisation :
python tenant_detail.py --tenant analytics-platform
python tenant_detail.py --tenant client-c-prod --start 2026-06-01 --end 2026-07-01
python tenant_detail.py --tenant analytics-platform --top 15
python tenant_detail.py --tenant analytics-platform --json
python tenant_detail.py --tenant analytics-platform --no-convert # devise d'origine
"""
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from providers import get_aws_cost, get_azure_cost, convert_result
from cost_report import default_period
def find_tenant(config: dict, tenant_name: str):
"""Cherche le tenant par son 'name' dans les sections aws/azure de la config.
Retourne (provider, entry) ou (None, None) si introuvable."""
for entry in config.get("aws", []):
if entry["name"] == tenant_name:
return "aws", entry
for entry in config.get("azure", []):
if entry["name"] == tenant_name:
return "azure", entry
return None, None
def main():
parser = argparse.ArgumentParser(
description="Détail des coûts par famille de service pour un tenant donné"
)
parser.add_argument("--tenant", required=True,
help="Nom du tenant tel que défini dans config.json")
parser.add_argument("--config", default="config.json", help="Chemin vers le fichier de config")
parser.add_argument("--start", help="Date de début (YYYY-MM-DD), défaut: 1er du mois en cours")
parser.add_argument("--end", help="Date de fin exclusive (YYYY-MM-DD), défaut: demain")
parser.add_argument("--currency", default="EUR", help="Devise cible (défaut: EUR)")
parser.add_argument("--no-convert", action="store_true",
help="Désactive la conversion, garde la devise d'origine")
parser.add_argument("--top", type=int, default=None,
help="N'afficher que les N services les plus coûteux")
parser.add_argument("--json", action="store_true", help="Sortie en JSON brut")
args = parser.parse_args()
try:
with open(args.config) as f:
config = json.load(f)
except FileNotFoundError:
print(f"Fichier de config introuvable : {args.config}", file=sys.stderr)
sys.exit(1)
provider, entry = find_tenant(config, args.tenant)
if entry is None:
available = [e["name"] for e in config.get("aws", [])] + \
[e["name"] for e in config.get("azure", [])]
print(f"Tenant '{args.tenant}' introuvable dans {args.config}", file=sys.stderr)
print(f"Tenants disponibles : {', '.join(available) or '(aucun)'}", file=sys.stderr)
sys.exit(1)
default_start, default_end = default_period()
start = args.start or default_start
end = args.end or default_end
try:
if provider == "aws":
data = get_aws_cost(entry, start, end)
else:
data = get_azure_cost(
entry["tenant_id"], entry["client_id"], entry["client_secret"],
entry["subscription_id"], start, end,
)
except Exception as e:
print(f"Erreur lors de la récupération des coûts pour '{args.tenant}' : {e}", file=sys.stderr)
sys.exit(1)
target_currency = None if args.no_convert else args.currency
if target_currency:
data = convert_result(data, target_currency)
if args.json:
print(json.dumps(
{"tenant": args.tenant, "provider": provider.upper(), "start": start, "end": end, **data},
indent=2, ensure_ascii=False,
))
return
print(f"\nDétail des coûts — {args.tenant} ({provider.upper()})")
print(f"Période : {start}{end}")
if data.get("original_currency") and data["original_currency"] != data["currency"]:
print(f"Total : {data['total']:.2f} {data['currency']} "
f"(orig. {data['original_total']:.2f} {data['original_currency']})")
else:
print(f"Total : {data['total']:.2f} {data['currency']}")
if data.get("conversion_error"):
print(f"[Conversion échouée, montant affiché dans sa devise d'origine : {data['conversion_error']}]")
services = list(data.get("by_service", {}).items())
if args.top:
services = services[:args.top]
print()
if not services:
print("Aucun coût détecté sur la période pour ce tenant.")
return
total = data["total"] or 1 # évite division par zéro
header = f"{'Famille de service':45} {'Montant':>12} {'%':>6}"
print(header)
print("-" * len(header))
for service, amount in services:
pct = (amount / total) * 100
print(f"{service:45} {amount:>12.2f} {pct:>5.1f}%")
print("-" * len(header))
print(f"{'TOTAL':45} {data['total']:>12.2f} {'100.0':>5}% ({data['currency']})")
if __name__ == "__main__":
main()