195 lines
6.6 KiB
Python
195 lines
6.6 KiB
Python
"""
|
|
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
|
|
the client's friendly value convention (see ``TEIClient.get_values``):
|
|
|
|
- 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),
|
|
)
|