""" Lightweight dataclasses for TEI API responses. These are *optional* — the client returns raw dicts. Use these when you want attribute access or IDE help in notebooks. >>> from core.tei_client import TEIClient, TEIReport >>> raw = TEIClient().get_report("abc123") >>> report = TEIReport.from_dict(raw) >>> report.discount_rate 0.10 """ from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal from typing import Any def _as_float(value: Any, default: float = 0.0) -> float: """Coerce Decimal/str/None into float.""" if value is None or value == "": return default if isinstance(value, Decimal): return float(value) try: return float(value) except (TypeError, ValueError): return default @dataclass class TEIReport: """A TEI report template (model definition).""" id: str # public_id name: str vendor: str version: str description: str = "" analysis_period_years: int = 3 discount_rate: float = 0.10 status: str = "active" field_count: int = 0 instance_count: int = 0 created_at: str = "" updated_at: str = "" @classmethod def from_dict(cls, data: dict) -> TEIReport: return cls( id=str(data.get("id", "")), name=data.get("name", ""), vendor=data.get("vendor", ""), version=data.get("version", ""), description=data.get("description", "") or "", analysis_period_years=int(data.get("analysis_period_years") or 3), discount_rate=_as_float(data.get("discount_rate"), 0.10), status=data.get("status", "active"), field_count=int(data.get("field_count") or 0), instance_count=int(data.get("instance_count") or 0), created_at=data.get("created_at", "") or "", updated_at=data.get("updated_at", "") or "", ) @dataclass class TEIField: """A field definition belonging to a TEI report template.""" id: int table: str # 'benefits' | 'costs' field_key: str label: str field_type: str # currency | percentage | integer | decimal | text description: str = "" category: str = "" default_value: str = "" is_annual: bool = True risk_adjustment: float | None = None sort_order: int = 0 is_required: bool = False source_notes: str = "" @classmethod def from_dict(cls, data: dict) -> TEIField: ra = data.get("risk_adjustment") return cls( id=int(data.get("id") or 0), table=data.get("table", "benefits"), field_key=data.get("field_key", ""), label=data.get("label", ""), field_type=data.get("field_type", "decimal"), description=data.get("description", "") or "", category=data.get("category", "") or "", default_value=data.get("default_value", "") or "", is_annual=bool(data.get("is_annual", True)), risk_adjustment=_as_float(ra) if ra is not None else None, sort_order=int(data.get("sort_order") or 0), is_required=bool(data.get("is_required", False)), source_notes=data.get("source_notes", "") or "", ) @dataclass class TEIValue: """ A field value for a specific TEI tool instance. The exact wire format is not fully pinned in the OpenAPI spec; we use a convention that the client `_normalize_value` helper builds: - annual fields: {field_key, year_values: {"1": ..., "2": ...}, risk_adjustment, notes} - non-annual scalar: {field_key, value, risk_adjustment, notes} """ field_key: str year_values: dict[str, float] = field(default_factory=dict) value: float | None = None risk_adjustment: float | None = None notes: str = "" @classmethod def from_dict(cls, data: dict) -> TEIValue: ra = data.get("risk_adjustment") yv_raw = data.get("year_values") or {} year_values = {str(k): _as_float(v) for k, v in yv_raw.items()} v = data.get("value") return cls( field_key=data.get("field_key", ""), year_values=year_values, value=_as_float(v) if v is not None else None, risk_adjustment=_as_float(ra) if ra is not None else None, notes=data.get("notes", "") or "", ) def to_dict(self) -> dict: out: dict[str, Any] = {"field_key": self.field_key} if self.year_values: out["year_values"] = self.year_values if self.value is not None: out["value"] = self.value if self.risk_adjustment is not None: out["risk_adjustment"] = self.risk_adjustment if self.notes: out["notes"] = self.notes return out @dataclass class TEISummary: """Calculated financial summary for a TEI tool instance.""" npv: float = 0.0 roi: float = 0.0 payback_months: float | None = None discount_rate: float = 0.10 analysis_years: int = 3 total_benefits_nominal: float = 0.0 total_benefits_risk_adjusted: float = 0.0 total_benefits_pv: float = 0.0 total_costs_nominal: float = 0.0 total_costs_risk_adjusted: float = 0.0 total_costs_pv: float = 0.0 yearly_breakdown: list[dict] = field(default_factory=list) category_breakdown: list[dict] = field(default_factory=list) raw: dict = field(default_factory=dict) @classmethod def from_dict(cls, data: dict) -> TEISummary: return cls( npv=_as_float(data.get("npv")), roi=_as_float(data.get("roi")), payback_months=( _as_float(data.get("payback_months")) if data.get("payback_months") is not None else None ), discount_rate=_as_float(data.get("discount_rate"), 0.10), analysis_years=int(data.get("analysis_years") or 3), total_benefits_nominal=_as_float(data.get("total_benefits_nominal")), total_benefits_risk_adjusted=_as_float( data.get("total_benefits_risk_adjusted") ), total_benefits_pv=_as_float(data.get("total_benefits_pv")), total_costs_nominal=_as_float(data.get("total_costs_nominal")), total_costs_risk_adjusted=_as_float(data.get("total_costs_risk_adjusted")), total_costs_pv=_as_float(data.get("total_costs_pv")), yearly_breakdown=list(data.get("yearly_breakdown") or []), category_breakdown=list(data.get("category_breakdown") or []), raw=dict(data), )