refactor: restructure repo into core/app modules with per-study folders
Reorganize Palladium codebase into a modular architecture with `core/` shared logic and `app/` Streamlit UI, separating per-study assets into `studies/YYYYMM_<Vendor>/` folders containing notebooks, seed data, and configuration. Update README to reflect new structure, add `.gitignore` entries for `.env` and study exports, and refresh component documentation.
This commit is contained in:
194
core/tei_client/models.py
Normal file
194
core/tei_client/models.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
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),
|
||||
)
|
||||
Reference in New Issue
Block a user