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:
138
app/main.py
Normal file
138
app/main.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Palladium Streamlit app — TEI data entry, calculation, versioning, export.
|
||||
|
||||
Run from the project root::
|
||||
|
||||
streamlit run app/main.py
|
||||
|
||||
The app picks a TEI tool by ``public_id`` (or creates one from a Report
|
||||
template) and exposes Benefits, Costs, Summary, and Versions pages. It is
|
||||
study-agnostic — the field set is loaded dynamically from Athena based on
|
||||
the linked Report template.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Allow `streamlit run app/main.py` from project root without `pip install -e .`
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from core.tei_client import AthenaAPIError, TEIClient
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Palladium — TEI Calculator",
|
||||
page_icon="🛡️",
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
|
||||
@st.cache_resource(show_spinner=False)
|
||||
def get_client() -> TEIClient:
|
||||
return TEIClient()
|
||||
|
||||
|
||||
def _safe_call(fn, *args, **kwargs):
|
||||
"""Run an API call, surfacing errors as Streamlit messages."""
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except AthenaAPIError as e:
|
||||
st.error(f"Athena API error {e.status_code}: {e.detail}")
|
||||
except ValueError as e:
|
||||
st.error(str(e))
|
||||
return None
|
||||
|
||||
|
||||
def sidebar_tool_picker(client: TEIClient) -> dict | None:
|
||||
"""Sidebar: pick an existing TEI tool or create one from a report template."""
|
||||
st.sidebar.title("🛡️ Palladium")
|
||||
st.sidebar.caption("TEI Calculator")
|
||||
|
||||
tools = _safe_call(client.list_tools) or []
|
||||
if tools:
|
||||
labels = {
|
||||
f"{t.get('name', '(unnamed)')} — {t.get('id', '')[:8]}…": t for t in tools
|
||||
}
|
||||
choice = st.sidebar.selectbox("TEI Tool", list(labels.keys()))
|
||||
tool = labels[choice]
|
||||
else:
|
||||
st.sidebar.info("No TEI tools yet. Create one below.")
|
||||
tool = None
|
||||
|
||||
with st.sidebar.expander("Create new tool"):
|
||||
reports = _safe_call(client.list_reports) or []
|
||||
if not reports:
|
||||
st.write("No report templates available.")
|
||||
else:
|
||||
report_labels = {f"{r['name']} ({r['vendor']} {r['version']})": r for r in reports}
|
||||
r_choice = st.selectbox("Report template", list(report_labels.keys()))
|
||||
new_name = st.text_input("Tool name (optional)", "")
|
||||
proposal_id = st.number_input(
|
||||
"Proposal ID (optional)", min_value=0, value=0, step=1
|
||||
)
|
||||
if st.button("Create"):
|
||||
report = report_labels[r_choice]
|
||||
created = _safe_call(
|
||||
client.create_tool,
|
||||
report_public_id=report["id"],
|
||||
proposal=int(proposal_id) or None,
|
||||
name=new_name or None,
|
||||
)
|
||||
if created:
|
||||
st.success(f"Created tool {created.get('id')}")
|
||||
st.rerun()
|
||||
|
||||
if tool:
|
||||
st.sidebar.divider()
|
||||
st.sidebar.markdown(f"**Public ID**: `{tool.get('id')}`")
|
||||
st.sidebar.markdown(f"**Status**: {tool.get('status', '?')}")
|
||||
st.sidebar.markdown(f"**Version**: {tool.get('current_version', 0)}")
|
||||
if st.sidebar.button("🔄 Recalculate"):
|
||||
_safe_call(client.calculate, tool["id"])
|
||||
st.toast("Recalculated.", icon="✅")
|
||||
st.cache_data.clear()
|
||||
return tool
|
||||
|
||||
|
||||
def main() -> None:
|
||||
st.title("Palladium — TEI Calculator")
|
||||
try:
|
||||
client = get_client()
|
||||
except ValueError as e:
|
||||
st.error(str(e))
|
||||
st.info("Set ATHENA_BASE_URL and ATHENA_API_KEY in your `.env` file.")
|
||||
st.stop()
|
||||
return
|
||||
|
||||
tool = sidebar_tool_picker(client)
|
||||
|
||||
if tool is None:
|
||||
st.info("Pick or create a TEI tool from the sidebar to begin.")
|
||||
return
|
||||
|
||||
# Tab navigation — matches `app/pages/*` modules but kept as tabs so all
|
||||
# views share the chosen tool/state without re-querying.
|
||||
tabs = st.tabs(["📊 Summary", "💰 Benefits", "💸 Costs", "🕒 Versions"])
|
||||
|
||||
from app.pages import benefits as benefits_page
|
||||
from app.pages import costs as costs_page
|
||||
from app.pages import summary as summary_page
|
||||
from app.pages import versions as versions_page
|
||||
|
||||
with tabs[0]:
|
||||
summary_page.render(client, tool)
|
||||
with tabs[1]:
|
||||
benefits_page.render(client, tool)
|
||||
with tabs[2]:
|
||||
costs_page.render(client, tool)
|
||||
with tabs[3]:
|
||||
versions_page.render(client, tool)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user