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:
1
core/cli/__init__.py
Normal file
1
core/cli/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Palladium CLI package — invoked via ``python -m palladium``."""
|
||||
229
core/cli/main.py
Normal file
229
core/cli/main.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Palladium CLI implementation.
|
||||
|
||||
Subcommands::
|
||||
|
||||
palladium test # Verify Athena connectivity
|
||||
palladium list # List TEI tool instances
|
||||
palladium reports # List TEI report templates
|
||||
palladium summary <public_id> # Print a tool's financial summary
|
||||
palladium calculate <public_id> # Trigger /calculate
|
||||
palladium export <public_id> -o file # Save export envelope as JSON
|
||||
|
||||
The CLI is invoked via ``python -m palladium`` (root shim) which calls
|
||||
:func:`main` here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
|
||||
from core.export import build_report_data, write_report_data
|
||||
from core.tei_client import AthenaAPIError, TEIClient
|
||||
|
||||
|
||||
def _configure_logging(verbosity: int) -> None:
|
||||
level = logging.WARNING
|
||||
if verbosity == 1:
|
||||
level = logging.INFO
|
||||
elif verbosity >= 2:
|
||||
level = logging.DEBUG
|
||||
logging.basicConfig(
|
||||
level=level, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(
|
||||
prog="palladium",
|
||||
description="Palladium — TEI Calculator CLI for Athena.",
|
||||
)
|
||||
p.add_argument("-v", "--verbose", action="count", default=0)
|
||||
sub = p.add_subparsers(dest="command", required=True)
|
||||
|
||||
sub.add_parser("test", help="Verify Athena API connectivity")
|
||||
sub.add_parser("list", help="List TEI tool instances")
|
||||
sub.add_parser("reports", help="List TEI report templates")
|
||||
|
||||
s_summary = sub.add_parser("summary", help="Print a tool's financial summary")
|
||||
s_summary.add_argument("public_id")
|
||||
|
||||
s_calc = sub.add_parser("calculate", help="Trigger calculation for a tool")
|
||||
s_calc.add_argument("public_id")
|
||||
|
||||
s_export = sub.add_parser("export", help="Export a tool's report data as JSON")
|
||||
s_export.add_argument("public_id")
|
||||
s_export.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
default="-",
|
||||
help="Output path (default: stdout)",
|
||||
)
|
||||
s_export.add_argument(
|
||||
"--no-scenarios",
|
||||
action="store_true",
|
||||
help="Skip computing conservative/moderate/aggressive scenarios.",
|
||||
)
|
||||
s_export.add_argument(
|
||||
"--study-slug",
|
||||
default=None,
|
||||
help="Optional study identifier to embed in metadata.",
|
||||
)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def _print_table(rows: list[dict], columns: list[tuple[str, str]]) -> None:
|
||||
"""Tiny pure-stdlib pretty-printer."""
|
||||
widths = [len(label) for _, label in columns]
|
||||
formatted: list[list[str]] = []
|
||||
for r in rows:
|
||||
rec = [str(r.get(key, "") or "") for key, _ in columns]
|
||||
formatted.append(rec)
|
||||
for i, val in enumerate(rec):
|
||||
if len(val) > widths[i]:
|
||||
widths[i] = len(val)
|
||||
fmt = " ".join(f"{{:<{w}}}" for w in widths)
|
||||
print(fmt.format(*[label for _, label in columns]))
|
||||
print(fmt.format(*["-" * w for w in widths]))
|
||||
for rec in formatted:
|
||||
print(fmt.format(*rec))
|
||||
|
||||
|
||||
def cmd_test(client: TEIClient, args) -> int:
|
||||
result = client.test_connection()
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result.get("status") == "ok" else 1
|
||||
|
||||
|
||||
def cmd_list(client: TEIClient, args) -> int:
|
||||
tools = client.list_tools()
|
||||
if not tools:
|
||||
print("(no TEI tools)")
|
||||
return 0
|
||||
rows = []
|
||||
for t in tools:
|
||||
report = t.get("report")
|
||||
if isinstance(report, dict):
|
||||
report_name = report.get("name", "")
|
||||
else:
|
||||
report_name = report or ""
|
||||
rows.append(
|
||||
{
|
||||
"id": t.get("id", ""),
|
||||
"name": t.get("name", ""),
|
||||
"report": report_name,
|
||||
"status": t.get("status", ""),
|
||||
"version": t.get("current_version", ""),
|
||||
"modified": t.get("modified_date", ""),
|
||||
}
|
||||
)
|
||||
_print_table(
|
||||
rows,
|
||||
[
|
||||
("id", "PUBLIC_ID"),
|
||||
("name", "NAME"),
|
||||
("report", "REPORT"),
|
||||
("status", "STATUS"),
|
||||
("version", "VER"),
|
||||
("modified", "MODIFIED"),
|
||||
],
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_reports(client: TEIClient, args) -> int:
|
||||
reports = client.list_reports()
|
||||
if not reports:
|
||||
print("(no reports)")
|
||||
return 0
|
||||
rows = [
|
||||
{
|
||||
"id": r.get("id", ""),
|
||||
"name": r.get("name", ""),
|
||||
"vendor": r.get("vendor", ""),
|
||||
"version": r.get("version", ""),
|
||||
"fields": r.get("field_count", 0),
|
||||
"instances": r.get("instance_count", 0),
|
||||
"status": r.get("status", ""),
|
||||
}
|
||||
for r in reports
|
||||
]
|
||||
_print_table(
|
||||
rows,
|
||||
[
|
||||
("id", "PUBLIC_ID"),
|
||||
("name", "NAME"),
|
||||
("vendor", "VENDOR"),
|
||||
("version", "VER"),
|
||||
("fields", "FIELDS"),
|
||||
("instances", "TOOLS"),
|
||||
("status", "STATUS"),
|
||||
],
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_summary(client: TEIClient, args) -> int:
|
||||
client.print_summary(args.public_id)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_calculate(client: TEIClient, args) -> int:
|
||||
client.calculate(args.public_id)
|
||||
print(f"Recalculated {args.public_id}.")
|
||||
client.print_summary(args.public_id)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_export(client: TEIClient, args) -> int:
|
||||
envelope = build_report_data(
|
||||
client,
|
||||
args.public_id,
|
||||
include_scenarios=not args.no_scenarios,
|
||||
study_slug=args.study_slug,
|
||||
)
|
||||
if args.output in ("-", ""):
|
||||
json.dump(envelope, sys.stdout, indent=2, default=str)
|
||||
sys.stdout.write("\n")
|
||||
else:
|
||||
path = write_report_data(envelope, args.output)
|
||||
print(f"Wrote {path}")
|
||||
return 0
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"test": cmd_test,
|
||||
"list": cmd_list,
|
||||
"reports": cmd_reports,
|
||||
"summary": cmd_summary,
|
||||
"calculate": cmd_calculate,
|
||||
"export": cmd_export,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
_configure_logging(args.verbose)
|
||||
|
||||
try:
|
||||
client = TEIClient()
|
||||
except ValueError as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
handler = COMMANDS[args.command]
|
||||
try:
|
||||
return handler(client, args)
|
||||
except AthenaAPIError as e:
|
||||
print(f"Athena API error {e.status_code}: {e.detail}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user