class1 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blue_book/actuals.py +126 -0
- blue_book/actuals_history.py +503 -0
- blue_book/allocation.py +31 -0
- blue_book/calibration_data.py +544 -0
- blue_book/estimate_store.py +101 -0
- blue_book/focus.py +143 -0
- blue_book/footprint_actuals.py +93 -0
- blue_book/ingest.py +61 -0
- blue_book/ingest_core.py +184 -0
- blue_book/mine.py +171 -0
- blue_book/opendata.py +180 -0
- blue_book/otel_receiver.py +260 -0
- blue_book/search_first.py +28 -0
- blue_book/usage_openai.py +57 -0
- class1-0.1.0.dist-info/METADATA +135 -0
- class1-0.1.0.dist-info/RECORD +72 -0
- class1-0.1.0.dist-info/WHEEL +5 -0
- class1-0.1.0.dist-info/entry_points.txt +2 -0
- class1-0.1.0.dist-info/top_level.txt +4 -0
- cost_engine/__init__.py +97 -0
- cost_engine/aliases.py +53 -0
- cost_engine/autopoiesis.py +109 -0
- cost_engine/basis.py +118 -0
- cost_engine/basis_of_estimate.py +65 -0
- cost_engine/budget.py +101 -0
- cost_engine/calibration.py +136 -0
- cost_engine/calibrator.py +91 -0
- cost_engine/capability.py +130 -0
- cost_engine/capability_data.py +43 -0
- cost_engine/capex.py +90 -0
- cost_engine/classification.py +45 -0
- cost_engine/cloud_cost.py +78 -0
- cost_engine/commitment.py +41 -0
- cost_engine/contingency.py +26 -0
- cost_engine/distributions.py +28 -0
- cost_engine/energy.py +92 -0
- cost_engine/escalation.py +40 -0
- cost_engine/estimate_decay.py +38 -0
- cost_engine/evidence.py +217 -0
- cost_engine/grades_real.py +123 -0
- cost_engine/mcp_overhead.py +80 -0
- cost_engine/monte_carlo.py +107 -0
- cost_engine/prices.py +89 -0
- cost_engine/pricing_loader.py +35 -0
- cost_engine/recommend.py +65 -0
- cost_engine/report.py +69 -0
- cost_engine/scenario.py +106 -0
- cost_engine/self_cost.py +27 -0
- cost_engine/structured_price.py +162 -0
- snapshots/__init__.py +0 -0
- snapshots/actuals_index.json +738 -0
- snapshots/actuarial_table_real.json +2132 -0
- snapshots/autobuild_runs.json +176 -0
- snapshots/capability.json +280 -0
- snapshots/cloud_price_index.json +219694 -0
- snapshots/estimates.json +54 -0
- snapshots/footprint_basis.json +33 -0
- snapshots/grid_intensity.json +31 -0
- snapshots/price_index.json +45810 -0
- snapshots/pricing.json +49088 -0
- snapshots/pricing_structure.json +10193 -0
- snapshots/spec_sheet.json +419463 -0
- snapshots/water_basis.json +17 -0
- takeoff/__init__.py +14 -0
- takeoff/estimate_pr.py +423 -0
- takeoff/license.py +91 -0
- takeoff/pilot.py +68 -0
- takeoff/policy.py +96 -0
- takeoff/post_pr.py +67 -0
- takeoff/scan.py +354 -0
- takeoff/scan_treesitter.py +173 -0
- takeoff/translate.py +92 -0
blue_book/actuals.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""FinOps actuals — FOCUS-IN + close the calibration loop.
|
|
2
|
+
|
|
3
|
+
The FinOps-canonical source of ACTUAL spend is a FOCUS dataset (FinOps Open Cost & Usage
|
|
4
|
+
Specification): a cloud/billing export, a FinOps platform (Vantage / CloudZero / Finout) export,
|
|
5
|
+
or the provider usage/costs API mapped to FOCUS. blue_book already EXPORTS FOCUS (focus.py); this is
|
|
6
|
+
the inverse — read a FOCUS dataset, aggregate the FinOps `EffectiveCost` per workload/month into the
|
|
7
|
+
monthly ACTUAL, and feed it (paired with the prior ESTIMATE) into the ActuarialTable so the estimate
|
|
8
|
+
class rises from 5 (a guess) toward validated. Pure/offline: the REAL FOCUS rows are the user's data.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from cost_engine.calibration import ActuarialTable # NOTE: layer violation — blue_book imports cost_engine. Tracked as tech debt.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_focus_csv(path: str | Path) -> list[dict]:
|
|
19
|
+
"""Read a FOCUS-format CSV (as exported by focus.export_to_csv or any FinOps tool / cloud billing).
|
|
20
|
+
Uses utf-8-SIG: real exports (verified on Microsoft's Azure EA FOCUS sample) carry a UTF-8 BOM that
|
|
21
|
+
would otherwise corrupt the first column name (\\ufeffBilledCost)."""
|
|
22
|
+
with Path(path).open(newline="", encoding="utf-8-sig") as f:
|
|
23
|
+
return list(csv.DictReader(f))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def monthly_actual(focus_rows: list[dict], workflow: str | None = None, month: str | None = None,
|
|
27
|
+
cost_col: str = "EffectiveCost", tag_col: str = "x_workflow_name") -> float:
|
|
28
|
+
"""The ACTUAL monthly spend = sum of FinOps EffectiveCost over FOCUS rows. Optionally scope to a
|
|
29
|
+
workload (the `tag_col` column == `workflow` — x_workflow_name for our LLM exports, or ServiceName/
|
|
30
|
+
ResourceId for cloud FOCUS) and/or a month (YYYY-MM prefix of ChargePeriodStart). EffectiveCost is
|
|
31
|
+
the post-discount FinOps cost — the right number to validate an estimate against."""
|
|
32
|
+
total = 0.0
|
|
33
|
+
for r in focus_rows:
|
|
34
|
+
if workflow is not None and (r.get(tag_col) or "") != workflow:
|
|
35
|
+
continue
|
|
36
|
+
if month is not None and not str(r.get("ChargePeriodStart", "")).startswith(month):
|
|
37
|
+
continue
|
|
38
|
+
try:
|
|
39
|
+
total += float(r.get(cost_col) or 0.0)
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
continue
|
|
42
|
+
return total
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def record_actual(table: ActuarialTable, workflow: str, estimate: dict, focus_rows: list[dict], *,
|
|
46
|
+
month: str | None = None, dominant_driver: str = "output length",
|
|
47
|
+
tag_col: str = "x_workflow_name") -> float:
|
|
48
|
+
"""Close the loop: pair the prior ESTIMATE for `workflow` with its ACTUAL spend (from the FOCUS
|
|
49
|
+
dataset) -> ActuarialTable. n_actuals rises -> estimate_class rises (the flywheel). Returns the
|
|
50
|
+
actual. This is the one step that turns a Class-5 guess into a validated estimate."""
|
|
51
|
+
actual = monthly_actual(focus_rows, workflow=workflow, month=month, tag_col=tag_col)
|
|
52
|
+
table.add(workflow, estimate, actual, dominant_driver)
|
|
53
|
+
return actual
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def variance_waterfall(estimate: dict, actual: float, driver_elasticities: dict[str, float] | None = None) -> dict[str, float]:
|
|
57
|
+
"""Isolate the cost variance (Actual - Expected) into a waterfall of risk drivers.
|
|
58
|
+
|
|
59
|
+
If actual exceeds expected, the variance is distributed proportionally
|
|
60
|
+
across the drivers based on their simulated elasticities. This bridges
|
|
61
|
+
the gap between 'we missed the budget' and 'here is exactly why'.
|
|
62
|
+
"""
|
|
63
|
+
driver_elasticities = driver_elasticities or {}
|
|
64
|
+
expected = float(estimate.get("expected", 0.0))
|
|
65
|
+
total_variance = actual - expected
|
|
66
|
+
|
|
67
|
+
waterfall = {}
|
|
68
|
+
if total_variance == 0 or not driver_elasticities:
|
|
69
|
+
waterfall["unexplained"] = total_variance
|
|
70
|
+
return waterfall
|
|
71
|
+
|
|
72
|
+
total_elasticity = sum(driver_elasticities.values())
|
|
73
|
+
if total_elasticity == 0:
|
|
74
|
+
waterfall["unexplained"] = total_variance
|
|
75
|
+
return waterfall
|
|
76
|
+
|
|
77
|
+
explained = 0.0
|
|
78
|
+
for driver, elasticity in driver_elasticities.items():
|
|
79
|
+
# Using abs(elasticity) in case negative correlation exists but we allocate magnitude
|
|
80
|
+
weight = abs(elasticity) / sum(abs(v) for v in driver_elasticities.values())
|
|
81
|
+
impact = total_variance * weight
|
|
82
|
+
waterfall[driver] = impact
|
|
83
|
+
explained += impact
|
|
84
|
+
|
|
85
|
+
remainder = total_variance - explained
|
|
86
|
+
if abs(remainder) > 0.01:
|
|
87
|
+
waterfall["remainder"] = remainder
|
|
88
|
+
|
|
89
|
+
return waterfall
|
|
90
|
+
|
|
91
|
+
def _main(argv=None) -> int:
|
|
92
|
+
"""CLI: record a real ACTUAL (from a FOCUS export) against a stored ESTIMATE -> persist the loop.
|
|
93
|
+
|
|
94
|
+
python -m blue_book.actuals --focus bill.csv --workflow support_agent \\
|
|
95
|
+
--estimate '{"expected":18,"p50":16,"p90":30}' [--month 2026-06]
|
|
96
|
+
"""
|
|
97
|
+
import argparse
|
|
98
|
+
import json
|
|
99
|
+
|
|
100
|
+
from cost_engine.calibration import ActuarialTable, load_table, save_table # NOTE: layer violation — blue_book imports cost_engine. Tracked as tech debt.
|
|
101
|
+
|
|
102
|
+
ap = argparse.ArgumentParser(description="Close the calibration loop: a FOCUS actual vs a prior estimate.")
|
|
103
|
+
ap.add_argument("--focus", required=True, help="FOCUS CSV (FinOps export / cloud billing / provider-usage->FOCUS)")
|
|
104
|
+
ap.add_argument("--workflow", required=True, help="x_workflow_name to scope the actual to")
|
|
105
|
+
ap.add_argument("--estimate", required=True, help="JSON (file path or inline) with expected/p50/p90")
|
|
106
|
+
ap.add_argument("--month", default=None, help="YYYY-MM to scope the actual (default: all)")
|
|
107
|
+
ap.add_argument("--table", default="snapshots/actuarial_table.json", help="persisted ActuarialTable")
|
|
108
|
+
ap.add_argument("--driver", default="output length")
|
|
109
|
+
ap.add_argument("--scope-col", default="x_workflow_name",
|
|
110
|
+
help="FOCUS column to scope by (x_workflow_name for LLM; ServiceName/ResourceId for cloud)")
|
|
111
|
+
a = ap.parse_args(argv)
|
|
112
|
+
|
|
113
|
+
est = json.loads(Path(a.estimate).read_text()) if Path(a.estimate).exists() else json.loads(a.estimate)
|
|
114
|
+
table = load_table(a.table) if Path(a.table).exists() else ActuarialTable()
|
|
115
|
+
actual = record_actual(table, a.workflow, est, read_focus_csv(a.focus),
|
|
116
|
+
month=a.month, dominant_driver=a.driver, tag_col=a.scope_col)
|
|
117
|
+
save_table(table, a.table)
|
|
118
|
+
cls = table.estimate_class(a.workflow)
|
|
119
|
+
print(f"actual ${actual:,.2f} recorded for '{a.workflow}' (month={a.month or 'all'}) -> "
|
|
120
|
+
f"n_actuals={table.n_actuals(a.workflow)}, class={cls.label}, "
|
|
121
|
+
f"verdict={table._scoped(a.workflow)[-1].verdict}")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
raise SystemExit(_main())
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""HISTORICAL ACTUALS corpus — real FinOps/FOCUS spend, the auditable basis for the COST side.
|
|
2
|
+
|
|
3
|
+
Prices are one half of the basis (the RATE). ACTUALS are the other (what was really BILLED), and
|
|
4
|
+
they are themselves historical data — a dated FOCUS dataset is the spend-equivalent of the Token
|
|
5
|
+
Price Index. This is the medallion ingest for actuals, mirroring `prices/history.py`:
|
|
6
|
+
|
|
7
|
+
catalog (SOURCES, each WIRED / CATALOG_ONLY / REJECTED with license + verdict)
|
|
8
|
+
-> fetch each WIRED dataset to snapshots/bronze/actuals/<name>/ (pinned to a commit sha, sha256-stamped)
|
|
9
|
+
-> aggregate ALL bronze FOCUS rows into snapshots/actuals_index.json
|
|
10
|
+
(real monthly EffectiveCost by provider / ServiceCategory / month)
|
|
11
|
+
|
|
12
|
+
Most public FOCUS data is CLOUD infrastructure — Compute, Storage, Networking, Databases — plus an
|
|
13
|
+
"AI and Machine Learning" ServiceCategory. That is exactly the non-token, fully-loaded FinOps stack
|
|
14
|
+
an LLM application ALSO pays: the agent-loop compute, the vector-DB hosting, the egress. ABC7D's
|
|
15
|
+
token cost comes from the price DB; its CLOUD cost comes from THIS corpus. The index is the grounded
|
|
16
|
+
basis that replaces the hand-set `orchestration_cost_per_call` / `fixed_monthly_usd` priors.
|
|
17
|
+
|
|
18
|
+
Offline discipline: `build_actuals_index` is pure (reads already-fetched bronze; the testable core).
|
|
19
|
+
`fetch_source` is the only network step (resolves a commit sha, downloads raw at that sha) — same
|
|
20
|
+
provenance contract as the price history.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import csv
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import subprocess
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import date
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
BRONZE = Path("snapshots/bronze/actuals")
|
|
34
|
+
INDEX = Path("snapshots/actuals_index.json")
|
|
35
|
+
|
|
36
|
+
# FOCUS canonical columns we aggregate on (the spec names; read tolerantly, see _col).
|
|
37
|
+
_COST_COLS = ("EffectiveCost", "BilledCost", "ContractedCost", "ListCost")
|
|
38
|
+
_AI_CATEGORY = "AI and Machine Learning" # the FOCUS ServiceCategory closest to LLM/AI infra
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ActualsSource:
|
|
43
|
+
"""One public actuals dataset. focus_native = already FOCUS (directly ingestable); provider_native
|
|
44
|
+
= a raw cloud export (AWS CUR / Azure EA) that needs the FOCUS converter first; api = a usage API."""
|
|
45
|
+
name: str
|
|
46
|
+
kind: str # focus_native | provider_native | api
|
|
47
|
+
license: str
|
|
48
|
+
verdict: str # WIRED | CATALOG_ONLY | REJECTED
|
|
49
|
+
repo: str = "" # owner/name (GitHub) for sha resolution + raw download
|
|
50
|
+
branch: str = "main"
|
|
51
|
+
path: str = "" # file path in the repo
|
|
52
|
+
note: str = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- WIRED: FOCUS-native, bulk-ingested into the actuals index ------------------------------
|
|
56
|
+
SOURCES: dict[str, ActualsSource] = {
|
|
57
|
+
"focus_validator_10000": ActualsSource(
|
|
58
|
+
"focus_validator_10000", "focus_native", "MIT", "WIRED",
|
|
59
|
+
repo="finopsfoundation/focus_validator", branch="main",
|
|
60
|
+
path="tests/samples/focus_sample_10000.csv",
|
|
61
|
+
note="10,000-row multi-cloud FOCUS 1.0 sample (AWS/Microsoft/Oracle, 2024-09). Full 44-col "
|
|
62
|
+
"spec incl. commitment-discount + an 'AI and Machine Learning' category."),
|
|
63
|
+
"focus_sample_100000": ActualsSource(
|
|
64
|
+
"focus_sample_100000", "focus_native", "MIT", "WIRED",
|
|
65
|
+
repo="FinOps-Open-Cost-and-Usage-Spec/FOCUS-Sample-Data", branch="main",
|
|
66
|
+
path="FOCUS-1.0/focus_sample_100000.csv.gz",
|
|
67
|
+
note="100,000-row multi-cloud FOCUS 1.0 sample (AWS/Microsoft/Oracle/GCP, 2024-09). "
|
|
68
|
+
"Same spec as the 10K validator set, 10x the rows + GCP representation."),
|
|
69
|
+
# --- WIRED provider-native: converted in-process to FOCUS via _convert_* functions -----------
|
|
70
|
+
"focus_converter_aws_cur": ActualsSource(
|
|
71
|
+
"focus_converter_aws_cur", "provider_native", "MIT", "WIRED",
|
|
72
|
+
repo="finopsfoundation/focus_converters", branch="dev",
|
|
73
|
+
path="focus_converter_base/tests/provider_config_tests/aws/sample-anonymous-aws-export-dataset.csv",
|
|
74
|
+
note="Real anonymized AWS CUR (~1.3K rows). Converted to FOCUS in-process via _convert_aws_cur."),
|
|
75
|
+
"focus_converter_azure_ea": ActualsSource(
|
|
76
|
+
"focus_converter_azure_ea", "provider_native", "MIT", "WIRED",
|
|
77
|
+
repo="finopsfoundation/focus_converters", branch="dev",
|
|
78
|
+
path="focus_converter_base/tests/provider_config_tests/azure/sample-anonymous-ea-export-dataset.csv",
|
|
79
|
+
note="Real anonymized Azure EA export (~27 rows). Converted to FOCUS via _convert_azure_ea."),
|
|
80
|
+
"focus_converter_oci": ActualsSource(
|
|
81
|
+
"focus_converter_oci", "provider_native", "MIT", "WIRED",
|
|
82
|
+
repo="finopsfoundation/focus_converters", branch="dev",
|
|
83
|
+
path="focus_converter_base/tests/provider_config_tests/oci/reports_cost-csv_0000000030000269.csv",
|
|
84
|
+
note="Real Oracle Cloud cost report (~506 rows). Converted to FOCUS via _convert_oci."),
|
|
85
|
+
# --- WIRED Kaggle/HuggingFace: real billing data, provider-native ---
|
|
86
|
+
"kaggle_azure_subscription": ActualsSource(
|
|
87
|
+
"kaggle_azure_subscription", "provider_native", "CC0 1.0", "WIRED",
|
|
88
|
+
note="93K-row anonymized Azure subscription costs (2022-12, MeterCategory-level). "
|
|
89
|
+
"Kaggle carrucciu/azure-costs. Converted via _convert_azure_subscription."),
|
|
90
|
+
"kaggle_gcp_billing": ActualsSource(
|
|
91
|
+
"kaggle_gcp_billing", "provider_native", "Apache 2.0", "WIRED",
|
|
92
|
+
note="124K-row GCP billing data (2022-2023, 25 services). HuggingFace sairamn/gcp-cloud-billing-cost. "
|
|
93
|
+
"Converted via _convert_gcp_billing."),
|
|
94
|
+
"kaggle_azure_org_expenses": ActualsSource(
|
|
95
|
+
"kaggle_azure_org_expenses", "provider_native", "CC0 1.0", "WIRED",
|
|
96
|
+
note="89-row Azure org expenses (2023-2024, monthly by service). "
|
|
97
|
+
"Kaggle rishi2123/oragnizations-expenses-2023-2024. Converted via _convert_azure_org_expenses."),
|
|
98
|
+
# --- CATALOG_ONLY: needs API key or manual setup ---
|
|
99
|
+
"openai_usage_api": ActualsSource(
|
|
100
|
+
"openai_usage_api", "api", "provider data (your account)", "CATALOG_ONLY",
|
|
101
|
+
note="The real LLM-actuals source. blue_book/usage_openai.fetch_openai_usage needs OPENAI_ADMIN_KEY; "
|
|
102
|
+
"maps Usage API buckets -> priced CanonicalEvents -> FOCUS via focus.py."),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _col(row: dict, name: str) -> str | None:
|
|
107
|
+
"""FOCUS column lookup tolerant of case (exports vary)."""
|
|
108
|
+
if name in row:
|
|
109
|
+
return row[name]
|
|
110
|
+
low = name.lower()
|
|
111
|
+
for k in row:
|
|
112
|
+
if k.lower() == low:
|
|
113
|
+
return row[k]
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _num(v) -> float:
|
|
118
|
+
try:
|
|
119
|
+
return float(v)
|
|
120
|
+
except (TypeError, ValueError):
|
|
121
|
+
return 0.0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class Provenance:
|
|
126
|
+
name: str
|
|
127
|
+
url: str
|
|
128
|
+
sha256: str
|
|
129
|
+
rows: int
|
|
130
|
+
license: str
|
|
131
|
+
fetched: str = field(default_factory=lambda: date.today().isoformat())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _latest_sha(repo: str, branch: str, path: str) -> str:
|
|
135
|
+
"""Resolve the file's latest commit sha (pinned, reproducible download). '' if unavailable."""
|
|
136
|
+
out = subprocess.run(
|
|
137
|
+
["gh", "api", f"repos/{repo}/commits", "-X", "GET",
|
|
138
|
+
"-f", f"sha={branch}", "-f", f"path={path}", "-f", "per_page=1"],
|
|
139
|
+
capture_output=True, text=True, check=False).stdout
|
|
140
|
+
try:
|
|
141
|
+
return json.loads(out)[0]["sha"]
|
|
142
|
+
except (json.JSONDecodeError, IndexError, KeyError):
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def fetch_source(src: ActualsSource, root: Path = BRONZE) -> Provenance | None:
|
|
147
|
+
"""Download a focus_native source to bronze, pinned to its latest commit sha, sha256-stamped.
|
|
148
|
+
Returns provenance (None for non-focus_native — those are CATALOG_ONLY recipes, not bulk-fetched)."""
|
|
149
|
+
if src.kind != "focus_native" or src.verdict != "WIRED":
|
|
150
|
+
return None
|
|
151
|
+
sha = _latest_sha(src.repo, src.branch, src.path) or src.branch
|
|
152
|
+
url = f"https://raw.githubusercontent.com/{src.repo}/{sha}/{src.path}"
|
|
153
|
+
dest = root / src.name / Path(src.path).name
|
|
154
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
data = subprocess.run(["curl", "-sS", "-L", url], capture_output=True, check=False).stdout
|
|
156
|
+
dest.write_bytes(data)
|
|
157
|
+
rows = max(0, data.count(b"\n") - 1)
|
|
158
|
+
prov = Provenance(src.name, url, hashlib.sha256(data).hexdigest(), rows, src.license)
|
|
159
|
+
(root / src.name / "provenance.json").write_text(json.dumps(prov.__dict__, indent=2))
|
|
160
|
+
return prov
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Provider-native → FOCUS converters (lightweight, in-process)
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Each converter reads provider-native CSV rows and returns FOCUS-shaped dicts
|
|
167
|
+
# with the columns aggregate_focus_rows needs: ServiceCategory, EffectiveCost,
|
|
168
|
+
# BilledCost, ProviderName, ChargePeriodStart. Minimal — just enough for the
|
|
169
|
+
# cost-basis rollup, not a full FOCUS export.
|
|
170
|
+
|
|
171
|
+
_AWS_PRODUCT_CATEGORY: dict[str, str] = {
|
|
172
|
+
"AmazonEC2": "Compute", "AWSLambda": "Compute", "AmazonECS": "Compute",
|
|
173
|
+
"AmazonEKS": "Compute", "ElasticMapReduce": "Compute", "AmazonLightsail": "Compute",
|
|
174
|
+
"AmazonS3": "Storage", "AmazonEFS": "Storage", "AmazonGlacier": "Storage",
|
|
175
|
+
"AmazonRDS": "Databases", "AmazonDynamoDB": "Databases", "AmazonRedshift": "Databases",
|
|
176
|
+
"AmazonElastiCache": "Databases", "AmazonNeptune": "Databases",
|
|
177
|
+
"AWSDataTransfer": "Networking", "AmazonVPC": "Networking", "AmazonCloudFront": "Networking",
|
|
178
|
+
"AmazonRoute53": "Networking", "ElasticLoadBalancing": "Networking",
|
|
179
|
+
"AmazonSageMaker": "AI and Machine Learning", "AmazonBedrock": "AI and Machine Learning",
|
|
180
|
+
"AmazonComprehend": "AI and Machine Learning", "AmazonRekognition": "AI and Machine Learning",
|
|
181
|
+
"AWSCloudTrail": "Management and Governance", "AmazonCloudWatch": "Management and Governance",
|
|
182
|
+
"AWSConfig": "Management and Governance", "AWSSystemsManager": "Management and Governance",
|
|
183
|
+
"awskms": "Security", "AWSSecretsManager": "Security", "AmazonGuardDuty": "Security",
|
|
184
|
+
"AmazonSNS": "Integration", "AWSQueueService": "Integration", "AmazonStates": "Integration",
|
|
185
|
+
"AWSGlue": "Analytics", "AmazonAthena": "Analytics", "AmazonKinesis": "Analytics",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _convert_aws_cur(rows: list[dict]) -> list[dict]:
|
|
190
|
+
"""Convert AWS CUR rows to FOCUS-shaped dicts."""
|
|
191
|
+
focus: list[dict] = []
|
|
192
|
+
for r in rows:
|
|
193
|
+
pc = r.get("lineItem/ProductCode", "")
|
|
194
|
+
cat = _AWS_PRODUCT_CATEGORY.get(pc, "Other")
|
|
195
|
+
cost = _num(r.get("lineItem/UnblendedCost", 0))
|
|
196
|
+
billed = _num(r.get("lineItem/BlendedCost", 0))
|
|
197
|
+
# Use reservation EffectiveCost if present, else unblended
|
|
198
|
+
eff = _num(r.get("reservation/EffectiveCost", "")) or cost
|
|
199
|
+
# SavingsPlan effective cost overrides if present
|
|
200
|
+
sp_eff = _num(r.get("savingsPlan/SavingsPlanEffectiveCost", ""))
|
|
201
|
+
if sp_eff:
|
|
202
|
+
eff = sp_eff
|
|
203
|
+
start = r.get("lineItem/UsageStartDate", "")
|
|
204
|
+
focus.append({
|
|
205
|
+
"ServiceCategory": cat,
|
|
206
|
+
"EffectiveCost": str(eff),
|
|
207
|
+
"BilledCost": str(billed),
|
|
208
|
+
"ContractedCost": "0",
|
|
209
|
+
"ListCost": str(_num(r.get("pricing/publicOnDemandCost", 0))),
|
|
210
|
+
"ProviderName": "AWS",
|
|
211
|
+
"ChargePeriodStart": start,
|
|
212
|
+
})
|
|
213
|
+
return focus
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_AZURE_FAMILY_CATEGORY: dict[str, str] = {
|
|
217
|
+
"Compute": "Compute", "Networking": "Networking", "Storage": "Storage",
|
|
218
|
+
"Databases": "Databases", "Analytics": "Analytics",
|
|
219
|
+
"AI + Machine Learning": "AI and Machine Learning",
|
|
220
|
+
"Internet of Things": "Integration", "Integration": "Integration",
|
|
221
|
+
"Security": "Security", "Identity": "Identity",
|
|
222
|
+
"Management and Governance": "Management and Governance",
|
|
223
|
+
"Developer Tools": "Developer Tools", "Web": "Web",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _convert_azure_ea(rows: list[dict]) -> list[dict]:
|
|
228
|
+
"""Convert Azure EA (amortized) rows to FOCUS-shaped dicts."""
|
|
229
|
+
focus: list[dict] = []
|
|
230
|
+
for r in rows:
|
|
231
|
+
sf = r.get("ServiceFamily", "")
|
|
232
|
+
cat = _AZURE_FAMILY_CATEGORY.get(sf, "Other")
|
|
233
|
+
cost = _num(r.get("CostInBillingCurrency", 0))
|
|
234
|
+
unit_price = _num(r.get("UnitPrice", 0))
|
|
235
|
+
qty = _num(r.get("Quantity", 0))
|
|
236
|
+
list_cost = _num(r.get("PayGPrice", 0)) * qty
|
|
237
|
+
start = r.get("Date", "")
|
|
238
|
+
focus.append({
|
|
239
|
+
"ServiceCategory": cat,
|
|
240
|
+
"EffectiveCost": str(cost),
|
|
241
|
+
"BilledCost": str(cost),
|
|
242
|
+
"ContractedCost": str(unit_price * qty),
|
|
243
|
+
"ListCost": str(list_cost),
|
|
244
|
+
"ProviderName": "Microsoft",
|
|
245
|
+
"ChargePeriodStart": start,
|
|
246
|
+
})
|
|
247
|
+
return focus
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
_OCI_SERVICE_CATEGORY: dict[str, str] = {
|
|
251
|
+
"COMPUTE": "Compute", "CONTAINER": "Compute", "FUNCTIONS": "Compute",
|
|
252
|
+
"BLOCK_STORAGE": "Storage", "OBJECTSTORE": "Storage", "FILE_STORAGE": "Storage",
|
|
253
|
+
"DATABASE": "Databases", "MYSQL": "Databases", "NOSQL": "Databases",
|
|
254
|
+
"POSTGRESQL": "Databases", "AUTONOMOUS_DATABASE": "Databases",
|
|
255
|
+
"NETWORK": "Networking", "VCN_FLOW_LOGS": "Networking", "LOAD_BALANCER": "Networking",
|
|
256
|
+
"ORACLE_STREAMING_SERVICE": "Integration", "ORACLE_INTEGRATION": "Integration",
|
|
257
|
+
"TELEMETRY": "Management and Governance", "MONITORING": "Management and Governance",
|
|
258
|
+
"DATA_SCIENCE": "AI and Machine Learning", "AI_SERVICES": "AI and Machine Learning",
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _convert_oci(rows: list[dict]) -> list[dict]:
|
|
263
|
+
"""Convert OCI cost-csv rows to FOCUS-shaped dicts."""
|
|
264
|
+
focus: list[dict] = []
|
|
265
|
+
for r in rows:
|
|
266
|
+
svc = r.get("product/service", "")
|
|
267
|
+
cat = _OCI_SERVICE_CATEGORY.get(svc, "Other")
|
|
268
|
+
cost = _num(r.get("cost/myCost", 0))
|
|
269
|
+
overage = _num(r.get("cost/myCostOverage", 0))
|
|
270
|
+
start = r.get("lineItem/intervalUsageStart", "")
|
|
271
|
+
focus.append({
|
|
272
|
+
"ServiceCategory": cat,
|
|
273
|
+
"EffectiveCost": str(cost + overage),
|
|
274
|
+
"BilledCost": str(cost + overage),
|
|
275
|
+
"ContractedCost": "0",
|
|
276
|
+
"ListCost": "0",
|
|
277
|
+
"ProviderName": "Oracle",
|
|
278
|
+
"ChargePeriodStart": start,
|
|
279
|
+
})
|
|
280
|
+
return focus
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# --- Kaggle / HuggingFace provider-native converters ----------------------------------------
|
|
284
|
+
|
|
285
|
+
_AZURE_METER_CATEGORY: dict[str, str] = {
|
|
286
|
+
"Virtual Machines": "Compute", "Container Instances": "Compute",
|
|
287
|
+
"Azure App Service": "Compute", "Functions": "Compute",
|
|
288
|
+
"Storage": "Storage", "Backup": "Storage",
|
|
289
|
+
"Azure Database for MySQL": "Databases", "SQL Database": "Databases",
|
|
290
|
+
"Azure Cosmos DB": "Databases", "Azure Database for PostgreSQL": "Databases",
|
|
291
|
+
"Virtual Network": "Networking", "Bandwidth": "Networking",
|
|
292
|
+
"Load Balancer": "Networking", "Azure Front Door Service": "Networking",
|
|
293
|
+
"Azure DNS": "Networking", "ExpressRoute": "Networking", "VPN Gateway": "Networking",
|
|
294
|
+
"Advanced Threat Protection": "Security", "Security Center": "Security",
|
|
295
|
+
"Advanced Data Security": "Security", "Azure Active Directory": "Identity",
|
|
296
|
+
"Log Analytics": "Management and Governance", "Azure Monitor": "Management and Governance",
|
|
297
|
+
"Logic Apps": "Integration", "Service Bus": "Integration", "Event Hubs": "Integration",
|
|
298
|
+
"Container Registry": "Compute", "Azure Kubernetes Service": "Compute",
|
|
299
|
+
"Azure Machine Learning": "AI and Machine Learning",
|
|
300
|
+
"Cognitive Services": "AI and Machine Learning",
|
|
301
|
+
"Azure Data Factory v2": "Analytics", "Azure Synapse Analytics": "Analytics",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _convert_azure_subscription(rows: list[dict]) -> list[dict]:
|
|
306
|
+
"""Convert Kaggle Azure subscription costs (MeterCategory-level) to FOCUS-shaped dicts."""
|
|
307
|
+
focus: list[dict] = []
|
|
308
|
+
for r in rows:
|
|
309
|
+
mc = r.get("MeterCategory", "")
|
|
310
|
+
cat = _AZURE_METER_CATEGORY.get(mc, "Other")
|
|
311
|
+
cost = _num(r.get("CostInBillingCurrency", 0))
|
|
312
|
+
start = r.get("Date", "")
|
|
313
|
+
focus.append({
|
|
314
|
+
"ServiceCategory": cat,
|
|
315
|
+
"EffectiveCost": str(cost),
|
|
316
|
+
"BilledCost": str(cost),
|
|
317
|
+
"ContractedCost": "0",
|
|
318
|
+
"ListCost": "0",
|
|
319
|
+
"ProviderName": "Microsoft",
|
|
320
|
+
"ChargePeriodStart": start,
|
|
321
|
+
})
|
|
322
|
+
return focus
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
_GCP_SERVICE_CATEGORY: dict[str, str] = {
|
|
326
|
+
"Compute Engine": "Compute", "Cloud Run": "Compute", "Cloud Functions": "Compute",
|
|
327
|
+
"App Engine": "Compute", "Google Kubernetes Engine": "Compute",
|
|
328
|
+
"Cloud Storage": "Storage",
|
|
329
|
+
"Cloud SQL": "Databases", "Cloud Spanner": "Databases", "Cloud Bigtable": "Databases",
|
|
330
|
+
"Cloud Memorystore": "Databases", "Firestore": "Databases",
|
|
331
|
+
"Cloud CDN": "Networking", "Cloud NAT": "Networking", "Cloud DNS": "Networking",
|
|
332
|
+
"Cloud Interconnect": "Networking", "Cloud VPN": "Networking",
|
|
333
|
+
"Cloud Armor": "Security", "Secret Manager": "Security",
|
|
334
|
+
"Cloud Pub/Sub": "Integration", "Cloud Tasks": "Integration",
|
|
335
|
+
"Cloud Scheduler": "Integration", "Cloud Composer": "Integration",
|
|
336
|
+
"BigQuery": "Analytics", "Cloud Dataproc": "Analytics", "Cloud Dataflow": "Analytics",
|
|
337
|
+
"Vertex AI": "AI and Machine Learning", "Dialogflow": "AI and Machine Learning",
|
|
338
|
+
"Cloud Vision API": "AI and Machine Learning", "Cloud Natural Language": "AI and Machine Learning",
|
|
339
|
+
"Cloud Build": "Developer Tools", "Artifact Registry": "Developer Tools",
|
|
340
|
+
"Cloud Logging": "Management and Governance", "Cloud Monitoring": "Management and Governance",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _convert_gcp_billing(rows: list[dict]) -> list[dict]:
|
|
345
|
+
"""Convert HuggingFace/Kaggle GCP billing data to FOCUS-shaped dicts."""
|
|
346
|
+
focus: list[dict] = []
|
|
347
|
+
for r in rows:
|
|
348
|
+
svc = r.get("Service Name", "")
|
|
349
|
+
cat = _GCP_SERVICE_CATEGORY.get(svc, "Other")
|
|
350
|
+
cost = _num(r.get("Unrounded Cost ($)", 0))
|
|
351
|
+
start = r.get("Usage Start Date", "")
|
|
352
|
+
focus.append({
|
|
353
|
+
"ServiceCategory": cat,
|
|
354
|
+
"EffectiveCost": str(cost),
|
|
355
|
+
"BilledCost": str(_num(r.get("Rounded Cost ($)", 0))),
|
|
356
|
+
"ContractedCost": "0",
|
|
357
|
+
"ListCost": "0",
|
|
358
|
+
"ProviderName": "Google Cloud",
|
|
359
|
+
"ChargePeriodStart": start,
|
|
360
|
+
})
|
|
361
|
+
return focus
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
_AZURE_SERVICE_CATEGORY_SIMPLE: dict[str, str] = {
|
|
365
|
+
"Virtual Machines": "Compute", "Azure App Service": "Compute",
|
|
366
|
+
"Storage": "Storage", "Automation": "Management and Governance",
|
|
367
|
+
"Azure DNS": "Networking", "Bandwidth": "Networking",
|
|
368
|
+
"Azure Active Directory Domain Services": "Identity",
|
|
369
|
+
"Azure Monitor": "Management and Governance",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _convert_azure_org_expenses(rows: list[dict]) -> list[dict]:
|
|
374
|
+
"""Convert Kaggle Azure org expenses (simple ServiceName/CostUSD) to FOCUS-shaped dicts."""
|
|
375
|
+
focus: list[dict] = []
|
|
376
|
+
for r in rows:
|
|
377
|
+
svc = r.get("ServiceName", "")
|
|
378
|
+
cat = _AZURE_SERVICE_CATEGORY_SIMPLE.get(svc, "Other")
|
|
379
|
+
cost = _num(r.get("CostUSD", 0))
|
|
380
|
+
start = r.get("UsageDate", "")
|
|
381
|
+
focus.append({
|
|
382
|
+
"ServiceCategory": cat,
|
|
383
|
+
"EffectiveCost": str(cost),
|
|
384
|
+
"BilledCost": str(cost),
|
|
385
|
+
"ContractedCost": "0",
|
|
386
|
+
"ListCost": "0",
|
|
387
|
+
"ProviderName": "Microsoft",
|
|
388
|
+
"ChargePeriodStart": start,
|
|
389
|
+
})
|
|
390
|
+
return focus
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
_PROVIDER_CONVERTERS: dict[str, callable] = {
|
|
394
|
+
"focus_converter_aws_cur": _convert_aws_cur,
|
|
395
|
+
"focus_converter_azure_ea": _convert_azure_ea,
|
|
396
|
+
"focus_converter_oci": _convert_oci,
|
|
397
|
+
"kaggle_azure_subscription": _convert_azure_subscription,
|
|
398
|
+
"kaggle_gcp_billing": _convert_gcp_billing,
|
|
399
|
+
"kaggle_azure_org_expenses": _convert_azure_org_expenses,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def aggregate_focus_rows(rows: list[dict]) -> dict:
|
|
404
|
+
"""Aggregate FOCUS rows into the cost basis: totals + per-(category, provider, month) breakdown.
|
|
405
|
+
EffectiveCost is the post-discount FinOps number; we also keep billed/contracted/list for spread."""
|
|
406
|
+
by_category: dict[str, dict] = defaultdict(lambda: defaultdict(float))
|
|
407
|
+
by_provider: dict[str, dict] = defaultdict(lambda: defaultdict(float))
|
|
408
|
+
by_month: dict[str, float] = defaultdict(float)
|
|
409
|
+
totals: dict[str, float] = defaultdict(float)
|
|
410
|
+
for r in rows:
|
|
411
|
+
cat = _col(r, "ServiceCategory") or "(uncategorized)"
|
|
412
|
+
prov = _col(r, "ProviderName") or "(unknown)"
|
|
413
|
+
month = str(_col(r, "ChargePeriodStart") or "")[:7]
|
|
414
|
+
eff = _num(_col(r, "EffectiveCost"))
|
|
415
|
+
for cc in _COST_COLS:
|
|
416
|
+
totals[cc] += _num(_col(r, cc))
|
|
417
|
+
by_category[cat]["effective"] += eff
|
|
418
|
+
by_category[cat]["rows"] += 1
|
|
419
|
+
by_provider[prov]["effective"] += eff
|
|
420
|
+
if month:
|
|
421
|
+
by_month[month] += eff
|
|
422
|
+
ai = by_category.get(_AI_CATEGORY, {}).get("effective", 0.0)
|
|
423
|
+
return {
|
|
424
|
+
"rows": len(rows),
|
|
425
|
+
"totals": {k: round(v, 6) for k, v in totals.items()},
|
|
426
|
+
"by_category": {k: {kk: round(vv, 6) for kk, vv in v.items()} for k, v in by_category.items()},
|
|
427
|
+
"by_provider": {k: round(v["effective"], 6) for k, v in by_provider.items()},
|
|
428
|
+
"months": sorted(by_month),
|
|
429
|
+
"ai_ml_effective": round(ai, 6),
|
|
430
|
+
"cloud_effective": round(totals["EffectiveCost"] - ai, 6), # non-AI/ML = pure cloud infra
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def build_actuals_index(root: Path = BRONZE, out: Path = INDEX) -> dict:
|
|
435
|
+
"""Pure/offline: read every fetched FOCUS dataset in bronze, aggregate, write the index. The index
|
|
436
|
+
is the real CLOUD-COST basis (by ServiceCategory) + the AI/ML actual, with provenance per source.
|
|
437
|
+
Handles both focus_native CSVs (direct read) and provider_native CSVs (converted in-process)."""
|
|
438
|
+
sources: dict[str, dict] = {}
|
|
439
|
+
roll_cat: dict[str, float] = defaultdict(float)
|
|
440
|
+
roll_tot = roll_ai = 0.0
|
|
441
|
+
for src in SOURCES.values():
|
|
442
|
+
if src.verdict != "WIRED":
|
|
443
|
+
continue
|
|
444
|
+
d = root / src.name
|
|
445
|
+
csvs = sorted(d.glob("*.csv")) if d.exists() else []
|
|
446
|
+
if not csvs:
|
|
447
|
+
continue
|
|
448
|
+
raw_rows: list[dict] = []
|
|
449
|
+
for c in csvs:
|
|
450
|
+
with c.open(newline="", encoding="utf-8-sig") as f:
|
|
451
|
+
raw_rows += list(csv.DictReader(f))
|
|
452
|
+
# Convert provider-native to FOCUS if a converter exists
|
|
453
|
+
converter = _PROVIDER_CONVERTERS.get(src.name)
|
|
454
|
+
rows = converter(raw_rows) if converter else raw_rows
|
|
455
|
+
agg = aggregate_focus_rows(rows)
|
|
456
|
+
prov_file = d / "provenance.json"
|
|
457
|
+
prov = json.loads(prov_file.read_text()) if prov_file.exists() else {}
|
|
458
|
+
sources[src.name] = {"license": src.license, "kind": src.kind,
|
|
459
|
+
"provenance": prov, **agg}
|
|
460
|
+
for cat, v in agg["by_category"].items():
|
|
461
|
+
roll_cat[cat] += v.get("effective", 0.0)
|
|
462
|
+
roll_tot += agg["totals"].get("EffectiveCost", 0.0)
|
|
463
|
+
roll_ai += agg["ai_ml_effective"]
|
|
464
|
+
index = {
|
|
465
|
+
"generated": date.today().isoformat(),
|
|
466
|
+
"catalog": {n: {"verdict": s.verdict, "kind": s.kind, "license": s.license, "note": s.note}
|
|
467
|
+
for n, s in SOURCES.items()},
|
|
468
|
+
"sources": sources,
|
|
469
|
+
"rollup": {
|
|
470
|
+
"by_category": {k: round(v, 6) for k, v in sorted(roll_cat.items(), key=lambda kv: -kv[1])},
|
|
471
|
+
"total_effective": round(roll_tot, 6),
|
|
472
|
+
"ai_ml_effective": round(roll_ai, 6),
|
|
473
|
+
"cloud_effective": round(roll_tot - roll_ai, 6),
|
|
474
|
+
},
|
|
475
|
+
}
|
|
476
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
out.write_text(json.dumps(index, indent=2))
|
|
478
|
+
return index
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _main(argv=None) -> int:
|
|
482
|
+
"""Fetch all WIRED focus_native sources, then (re)build the actuals index. Prints the cloud basis."""
|
|
483
|
+
import argparse
|
|
484
|
+
ap = argparse.ArgumentParser(description="Build the historical ACTUALS corpus (real cloud + AI/ML spend).")
|
|
485
|
+
ap.add_argument("--no-fetch", action="store_true", help="skip download; rebuild index from existing bronze")
|
|
486
|
+
a = ap.parse_args(argv)
|
|
487
|
+
if not a.no_fetch:
|
|
488
|
+
for src in SOURCES.values():
|
|
489
|
+
prov = fetch_source(src)
|
|
490
|
+
if prov:
|
|
491
|
+
print(f"fetched {src.name}: {prov.rows} rows, sha256={prov.sha256[:12]}… ({src.license})")
|
|
492
|
+
idx = build_actuals_index()
|
|
493
|
+
r = idx["rollup"]
|
|
494
|
+
print(f"\nactuals_index.json — {len(idx['sources'])} source(s), ${r['total_effective']:,.2f} effective")
|
|
495
|
+
print(f" cloud infra: ${r['cloud_effective']:,.2f} AI/ML: ${r['ai_ml_effective']:,.2f}")
|
|
496
|
+
print(" top categories:")
|
|
497
|
+
for cat, v in list(r["by_category"].items())[:6]:
|
|
498
|
+
print(f" {cat:<28} ${v:,.2f}")
|
|
499
|
+
return 0
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
raise SystemExit(_main())
|
blue_book/allocation.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""FinOps cost allocation / showback — attribute effective cost to an owner dimension.
|
|
2
|
+
|
|
3
|
+
"Unallocated cost is unmanaged cost." Pure: groups priced events (CanonicalEvent objects OR the
|
|
4
|
+
ledger's row dicts) by a tag — repo_slug / organization_slug / workflow_name / environment — so cost
|
|
5
|
+
is shown back to the team/feature/tenant that incurred it. The ingest already captures these tags;
|
|
6
|
+
this turns them into a showback table.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get(event, key, default=None):
|
|
14
|
+
return event.get(key, default) if isinstance(event, dict) else getattr(event, key, default)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def allocate(events, dimension: str = "repo_slug") -> dict[str, float]:
|
|
18
|
+
"""Sum `effective_cost_usd` of priced events by `dimension`, descending. Untagged events fall
|
|
19
|
+
into '(unallocated)' — the FinOps red flag (cost you cannot attribute to an owner)."""
|
|
20
|
+
totals: dict[str, float] = defaultdict(float)
|
|
21
|
+
for e in events:
|
|
22
|
+
key = _get(e, dimension) or "(unallocated)"
|
|
23
|
+
totals[key] += float(_get(e, "effective_cost_usd", 0.0) or 0.0)
|
|
24
|
+
return dict(sorted(totals.items(), key=lambda kv: -kv[1]))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def unallocated_fraction(events, dimension: str = "repo_slug") -> float:
|
|
28
|
+
"""Fraction of total effective cost that has NO owner tag — the showback coverage gap."""
|
|
29
|
+
alloc = allocate(events, dimension)
|
|
30
|
+
total = sum(alloc.values())
|
|
31
|
+
return (alloc.get("(unallocated)", 0.0) / total) if total else 0.0
|