""" Excel / CSV / JSON export. Excel uses openpyxl via pandas — multi-sheet workbooks readable in Excel 2019+. JSON round-trips the full input state (sites, takeouts, feature scopes) so a scenario can be saved and reloaded. """ from __future__ import annotations import dataclasses import json from pathlib import Path import pandas as pd from .inputs import CostTakeout, FeatureScope, SiteInput from .meters import Confidence, TokenMeter from .rollout import RolloutPlan def meters_dataframe(meters: dict[str, TokenMeter]) -> pd.DataFrame: """Meter catalogue as a display/export-ready DataFrame.""" return pd.DataFrame( [ { "feature": m.feature, "meter_type": m.meter_type.value, "units_per_token": m.units_per_token or None, "tokens_per_unit": m.tokens_per_unit, "confidence": f"{m.confidence.icon} {m.confidence.value}", "notes": m.notes, "source": m.source_url or "", } for m in meters.values() ] ) def sites_dataframe(sites: list[SiteInput]) -> pd.DataFrame: rows = [] for s in sites: d = dataclasses.asdict(s) d["languages"] = ", ".join(d["languages"]) rows.append(d) return pd.DataFrame(rows) def export_excel( sheets: dict[str, pd.DataFrame], path: str | Path, ) -> Path: """Write a multi-sheet Excel workbook. Sheet names are truncated to Excel's 31-character limit.""" path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) with pd.ExcelWriter(path, engine="openpyxl") as writer: for name, df in sheets.items(): df.to_excel(writer, sheet_name=name[:31], index=False) return path def export_csv(df: pd.DataFrame, path: str | Path) -> Path: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) df.to_csv(path, index=False) return path # ── JSON scenario save / load ──────────────────────────────────────── def scenario_state_to_json( sites: list[SiteInput], takeouts: list[CostTakeout], feature_scopes: list[FeatureScope], path: str | Path | None = None, rollout: RolloutPlan | None = None, ) -> str: """Serialize the full input state; optionally write to ``path``.""" state = { "sites": [dataclasses.asdict(s) for s in sites], "takeouts": [ {**dataclasses.asdict(t), "confidence": t.confidence.value} for t in takeouts ], "feature_scopes": [ { **dataclasses.asdict(f), "adoption_curve": {str(k): v for k, v in f.adoption_curve.items()}, } for f in feature_scopes ], } if rollout is not None: state["rollout"] = dataclasses.asdict(rollout) text = json.dumps(state, indent=2) if path is not None: Path(path).write_text(text) return text def scenario_state_from_json( source: str | Path, ) -> tuple[list[SiteInput], list[CostTakeout], list[FeatureScope], RolloutPlan | None]: """Inverse of :func:`scenario_state_to_json`. ``source`` is a JSON string or a file path. The fourth element is None for legacy files saved without a rollout plan.""" raw = ( Path(source).read_text() if isinstance(source, Path) or (isinstance(source, str) and source.strip().endswith(".json")) else str(source) ) state = json.loads(raw) sites = [SiteInput(**s) for s in state["sites"]] takeouts = [ CostTakeout(**{**t, "confidence": Confidence(t["confidence"])}) for t in state["takeouts"] ] scopes = [ FeatureScope( **{ **f, "adoption_curve": {int(k): v for k, v in f["adoption_curve"].items()}, } ) for f in state["feature_scopes"] ] rollout = ( RolloutPlan(**state["rollout"]) if "rollout" in state else None ) return sites, takeouts, scopes, rollout