Files
palladium/app/components/tables.py
Robert Helewka ecd164ee6d feat: add locale formatting config and update notebook outputs
Add configurable locale/display formatting environment variables
(`PALLADIUM_CURRENCY_SYMBOL`, `PALLADIUM_THOUSANDS_SEP`,
`PALLADIUM_DECIMAL_SEP`) to support regional number formatting in the
Streamlit app. Update `.env.example` with documentation for these new
variables.

Also refresh `00_setup.ipynb` with current execution outputs reflecting
a live Athena connection with report templates, a selected client
(Global Guardian Insurance, ID=2), and resolved NameError in assumption
override cells.
2026-06-10 11:54:28 -04:00

154 lines
5.5 KiB
Python

"""Streamlit data-editor wrappers for benefit/cost rows."""
from __future__ import annotations
import pandas as pd
import streamlit as st
from app.locale import currency_fmt, fmt_currency, fmt_pct, pct_fmt, _STANDARD_LOCALE
def _years_for_table(fields: list[dict], analysis_years: int) -> list[int]:
"""Years 1..N -- taken from analysis_period_years on the report."""
return list(range(1, max(int(analysis_years or 3), 1) + 1))
def value_editor(
table: str,
fields: list[dict],
values: list[dict],
*,
analysis_years: int,
key: str,
) -> pd.DataFrame:
"""
Render an ``st.data_editor`` for benefit or cost values.
The editor shows one row per field (filtered to ``table``), with year
columns, an ``initial`` column for costs, a risk_adjustment column, and
a notes column. Returns the edited DataFrame; the caller is responsible
for converting it back to value-row dicts and PUTting to Athena.
Currency columns use the locale configured via PALLADIUM_CURRENCY_SYMBOL /
PALLADIUM_THOUSANDS_SEP / PALLADIUM_DECIMAL_SEP in .env.
The risk_adj column is stored as a 0-1 fraction and displayed as a
percentage (e.g. 0.20 -> "20.00%").
"""
fields = [
f
for f in fields
if f.get("table") == table
# Companion "<key>_initial" fields are edited via the Initial column
# on their parent cost row, not as standalone rows.
and not str(f.get("field_key", "")).endswith("_initial")
]
fields.sort(key=lambda f: int(f.get("sort_order") or 0))
by_key = {v.get("field_key"): v for v in values}
years = _years_for_table(fields, analysis_years)
rows: list[dict] = []
for f in fields:
v = by_key.get(f["field_key"], {}) or {}
yv = v.get("year_values") or {}
risk_raw = float(v.get("risk_adjustment") or 0.0)
row = {
"field_key": f["field_key"],
"label": f.get("label", f["field_key"]),
"category": f.get("category", "") or "",
}
if table == "costs":
if _STANDARD_LOCALE:
row["Initial"] = float(v.get("initial") or 0.0)
else:
row["Initial"] = fmt_currency(float(v.get("initial") or 0.0))
for y in years:
raw = float(yv.get(str(y)) or 0.0)
if _STANDARD_LOCALE:
row[f"Year {y}"] = raw
else:
row[f"Year {y}"] = fmt_currency(raw)
# Risk adj: store as fraction for standard locales (NumberColumn handles
# display), or pre-format as "20.00%" string for non-standard locales.
if _STANDARD_LOCALE:
row["risk_adj"] = risk_raw
else:
row["risk_adj"] = fmt_pct(risk_raw)
row["notes"] = v.get("notes", "") or ""
rows.append(row)
df = pd.DataFrame(rows)
_cur_fmt = currency_fmt()
_pct_fmt_str = pct_fmt()
column_config: dict = {
"field_key": st.column_config.TextColumn("Key", disabled=True, width="small"),
"label": st.column_config.TextColumn("Field", disabled=True),
"category": st.column_config.TextColumn("Category", disabled=True, width="small"),
"notes": st.column_config.TextColumn("Notes", width="medium"),
}
if _STANDARD_LOCALE:
column_config["risk_adj"] = st.column_config.NumberColumn(
"Risk Adj.",
min_value=0.0,
max_value=1.0,
step=0.05,
format=_pct_fmt_str,
help="Enter as a decimal fraction (e.g. 0.20 = 20%)",
)
if table == "costs":
column_config["Initial"] = st.column_config.NumberColumn(
"Initial", format=_cur_fmt
)
for y in years:
column_config[f"Year {y}"] = st.column_config.NumberColumn(
f"Year {y}", format=_cur_fmt
)
else:
# Non-standard locale: display as pre-formatted strings (read-only display;
# user edits the raw number and we re-format on save).
column_config["risk_adj"] = st.column_config.TextColumn(
"Risk Adj.", help="Displayed as percentage; stored as 0-1 fraction"
)
if table == "costs":
column_config["Initial"] = st.column_config.TextColumn("Initial")
for y in years:
column_config[f"Year {y}"] = st.column_config.TextColumn(f"Year {y}")
edited = st.data_editor(
df,
column_config=column_config,
width="stretch",
num_rows="fixed",
hide_index=True,
key=key,
)
return edited
def df_to_values(df: pd.DataFrame, table: str, analysis_years: int) -> list[dict]:
"""Convert an edited DataFrame back to wire-format value rows."""
out: list[dict] = []
years = list(range(1, max(int(analysis_years or 3), 1) + 1))
for _, row in df.iterrows():
item: dict = {"field_key": row["field_key"], "table": table}
yv = {}
for y in years:
col = f"Year {y}"
if col in df.columns:
yv[str(y)] = float(row[col] or 0)
if yv:
item["year_values"] = yv
if table == "costs" and "Initial" in df.columns:
item["initial"] = float(row["Initial"] or 0)
ra = row.get("risk_adj")
if ra is not None and not pd.isna(ra):
item["risk_adjustment"] = float(ra)
notes = row.get("notes")
if isinstance(notes, str) and notes.strip():
item["notes"] = notes.strip()
out.append(item)
return out