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.
230 lines
6.4 KiB
Python
230 lines
6.4 KiB
Python
"""
|
|
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())
|