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.
This commit is contained in:
13
.env.example
13
.env.example
@@ -10,3 +10,16 @@ ATHENA_API_KEY=your-api-key-here
|
|||||||
# PALLADIUM_TOOL_PUBLIC_ID=
|
# PALLADIUM_TOOL_PUBLIC_ID=
|
||||||
# PALLADIUM_PROPOSAL_ID=
|
# PALLADIUM_PROPOSAL_ID=
|
||||||
# PALLADIUM_ENGAGEMENT_ID=
|
# PALLADIUM_ENGAGEMENT_ID=
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Locale / display formatting (Streamlit app)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Currency symbol prefix (default: $)
|
||||||
|
# PALLADIUM_CURRENCY_SYMBOL=$
|
||||||
|
#
|
||||||
|
# Thousands separator (default: , for Americas/UK; use . for continental Europe)
|
||||||
|
# PALLADIUM_THOUSANDS_SEP=,
|
||||||
|
#
|
||||||
|
# Decimal separator (default: . for Americas/UK; use , for continental Europe)
|
||||||
|
# PALLADIUM_DECIMAL_SEP=.
|
||||||
|
|
||||||
|
|||||||
107
00_setup.ipynb
107
00_setup.ipynb
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 1,
|
"execution_count": 6,
|
||||||
"id": "53fcc345",
|
"id": "53fcc345",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"Palladium(root='palladium', athena='not tested')"
|
"Palladium(root='palladium', athena='not tested')"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 1,
|
"execution_count": 6,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 2,
|
"execution_count": 7,
|
||||||
"id": "853aaab8",
|
"id": "853aaab8",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 8,
|
||||||
"id": "9b7fcc97",
|
"id": "9b7fcc97",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"✅ Athena connected — https://athena.ouranos.helu.ca (0 report templates visible)\n"
|
"✅ Athena connected — https://athena.ouranos.helu.ca (1 report templates visible)\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -119,11 +119,11 @@
|
|||||||
"{'status': 'ok',\n",
|
"{'status': 'ok',\n",
|
||||||
" 'base_url': 'https://athena.ouranos.helu.ca',\n",
|
" 'base_url': 'https://athena.ouranos.helu.ca',\n",
|
||||||
" 'authenticated': True,\n",
|
" 'authenticated': True,\n",
|
||||||
" 'reports_found': 0,\n",
|
" 'reports_found': 1,\n",
|
||||||
" 'timestamp': '2026-06-10T06:45:10.418874'}"
|
" 'timestamp': '2026-06-10T07:08:06.947037'}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 3,
|
"execution_count": 8,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@@ -144,16 +144,69 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 9,
|
||||||
"id": "83edbe4d",
|
"id": "83edbe4d",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"data": {
|
||||||
"output_type": "stream",
|
"text/html": [
|
||||||
"text": [
|
"<div>\n",
|
||||||
"No TEI report templates yet — studies/202602_AmazonConnect/notebooks/00_provision.ipynb creates one.\n"
|
"<style scoped>\n",
|
||||||
]
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
|
" vertical-align: middle;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>id</th>\n",
|
||||||
|
" <th>name</th>\n",
|
||||||
|
" <th>vendor</th>\n",
|
||||||
|
" <th>version</th>\n",
|
||||||
|
" <th>status</th>\n",
|
||||||
|
" <th>analysis_period_years</th>\n",
|
||||||
|
" <th>discount_rate</th>\n",
|
||||||
|
" <th>field_count</th>\n",
|
||||||
|
" <th>instance_count</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>0</th>\n",
|
||||||
|
" <td>xsUTbjh4iDnJ</td>\n",
|
||||||
|
" <td>Amazon Connect 2026</td>\n",
|
||||||
|
" <td>AWS</td>\n",
|
||||||
|
" <td>1.0</td>\n",
|
||||||
|
" <td>active</td>\n",
|
||||||
|
" <td>3</td>\n",
|
||||||
|
" <td>0.1000</td>\n",
|
||||||
|
" <td>11</td>\n",
|
||||||
|
" <td>0</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" id name vendor version status \\\n",
|
||||||
|
"0 xsUTbjh4iDnJ Amazon Connect 2026 AWS 1.0 active \n",
|
||||||
|
"\n",
|
||||||
|
" analysis_period_years discount_rate field_count instance_count \n",
|
||||||
|
"0 3 0.1000 11 0 "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
@@ -172,7 +225,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 5,
|
"execution_count": 10,
|
||||||
"id": "a247bedd",
|
"id": "a247bedd",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -209,6 +262,30 @@
|
|||||||
"2. **Work the study** → notebooks `01_benefits` → `04_export` in the same folder.\n",
|
"2. **Work the study** → notebooks `01_benefits` → `04_export` in the same folder.\n",
|
||||||
"3. **Interactive data entry** → `make app` (or `streamlit run app/main.py`)."
|
"3. **Interactive data entry** → `make app` (or `streamlit run app/main.py`)."
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "d20d824f-e464-4ff7-8191-10c2495842a0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "630ee935-7c7b-47e5-9c13-6285316823e2",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7eba3877-8e51-443f-9953-9d0a48425f9f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ palladium/
|
|||||||
│ └── __main__.py
|
│ └── __main__.py
|
||||||
├── app/ # Streamlit UI — works with any TEI study
|
├── app/ # Streamlit UI — works with any TEI study
|
||||||
│ ├── main.py # entry point
|
│ ├── main.py # entry point
|
||||||
│ ├── pages/ # benefits, costs, summary, versions
|
│ ├── views/ # benefits, costs, summary, versions (NOT `pages/` — avoids Streamlit auto-multipage)
|
||||||
│ └── components/ # tables, charts
|
│ └── components/ # tables, charts
|
||||||
├── studies/ # One folder per TEI engagement
|
├── studies/ # One folder per TEI engagement
|
||||||
│ └── 202602_AmazonConnect/
|
│ └── 202602_AmazonConnect/
|
||||||
|
|||||||
@@ -9,24 +9,24 @@ from core.notebook_helpers import charts as core_charts
|
|||||||
|
|
||||||
def cashflow(yearly_breakdown, *, initial_cost: float = 0.0) -> None:
|
def cashflow(yearly_breakdown, *, initial_cost: float = 0.0) -> None:
|
||||||
fig = core_charts.cashflow_chart(yearly_breakdown, initial_cost=initial_cost)
|
fig = core_charts.cashflow_chart(yearly_breakdown, initial_cost=initial_cost)
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, width="stretch")
|
||||||
|
|
||||||
|
|
||||||
def benefits_bar(items) -> None:
|
def benefits_bar(items) -> None:
|
||||||
fig = core_charts.benefits_bar(items)
|
fig = core_charts.benefits_bar(items)
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, width="stretch")
|
||||||
|
|
||||||
|
|
||||||
def cost_pie(items) -> None:
|
def cost_pie(items) -> None:
|
||||||
fig = core_charts.cost_breakdown_pie(items)
|
fig = core_charts.cost_breakdown_pie(items)
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, width="stretch")
|
||||||
|
|
||||||
|
|
||||||
def scenario_bars(scenarios) -> None:
|
def scenario_bars(scenarios) -> None:
|
||||||
fig = core_charts.scenario_comparison(scenarios)
|
fig = core_charts.scenario_comparison(scenarios)
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, width="stretch")
|
||||||
|
|
||||||
|
|
||||||
def waterfall(values) -> None:
|
def waterfall(values) -> None:
|
||||||
fig = core_charts.waterfall(values)
|
fig = core_charts.waterfall(values)
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, width="stretch")
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ from __future__ import annotations
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import streamlit as st
|
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]:
|
def _years_for_table(fields: list[dict], analysis_years: int) -> list[int]:
|
||||||
"""Years 1..N — taken from analysis_period_years on the report."""
|
"""Years 1..N -- taken from analysis_period_years on the report."""
|
||||||
return list(range(1, max(int(analysis_years or 3), 1) + 1))
|
return list(range(1, max(int(analysis_years or 3), 1) + 1))
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +28,11 @@ def value_editor(
|
|||||||
columns, an ``initial`` column for costs, a risk_adjustment column, and
|
columns, an ``initial`` column for costs, a risk_adjustment column, and
|
||||||
a notes column. Returns the edited DataFrame; the caller is responsible
|
a notes column. Returns the edited DataFrame; the caller is responsible
|
||||||
for converting it back to value-row dicts and PUTting to Athena.
|
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 = [
|
fields = [
|
||||||
f
|
f
|
||||||
@@ -44,43 +51,76 @@ def value_editor(
|
|||||||
for f in fields:
|
for f in fields:
|
||||||
v = by_key.get(f["field_key"], {}) or {}
|
v = by_key.get(f["field_key"], {}) or {}
|
||||||
yv = v.get("year_values") or {}
|
yv = v.get("year_values") or {}
|
||||||
|
risk_raw = float(v.get("risk_adjustment") or 0.0)
|
||||||
row = {
|
row = {
|
||||||
"field_key": f["field_key"],
|
"field_key": f["field_key"],
|
||||||
"label": f.get("label", f["field_key"]),
|
"label": f.get("label", f["field_key"]),
|
||||||
"category": f.get("category", "") or "",
|
"category": f.get("category", "") or "",
|
||||||
}
|
}
|
||||||
if table == "costs":
|
if table == "costs":
|
||||||
row["Initial"] = float(v.get("initial") or 0.0)
|
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:
|
for y in years:
|
||||||
row[f"Year {y}"] = float(yv.get(str(y)) or 0.0)
|
raw = float(yv.get(str(y)) or 0.0)
|
||||||
row["risk_adj"] = float(v.get("risk_adjustment") 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 ""
|
row["notes"] = v.get("notes", "") or ""
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
df = pd.DataFrame(rows)
|
df = pd.DataFrame(rows)
|
||||||
|
|
||||||
|
_cur_fmt = currency_fmt()
|
||||||
|
_pct_fmt_str = pct_fmt()
|
||||||
|
|
||||||
column_config: dict = {
|
column_config: dict = {
|
||||||
"field_key": st.column_config.TextColumn("Key", disabled=True, width="small"),
|
"field_key": st.column_config.TextColumn("Key", disabled=True, width="small"),
|
||||||
"label": st.column_config.TextColumn("Field", disabled=True),
|
"label": st.column_config.TextColumn("Field", disabled=True),
|
||||||
"category": st.column_config.TextColumn("Category", disabled=True, width="small"),
|
"category": st.column_config.TextColumn("Category", disabled=True, width="small"),
|
||||||
"risk_adj": st.column_config.NumberColumn(
|
|
||||||
"Risk Adj.", min_value=0.0, max_value=1.0, step=0.05, format="%.2f"
|
|
||||||
),
|
|
||||||
"notes": st.column_config.TextColumn("Notes", width="medium"),
|
"notes": st.column_config.TextColumn("Notes", width="medium"),
|
||||||
}
|
}
|
||||||
if table == "costs":
|
|
||||||
column_config["Initial"] = st.column_config.NumberColumn(
|
if _STANDARD_LOCALE:
|
||||||
"Initial", format="$%.0f"
|
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%)",
|
||||||
)
|
)
|
||||||
for y in years:
|
if table == "costs":
|
||||||
column_config[f"Year {y}"] = st.column_config.NumberColumn(
|
column_config["Initial"] = st.column_config.NumberColumn(
|
||||||
f"Year {y}", format="$%.0f"
|
"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(
|
edited = st.data_editor(
|
||||||
df,
|
df,
|
||||||
column_config=column_config,
|
column_config=column_config,
|
||||||
use_container_width=True,
|
width="stretch",
|
||||||
num_rows="fixed",
|
num_rows="fixed",
|
||||||
hide_index=True,
|
hide_index=True,
|
||||||
key=key,
|
key=key,
|
||||||
|
|||||||
87
app/locale.py
Normal file
87
app/locale.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
Locale / formatting settings for the Palladium Streamlit app.
|
||||||
|
|
||||||
|
All settings are read from environment variables (via .env) so the same
|
||||||
|
codebase can be deployed for different regions without code changes.
|
||||||
|
|
||||||
|
Environment variables
|
||||||
|
---------------------
|
||||||
|
PALLADIUM_CURRENCY_SYMBOL Default: "$"
|
||||||
|
Prefix shown before monetary values (e.g. "$", "€", "£", "CAD ").
|
||||||
|
|
||||||
|
PALLADIUM_THOUSANDS_SEP Default: ","
|
||||||
|
Thousands separator used in number display (e.g. "," for Americas,
|
||||||
|
"." for continental Europe, " " for some locales).
|
||||||
|
|
||||||
|
PALLADIUM_DECIMAL_SEP Default: "."
|
||||||
|
Decimal separator (e.g. "." for Americas/UK, "," for continental Europe).
|
||||||
|
|
||||||
|
Note: Streamlit's NumberColumn ``format`` uses printf-style strings.
|
||||||
|
The ``%,`` flag (thousands separator) is supported in Streamlit ≥ 1.31.
|
||||||
|
For non-standard separators (e.g. European "." thousands / "," decimal)
|
||||||
|
the values are pre-formatted as strings and displayed in TextColumns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def _env(key: str, default: str) -> str:
|
||||||
|
return os.environ.get(key, default).strip()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Resolved settings (read once at import time; restart app to pick up changes)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CURRENCY_SYMBOL: str = _env("PALLADIUM_CURRENCY_SYMBOL", "$")
|
||||||
|
THOUSANDS_SEP: str = _env("PALLADIUM_THOUSANDS_SEP", ",")
|
||||||
|
DECIMAL_SEP: str = _env("PALLADIUM_DECIMAL_SEP", ".")
|
||||||
|
|
||||||
|
# True when the locale uses standard printf-compatible separators
|
||||||
|
# (i.e. "," thousands + "." decimal — the C/POSIX default).
|
||||||
|
# When False, we pre-format values as strings instead of relying on printf.
|
||||||
|
_STANDARD_LOCALE: bool = THOUSANDS_SEP == "," and DECIMAL_SEP == "."
|
||||||
|
|
||||||
|
|
||||||
|
def currency_fmt() -> str:
|
||||||
|
"""Return a Streamlit NumberColumn ``format`` string for currency.
|
||||||
|
|
||||||
|
For standard locales returns e.g. ``"$%,.0f"`` (thousands-separated,
|
||||||
|
no decimal places). For non-standard locales returns ``"%s"`` and
|
||||||
|
callers should use :func:`fmt_currency` to pre-format the value.
|
||||||
|
"""
|
||||||
|
if _STANDARD_LOCALE:
|
||||||
|
return f"{CURRENCY_SYMBOL}%,.0f"
|
||||||
|
return "%s"
|
||||||
|
|
||||||
|
|
||||||
|
def pct_fmt() -> str:
|
||||||
|
"""Return a Streamlit NumberColumn ``format`` string for percentages.
|
||||||
|
|
||||||
|
Stores the value as a fraction (0–1) and displays as e.g. ``"20.00%"``.
|
||||||
|
Streamlit's ``%%`` in format strings renders a literal ``%``.
|
||||||
|
"""
|
||||||
|
if _STANDARD_LOCALE:
|
||||||
|
return "%.2f%%"
|
||||||
|
return "%s"
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_currency(value: float) -> str:
|
||||||
|
"""Format *value* as a currency string using the configured locale."""
|
||||||
|
if _STANDARD_LOCALE:
|
||||||
|
return f"{CURRENCY_SYMBOL}{value:,.0f}"
|
||||||
|
# Non-standard: build manually
|
||||||
|
integer_part = f"{int(abs(value)):,}".replace(",", THOUSANDS_SEP)
|
||||||
|
sign = "-" if value < 0 else ""
|
||||||
|
return f"{sign}{CURRENCY_SYMBOL}{integer_part}"
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_pct(value: float) -> str:
|
||||||
|
"""Format *value* (0–1 fraction) as a percentage string."""
|
||||||
|
pct = value * 100
|
||||||
|
if _STANDARD_LOCALE:
|
||||||
|
return f"{pct:.2f}%"
|
||||||
|
integer_part = f"{int(pct)}"
|
||||||
|
decimal_part = f"{abs(pct) % 1:.2f}"[1:] # ".xx"
|
||||||
|
return f"{integer_part}{DECIMAL_SEP}{decimal_part[1:]}%"
|
||||||
34
app/main.py
34
app/main.py
@@ -24,6 +24,7 @@ if str(_ROOT) not in sys.path:
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from core.tei_client import AthenaAPIError, TEIClient
|
from core.tei_client import AthenaAPIError, TEIClient
|
||||||
|
from app.utils import icon, inject_icons
|
||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title="Palladium — TEI Calculator",
|
page_title="Palladium — TEI Calculator",
|
||||||
@@ -32,6 +33,7 @@ st.set_page_config(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@st.cache_resource(show_spinner=False)
|
@st.cache_resource(show_spinner=False)
|
||||||
def get_client() -> TEIClient:
|
def get_client() -> TEIClient:
|
||||||
return TEIClient()
|
return TEIClient()
|
||||||
@@ -75,7 +77,9 @@ def _crm_engagements(_client: TEIClient, client_name: str) -> list[dict]:
|
|||||||
|
|
||||||
def sidebar_tool_picker(client: TEIClient) -> dict | None:
|
def sidebar_tool_picker(client: TEIClient) -> dict | None:
|
||||||
"""Sidebar: pick an existing TEI tool or create one from a report template."""
|
"""Sidebar: pick an existing TEI tool or create one from a report template."""
|
||||||
st.sidebar.title("🛡️ Palladium")
|
st.sidebar.markdown(
|
||||||
|
f"{icon('shield-fill')} **Palladium**", unsafe_allow_html=True
|
||||||
|
)
|
||||||
st.sidebar.caption("TEI Calculator")
|
st.sidebar.caption("TEI Calculator")
|
||||||
|
|
||||||
tools = _safe_call(client.list_tools) or []
|
tools = _safe_call(client.list_tools) or []
|
||||||
@@ -163,15 +167,20 @@ def sidebar_tool_picker(client: TEIClient) -> dict | None:
|
|||||||
st.sidebar.markdown(f"**Public ID**: `{tool.get('id')}`")
|
st.sidebar.markdown(f"**Public ID**: `{tool.get('id')}`")
|
||||||
st.sidebar.markdown(f"**Status**: {tool.get('status', '?')}")
|
st.sidebar.markdown(f"**Status**: {tool.get('status', '?')}")
|
||||||
st.sidebar.markdown(f"**Version**: {tool.get('current_version', 0)}")
|
st.sidebar.markdown(f"**Version**: {tool.get('current_version', 0)}")
|
||||||
if st.sidebar.button("🔄 Recalculate"):
|
if st.sidebar.button("Recalculate"):
|
||||||
_safe_call(client.calculate, tool["id"])
|
_safe_call(client.calculate, tool["id"])
|
||||||
st.toast("Recalculated.", icon="✅")
|
st.toast("Recalculated.", icon=None)
|
||||||
st.cache_data.clear()
|
st.cache_data.clear()
|
||||||
return tool
|
return tool
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
st.title("Palladium — TEI Calculator")
|
inject_icons()
|
||||||
|
|
||||||
|
st.markdown(
|
||||||
|
f"<h1 style='margin-bottom:0'>{icon('shield-fill')} Palladium — TEI Calculator</h1>",
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = get_client()
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -186,14 +195,19 @@ def main() -> None:
|
|||||||
st.info("Pick or create a TEI tool from the sidebar to begin.")
|
st.info("Pick or create a TEI tool from the sidebar to begin.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Tab navigation — matches `app/pages/*` modules but kept as tabs so all
|
# Tab navigation — matches `app/views/*` modules but kept as tabs so all
|
||||||
# views share the chosen tool/state without re-querying.
|
# views share the chosen tool/state without re-querying.
|
||||||
tabs = st.tabs(["📊 Summary", "💰 Benefits", "💸 Costs", "🕒 Versions"])
|
#
|
||||||
|
# NOTE: the directory is `app/views/`, NOT `app/pages/`. Streamlit treats a
|
||||||
|
# `pages/` directory next to the entrypoint as auto-discovered multipage
|
||||||
|
# scripts, which would render blank since these modules only define
|
||||||
|
# `render()` and have no top-level output.
|
||||||
|
tabs = st.tabs(["Summary", "Benefits", "Costs", "Versions"])
|
||||||
|
|
||||||
from app.pages import benefits as benefits_page
|
from app.views import benefits as benefits_page
|
||||||
from app.pages import costs as costs_page
|
from app.views import costs as costs_page
|
||||||
from app.pages import summary as summary_page
|
from app.views import summary as summary_page
|
||||||
from app.pages import versions as versions_page
|
from app.views import versions as versions_page
|
||||||
|
|
||||||
with tabs[0]:
|
with tabs[0]:
|
||||||
summary_page.render(client, tool)
|
summary_page.render(client, tool)
|
||||||
|
|||||||
45
app/utils.py
Normal file
45
app/utils.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Shared UI utilities for the Palladium Streamlit app.
|
||||||
|
|
||||||
|
Kept in a separate module so that ``app.main`` and ``app.views.*`` can both
|
||||||
|
import from here without creating a circular dependency.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bootstrap Icons — injected once at the top of every page render.
|
||||||
|
# Using the CDN stylesheet so no npm/build step is needed.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_BI_CSS = """
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
/* Tighten up the default Streamlit header spacing */
|
||||||
|
.block-container { padding-top: 1.5rem; }
|
||||||
|
/* Make BI icons align nicely with surrounding text */
|
||||||
|
.bi { vertical-align: -0.125em; }
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def inject_icons() -> None:
|
||||||
|
"""Inject Bootstrap Icons CSS (idempotent — Streamlit deduplicates identical HTML)."""
|
||||||
|
st.markdown(_BI_CSS, unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
|
def icon(name: str, *, cls: str = "") -> str:
|
||||||
|
"""Return an inline Bootstrap Icon ``<i>`` tag.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
st.markdown(icon("bar-chart") + " Financial Summary", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
See the full icon catalogue at https://icons.getbootstrap.com/
|
||||||
|
"""
|
||||||
|
extra = f" {cls}" if cls else ""
|
||||||
|
return f'<i class="bi bi-{name}{extra}"></i>'
|
||||||
@@ -5,12 +5,17 @@ from __future__ import annotations
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from app.components.tables import df_to_values, value_editor
|
from app.components.tables import df_to_values, value_editor
|
||||||
from app.pages._helpers import report_meta, safe
|
from app.utils import icon
|
||||||
|
|
||||||
|
from app.views._helpers import report_meta, safe
|
||||||
from core.tei_client import TEIClient
|
from core.tei_client import TEIClient
|
||||||
|
|
||||||
|
|
||||||
def render(client: TEIClient, tool: dict) -> None:
|
def render(client: TEIClient, tool: dict) -> None:
|
||||||
st.header("💰 Benefits")
|
st.markdown(
|
||||||
|
f"<h2>{icon('graph-up-arrow')} Benefits</h2>",
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
public_id = tool["id"]
|
public_id = tool["id"]
|
||||||
report = report_meta(client, tool)
|
report = report_meta(client, tool)
|
||||||
analysis_years = int(report.get("analysis_period_years") or 3)
|
analysis_years = int(report.get("analysis_period_years") or 3)
|
||||||
@@ -32,7 +37,8 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
|
|
||||||
col1, col2 = st.columns([1, 4])
|
col1, col2 = st.columns([1, 4])
|
||||||
with col1:
|
with col1:
|
||||||
if st.button("💾 Save benefits", use_container_width=True):
|
if st.button("Save benefits", width="stretch"):
|
||||||
|
|
||||||
payload = df_to_values(edited, "benefits", analysis_years)
|
payload = df_to_values(edited, "benefits", analysis_years)
|
||||||
result = safe(client.update_values, public_id, payload)
|
result = safe(client.update_values, public_id, payload)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ from __future__ import annotations
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from app.components.tables import df_to_values, value_editor
|
from app.components.tables import df_to_values, value_editor
|
||||||
from app.pages._helpers import report_meta, safe
|
from app.utils import icon
|
||||||
|
|
||||||
|
from app.views._helpers import report_meta, safe
|
||||||
from core.tei_client import TEIClient
|
from core.tei_client import TEIClient
|
||||||
|
|
||||||
|
|
||||||
def render(client: TEIClient, tool: dict) -> None:
|
def render(client: TEIClient, tool: dict) -> None:
|
||||||
st.header("💸 Costs")
|
st.markdown(
|
||||||
|
f"<h2>{icon('receipt')} Costs</h2>",
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
public_id = tool["id"]
|
public_id = tool["id"]
|
||||||
report = report_meta(client, tool)
|
report = report_meta(client, tool)
|
||||||
analysis_years = int(report.get("analysis_period_years") or 3)
|
analysis_years = int(report.get("analysis_period_years") or 3)
|
||||||
@@ -32,7 +37,8 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
|
|
||||||
col1, col2 = st.columns([1, 4])
|
col1, col2 = st.columns([1, 4])
|
||||||
with col1:
|
with col1:
|
||||||
if st.button("💾 Save costs", use_container_width=True):
|
if st.button("Save costs", width="stretch"):
|
||||||
|
|
||||||
payload = df_to_values(edited, "costs", analysis_years)
|
payload = df_to_values(edited, "costs", analysis_years)
|
||||||
result = safe(client.update_values, public_id, payload)
|
result = safe(client.update_values, public_id, payload)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ from __future__ import annotations
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from app.components import charts
|
from app.components import charts
|
||||||
from app.pages._helpers import report_meta, safe
|
from app.locale import CURRENCY_SYMBOL, currency_fmt, fmt_currency
|
||||||
|
from app.utils import icon
|
||||||
|
from app.views._helpers import report_meta, safe
|
||||||
|
|
||||||
from core.export import build_report_data
|
from core.export import build_report_data
|
||||||
from core.tei_client import AthenaAPIError, TEIClient
|
from core.tei_client import AthenaAPIError, TEIClient
|
||||||
|
|
||||||
|
|
||||||
def render(client: TEIClient, tool: dict) -> None:
|
def render(client: TEIClient, tool: dict) -> None:
|
||||||
st.header("📊 Financial Summary")
|
st.markdown(
|
||||||
|
f"<h2>{icon('bar-chart-line')} Financial Summary</h2>",
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
public_id = tool["id"]
|
public_id = tool["id"]
|
||||||
report = report_meta(client, tool)
|
report = report_meta(client, tool)
|
||||||
|
|
||||||
@@ -39,14 +45,14 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
cpv = float(summary.get("total_costs_pv") or 0)
|
cpv = float(summary.get("total_costs_pv") or 0)
|
||||||
|
|
||||||
cols = st.columns(5)
|
cols = st.columns(5)
|
||||||
cols[0].metric("NPV", f"${npv/1_000_000:,.1f}M")
|
cols[0].metric("NPV", f"{CURRENCY_SYMBOL}{npv/1_000_000:,.1f}M")
|
||||||
cols[1].metric("ROI", f"{roi:,.0f}%")
|
cols[1].metric("ROI", f"{roi:,.0f}%")
|
||||||
cols[2].metric(
|
cols[2].metric(
|
||||||
"Payback",
|
"Payback",
|
||||||
f"{float(payback):.1f} months" if payback is not None else "N/A",
|
f"{float(payback):.1f} months" if payback is not None else "N/A",
|
||||||
)
|
)
|
||||||
cols[3].metric("Benefits PV", f"${bpv/1_000_000:,.1f}M")
|
cols[3].metric("Benefits PV", f"{CURRENCY_SYMBOL}{bpv/1_000_000:,.1f}M")
|
||||||
cols[4].metric("Costs PV", f"${cpv/1_000_000:,.1f}M")
|
cols[4].metric("Costs PV", f"{CURRENCY_SYMBOL}{cpv/1_000_000:,.1f}M")
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
@@ -64,7 +70,18 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
if yb:
|
if yb:
|
||||||
charts.cashflow(yb, initial_cost=initial)
|
charts.cashflow(yb, initial_cost=initial)
|
||||||
with st.expander("Cash flow table"):
|
with st.expander("Cash flow table"):
|
||||||
st.dataframe(yb, use_container_width=True, hide_index=True)
|
_cur = currency_fmt()
|
||||||
|
st.dataframe(
|
||||||
|
yb,
|
||||||
|
column_config={
|
||||||
|
"year": st.column_config.NumberColumn("Year", format="%d"),
|
||||||
|
"benefits": st.column_config.NumberColumn("Benefits", format=_cur),
|
||||||
|
"costs": st.column_config.NumberColumn("Costs", format=_cur),
|
||||||
|
"net": st.column_config.NumberColumn("Net", format=_cur),
|
||||||
|
},
|
||||||
|
width="stretch",
|
||||||
|
hide_index=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
st.caption("No yearly breakdown in this summary.")
|
st.caption("No yearly breakdown in this summary.")
|
||||||
|
|
||||||
@@ -94,11 +111,26 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
}
|
}
|
||||||
for k, v in envelope["scenarios"].items()
|
for k, v in envelope["scenarios"].items()
|
||||||
]
|
]
|
||||||
st.dataframe(rows, use_container_width=True, hide_index=True)
|
_cur = currency_fmt()
|
||||||
|
st.dataframe(
|
||||||
|
rows,
|
||||||
|
column_config={
|
||||||
|
"Scenario": st.column_config.TextColumn("Scenario"),
|
||||||
|
"Benefits PV": st.column_config.NumberColumn("Benefits PV", format=_cur),
|
||||||
|
"Costs PV": st.column_config.NumberColumn("Costs PV", format=_cur),
|
||||||
|
"NPV": st.column_config.NumberColumn("NPV", format=_cur),
|
||||||
|
"ROI %": st.column_config.NumberColumn("ROI %", format="%.1f%%"),
|
||||||
|
"Payback (months)": st.column_config.NumberColumn(
|
||||||
|
"Payback (months)", format="%.1f"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
width="stretch",
|
||||||
|
hide_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Export button
|
# Export button
|
||||||
st.divider()
|
st.divider()
|
||||||
if st.button("📦 Build export envelope (JSON)"):
|
if st.button("Build export envelope (JSON)"):
|
||||||
envelope = safe(
|
envelope = safe(
|
||||||
build_report_data,
|
build_report_data,
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from app.pages._helpers import safe
|
from app.utils import icon
|
||||||
|
|
||||||
|
from app.views._helpers import safe
|
||||||
from core.tei_client import TEIClient
|
from core.tei_client import TEIClient
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ def _diff_rows(a: dict[str, dict], b: dict[str, dict]) -> list[dict]:
|
|||||||
"""Return one row per field with side-by-side year values."""
|
"""Return one row per field with side-by-side year values."""
|
||||||
keys = sorted(set(a.keys()) | set(b.keys()))
|
keys = sorted(set(a.keys()) | set(b.keys()))
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
|
|
||||||
def _years_of(v: dict) -> dict:
|
def _years_of(v: dict) -> dict:
|
||||||
"""Accept both friendly (year_values) and wire (nested years) shapes."""
|
"""Accept both friendly (year_values) and wire (nested years) shapes."""
|
||||||
if isinstance(v.get("year_values"), dict):
|
if isinstance(v.get("year_values"), dict):
|
||||||
@@ -54,7 +57,10 @@ def _diff_rows(a: dict[str, dict], b: dict[str, dict]) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def render(client: TEIClient, tool: dict) -> None:
|
def render(client: TEIClient, tool: dict) -> None:
|
||||||
st.header("🕒 Versions")
|
st.markdown(
|
||||||
|
f"<h2>{icon('clock-history')} Versions</h2>",
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
public_id = tool["id"]
|
public_id = tool["id"]
|
||||||
|
|
||||||
versions = safe(client.list_versions, public_id) or []
|
versions = safe(client.list_versions, public_id) or []
|
||||||
@@ -63,7 +69,7 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Save new version
|
# Save new version
|
||||||
with st.expander("➕ Save current state as a new version", expanded=not versions):
|
with st.expander("Save current state as a new version", expanded=not versions):
|
||||||
note = st.text_area(
|
note = st.text_area(
|
||||||
"Version note",
|
"Version note",
|
||||||
placeholder=(
|
placeholder=(
|
||||||
@@ -71,7 +77,7 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
"raised legacy license cost from $160 to $180/agent.'"
|
"raised legacy license cost from $160 to $180/agent.'"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if st.button("💾 Save version", disabled=not note.strip()):
|
if st.button("Save version", disabled=not note.strip()):
|
||||||
result = safe(client.save_version, public_id, note.strip())
|
result = safe(client.save_version, public_id, note.strip())
|
||||||
if result:
|
if result:
|
||||||
st.success(
|
st.success(
|
||||||
@@ -102,7 +108,8 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
"Note": v.get("note", ""),
|
"Note": v.get("note", ""),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
st.dataframe(rows, use_container_width=True, hide_index=True)
|
st.dataframe(rows, width="stretch", hide_index=True)
|
||||||
|
|
||||||
|
|
||||||
# Compare two versions
|
# Compare two versions
|
||||||
st.subheader("Compare")
|
st.subheader("Compare")
|
||||||
@@ -132,4 +139,5 @@ def render(client: TEIClient, tool: dict) -> None:
|
|||||||
if not diff:
|
if not diff:
|
||||||
st.success("No value differences between these versions.")
|
st.success("No value differences between these versions.")
|
||||||
else:
|
else:
|
||||||
st.dataframe(diff, use_container_width=True, hide_index=True)
|
st.dataframe(diff, width="stretch", hide_index=True)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 5,
|
"execution_count": 2,
|
||||||
"id": "5bcc7740",
|
"id": "5bcc7740",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 6,
|
"execution_count": 3,
|
||||||
"id": "386ae38b",
|
"id": "386ae38b",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 4,
|
||||||
"id": "dc46ab46",
|
"id": "dc46ab46",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -198,20 +198,89 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 12,
|
"execution_count": 5,
|
||||||
"id": "4070b9c2",
|
"id": "4070b9c2",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"ename": "AttributeError",
|
"data": {
|
||||||
"evalue": "'TEIClient' object has no attribute 'list_clients'",
|
"text/html": [
|
||||||
"output_type": "error",
|
"<div>\n",
|
||||||
"traceback": [
|
"<style scoped>\n",
|
||||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
"\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)",
|
" vertical-align: middle;\n",
|
||||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m CLIENT_SEARCH = \u001b[33m\"Global\"\u001b[39m \u001b[38;5;66;03m# e.g. \"Acme\" — empty lists everyone\u001b[39;00m\n\u001b[32m 2\u001b[39m \n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m clients = client.list_clients(search=CLIENT_SEARCH \u001b[38;5;28;01mor\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 4\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m clients:\n\u001b[32m 5\u001b[39m display(pd.DataFrame(clients)[\n\u001b[32m 6\u001b[39m [c for c in (\"id\", \"name\", \"vertical\", \"client_type\", \"employee_count\",\n",
|
" }\n",
|
||||||
"\u001b[31mAttributeError\u001b[39m: 'TEIClient' object has no attribute 'list_clients'"
|
"\n",
|
||||||
]
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>id</th>\n",
|
||||||
|
" <th>name</th>\n",
|
||||||
|
" <th>vertical</th>\n",
|
||||||
|
" <th>client_type</th>\n",
|
||||||
|
" <th>employee_count</th>\n",
|
||||||
|
" <th>contact_center_agent_count</th>\n",
|
||||||
|
" <th>supervisor_count</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>0</th>\n",
|
||||||
|
" <td>2</td>\n",
|
||||||
|
" <td>Global Guardian Insurance</td>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" <td>For-Profit</td>\n",
|
||||||
|
" <td>12000</td>\n",
|
||||||
|
" <td>2500</td>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>1</th>\n",
|
||||||
|
" <td>3</td>\n",
|
||||||
|
" <td>Eudaimonix</td>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" <td>For-Profit</td>\n",
|
||||||
|
" <td>1500</td>\n",
|
||||||
|
" <td>300</td>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2</th>\n",
|
||||||
|
" <td>4</td>\n",
|
||||||
|
" <td>Aetherium Forge</td>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" <td>For-Profit</td>\n",
|
||||||
|
" <td>500</td>\n",
|
||||||
|
" <td>42</td>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" id name vertical client_type employee_count \\\n",
|
||||||
|
"0 2 Global Guardian Insurance None For-Profit 12000 \n",
|
||||||
|
"1 3 Eudaimonix None For-Profit 1500 \n",
|
||||||
|
"2 4 Aetherium Forge None For-Profit 500 \n",
|
||||||
|
"\n",
|
||||||
|
" contact_center_agent_count supervisor_count \n",
|
||||||
|
"0 2500 None \n",
|
||||||
|
"1 300 None \n",
|
||||||
|
"2 42 None "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
@@ -230,24 +299,112 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 10,
|
"execution_count": 6,
|
||||||
"id": "4e97978c",
|
"id": "4e97978c",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"ename": "NameError",
|
"name": "stdout",
|
||||||
"evalue": "name 'clients' is not defined",
|
"output_type": "stream",
|
||||||
"output_type": "error",
|
"text": [
|
||||||
"traceback": [
|
"\n",
|
||||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
"Client profile — no re-entry needed downstream:\n"
|
||||||
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
|
|
||||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m CLIENT_ID = \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;66;03m# ← set from the `id` column above, or leave for auto-pick\u001b[39;00m\n\u001b[32m 2\u001b[39m \n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m CLIENT_ID \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01mand\u001b[39;00m len(clients) == \u001b[32m1\u001b[39m:\n\u001b[32m 4\u001b[39m CLIENT_ID = clients[\u001b[32m0\u001b[39m][\u001b[33m\"id\"\u001b[39m]\n\u001b[32m 5\u001b[39m print(f\"Auto-selected the only client: {clients[\u001b[32m0\u001b[39m][\u001b[33m'name'\u001b[39m]} (id={CLIENT_ID})\")\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01massert\u001b[39;00m CLIENT_ID \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[33m\"Set CLIENT_ID from the table above and re-run this cell.\"\u001b[39m\n",
|
|
||||||
"\u001b[31mNameError\u001b[39m: name 'clients' is not defined"
|
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/html": [
|
||||||
|
"<div>\n",
|
||||||
|
"<style scoped>\n",
|
||||||
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
|
" vertical-align: middle;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>Global Guardian Insurance</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>id</th>\n",
|
||||||
|
" <td>2</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>name</th>\n",
|
||||||
|
" <td>Global Guardian Insurance</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>abbreviated_name</th>\n",
|
||||||
|
" <td>GGI</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>vertical</th>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>client_type</th>\n",
|
||||||
|
" <td>For-Profit</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>employee_count</th>\n",
|
||||||
|
" <td>12000</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>revenue</th>\n",
|
||||||
|
" <td>4500000000.0</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>contact_center_agent_count</th>\n",
|
||||||
|
" <td>2500</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>service_desk_agent_count</th>\n",
|
||||||
|
" <td>300</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>supervisor_count</th>\n",
|
||||||
|
" <td>None</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>location_count</th>\n",
|
||||||
|
" <td>120</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" Global Guardian Insurance\n",
|
||||||
|
"id 2\n",
|
||||||
|
"name Global Guardian Insurance\n",
|
||||||
|
"abbreviated_name GGI\n",
|
||||||
|
"vertical None\n",
|
||||||
|
"client_type For-Profit\n",
|
||||||
|
"employee_count 12000\n",
|
||||||
|
"revenue 4500000000.0\n",
|
||||||
|
"contact_center_agent_count 2500\n",
|
||||||
|
"service_desk_agent_count 300\n",
|
||||||
|
"supervisor_count None\n",
|
||||||
|
"location_count 120"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"CLIENT_ID = None # ← set from the `id` column above, or leave for auto-pick\n",
|
"CLIENT_ID = 2 # ← set from the `id` column above, or leave for auto-pick\n",
|
||||||
"\n",
|
"\n",
|
||||||
"if CLIENT_ID is None and len(clients) == 1:\n",
|
"if CLIENT_ID is None and len(clients) == 1:\n",
|
||||||
" CLIENT_ID = clients[0][\"id\"]\n",
|
" CLIENT_ID = clients[0][\"id\"]\n",
|
||||||
@@ -274,19 +431,60 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 11,
|
"execution_count": 7,
|
||||||
"id": "fcccc591",
|
"id": "fcccc591",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"ename": "NameError",
|
"data": {
|
||||||
"evalue": "name 'profile' is not defined",
|
"text/html": [
|
||||||
"output_type": "error",
|
"<div>\n",
|
||||||
"traceback": [
|
"<style scoped>\n",
|
||||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
|
" vertical-align: middle;\n",
|
||||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m CLIENT_ASSUMPTIONS = dict(seed.ASSUMPTIONS)\n\u001b[32m 2\u001b[39m overrides = {\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[33m\"agents_fte\"\u001b[39m: profile.get(\u001b[33m\"contact_center_agent_count\"\u001b[39m),\n\u001b[32m 4\u001b[39m \u001b[33m\"supervisors_fte\"\u001b[39m: profile.get(\u001b[33m\"supervisor_count\"\u001b[39m),\n\u001b[32m 5\u001b[39m }\n\u001b[32m 6\u001b[39m rows = []\n",
|
" }\n",
|
||||||
"\u001b[31mNameError\u001b[39m: name 'profile' is not defined"
|
"\n",
|
||||||
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>assumption</th>\n",
|
||||||
|
" <th>Forrester composite</th>\n",
|
||||||
|
" <th>Global Guardian Insurance (CRM)</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>0</th>\n",
|
||||||
|
" <td>agents_fte</td>\n",
|
||||||
|
" <td>2000</td>\n",
|
||||||
|
" <td>2500</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" assumption Forrester composite Global Guardian Insurance (CRM)\n",
|
||||||
|
"0 agents_fte 2000 2500"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Indicative scale factor vs composite: 1.25× (apply judgement — benefits don't all scale linearly)\n"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -326,10 +524,70 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 8,
|
||||||
"id": "57dec6cf",
|
"id": "57dec6cf",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Proposals for Global Guardian Insurance:\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/html": [
|
||||||
|
"<div>\n",
|
||||||
|
"<style scoped>\n",
|
||||||
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
|
" vertical-align: middle;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>id</th>\n",
|
||||||
|
" <th>name</th>\n",
|
||||||
|
" <th>status</th>\n",
|
||||||
|
" <th>opportunity</th>\n",
|
||||||
|
" <th>due_date</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>0</th>\n",
|
||||||
|
" <td>1</td>\n",
|
||||||
|
" <td>Secure Cloud Infrastructure Modernization</td>\n",
|
||||||
|
" <td>Draft</td>\n",
|
||||||
|
" <td>Secure Cloud Infrastructure Modernization</td>\n",
|
||||||
|
" <td>2026-08-28</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" id name status \\\n",
|
||||||
|
"0 1 Secure Cloud Infrastructure Modernization Draft \n",
|
||||||
|
"\n",
|
||||||
|
" opportunity due_date \n",
|
||||||
|
"0 Secure Cloud Infrastructure Modernization 2026-08-28 "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"proposals = client.proposals_for_client(CLIENT_ID)\n",
|
"proposals = client.proposals_for_client(CLIENT_ID)\n",
|
||||||
"engagements = client.engagements_for_client(CLIENT_NAME)\n",
|
"engagements = client.engagements_for_client(CLIENT_NAME)\n",
|
||||||
@@ -356,10 +614,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 9,
|
||||||
"id": "19336bcc",
|
"id": "19336bcc",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Auto-selected proposal 1: Secure Cloud Infrastructure Modernization\n",
|
||||||
|
"Attaching via: {'proposal': 1}\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"# Set exactly ONE of these (ids from the tables above). Leave both None to\n",
|
"# Set exactly ONE of these (ids from the tables above). Leave both None to\n",
|
||||||
"# auto-pick — single existing proposal/engagement wins; otherwise a sandbox\n",
|
"# auto-pick — single existing proposal/engagement wins; otherwise a sandbox\n",
|
||||||
@@ -406,10 +673,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 10,
|
||||||
"id": "017ae9db",
|
"id": "017ae9db",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Created tool pkrsQ9SRf654 attached to {'proposal': 1}\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"from core.tei_client import AthenaAPIError\n",
|
"from core.tei_client import AthenaAPIError\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -452,10 +727,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 11,
|
||||||
"id": "20e2a736",
|
"id": "20e2a736",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Pushed values for 8 fields.\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"payload = []\n",
|
"payload = []\n",
|
||||||
"for b in seed.BENEFITS: # nominal; Athena risk-adjusts via the field definition\n",
|
"for b in seed.BENEFITS: # nominal; Athena risk-adjusts via the field definition\n",
|
||||||
@@ -487,10 +770,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 12,
|
||||||
"id": "b7ac5d24",
|
"id": "b7ac5d24",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"════════════════════════════════════════════════════════\n",
|
||||||
|
" TEI Financial Summary\n",
|
||||||
|
"════════════════════════════════════════════════════════\n",
|
||||||
|
" Total Benefits (PV): $ 101,696,568\n",
|
||||||
|
" Total Costs (PV): $ 22,874,326\n",
|
||||||
|
"────────────────────────────────────────────────────────\n",
|
||||||
|
" Net Present Value: $ 78,822,242\n",
|
||||||
|
" ROI: 345%\n",
|
||||||
|
" Payback: 1.0 months\n",
|
||||||
|
"════════════════════════════════════════════════════════\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"summary = client.calculate(TOOL_ID)\n",
|
"summary = client.calculate(TOOL_ID)\n",
|
||||||
"client.print_summary(TOOL_ID)"
|
"client.print_summary(TOOL_ID)"
|
||||||
@@ -498,10 +798,90 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 13,
|
||||||
"id": "13d84001",
|
"id": "13d84001",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/html": [
|
||||||
|
"<div>\n",
|
||||||
|
"<style scoped>\n",
|
||||||
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
|
" vertical-align: middle;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>metric</th>\n",
|
||||||
|
" <th>published</th>\n",
|
||||||
|
" <th>athena</th>\n",
|
||||||
|
" <th>diff</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>0</th>\n",
|
||||||
|
" <td>total_benefits_pv</td>\n",
|
||||||
|
" <td>101,696,791</td>\n",
|
||||||
|
" <td>101,696,568</td>\n",
|
||||||
|
" <td>-0.00%</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>1</th>\n",
|
||||||
|
" <td>total_costs_pv</td>\n",
|
||||||
|
" <td>22,983,076</td>\n",
|
||||||
|
" <td>22,874,326</td>\n",
|
||||||
|
" <td>-0.47%</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2</th>\n",
|
||||||
|
" <td>net_present_value</td>\n",
|
||||||
|
" <td>78,713,715</td>\n",
|
||||||
|
" <td>78,822,242</td>\n",
|
||||||
|
" <td>+0.14%</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>3</th>\n",
|
||||||
|
" <td>roi_percentage</td>\n",
|
||||||
|
" <td>342</td>\n",
|
||||||
|
" <td>345</td>\n",
|
||||||
|
" <td>+0.76%</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" metric published athena diff\n",
|
||||||
|
"0 total_benefits_pv 101,696,791 101,696,568 -0.00%\n",
|
||||||
|
"1 total_costs_pv 22,983,076 22,874,326 -0.47%\n",
|
||||||
|
"2 net_present_value 78,713,715 78,822,242 +0.14%\n",
|
||||||
|
"3 roi_percentage 342 345 +0.76%"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Payback: 1 months (published: <6 months)\n",
|
||||||
|
"✅ Verified — Athena reproduces the published Forrester totals.\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"# Published Forrester totals (3-yr risk-adjusted PV @ 10%)\n",
|
"# Published Forrester totals (3-yr risk-adjusted PV @ 10%)\n",
|
||||||
"PUBLISHED = {\n",
|
"PUBLISHED = {\n",
|
||||||
@@ -539,10 +919,22 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 14,
|
||||||
"id": "148bdb2a",
|
"id": "148bdb2a",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Saved version 1 (baseline).\n",
|
||||||
|
"Saved to /Users/robert/git/palladium/.env:\n",
|
||||||
|
" PALLADIUM_REPORT_PUBLIC_ID=xsUTbjh4iDnJ\n",
|
||||||
|
" PALLADIUM_TOOL_PUBLIC_ID=pkrsQ9SRf654\n",
|
||||||
|
" PALLADIUM_PROPOSAL_ID=1\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"if not client.list_versions(TOOL_ID):\n",
|
"if not client.list_versions(TOOL_ID):\n",
|
||||||
" client.save_version(TOOL_ID, note=\"Baseline — published Forrester TEI figures (Feb 2026), moderate scenario.\")\n",
|
" client.save_version(TOOL_ID, note=\"Baseline — published Forrester TEI figures (Feb 2026), moderate scenario.\")\n",
|
||||||
@@ -578,6 +970,14 @@
|
|||||||
"- **Interactive editing** → `make app` / `streamlit run app/main.py` — the tool appears in the sidebar\n",
|
"- **Interactive editing** → `make app` / `streamlit run app/main.py` — the tool appears in the sidebar\n",
|
||||||
"- **CLI sanity check** → `python -m palladium summary $PALLADIUM_TOOL_PUBLIC_ID`"
|
"- **CLI sanity check** → `python -m palladium summary $PALLADIUM_TOOL_PUBLIC_ID`"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e9285087-5a2d-4a8d-856c-802474432892",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
Reference in New Issue
Block a user