← Files ClaraARCHIVED FILE

modules/reporting-engine/scripts/budget_report.py

4.43 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

"""Build Clara's budget report using the shared management-control calculation core."""

from __future__ import annotations

# Direct CLI calls must select dependencies before importing workflow modules.
if __name__ == "__main__":
    import runpy as _runpy
    from pathlib import Path as _Path

    for _parent in _Path(__file__).resolve().parents:
        _launcher = _parent / "scripts" / "self_relaunch.py"
        if not _launcher.is_file():
            _launcher = _parent / "clara" / "scripts" / "self_relaunch.py"
        if _launcher.is_file():
            _runpy.run_path(str(_launcher))["ensure_running_in_managed_venv"](__file__)
            break
    else:
        # Standalone components retain their host's dependency setup.
        if any(
            (_p / "components.json").is_file()
            for _p in _Path(__file__).resolve().parents
        ):
            raise SystemExit(
                "Managed Python launcher is missing; rebuild the plugin package."
            )


import argparse
import logging
import sys
from pathlib import Path

__all__ = ["main"]

CLARA_ROOT = Path(__file__).resolve().parents[3]
COMPONENT = next(
    (
        candidate
        for candidate in (
            CLARA_ROOT / "modules/management-control-pack",
            CLARA_ROOT.parent / "management-control-pack",
        )
        if candidate.is_dir()
    ),
    CLARA_ROOT / "modules/management-control-pack",
)
sys.path.insert(0, str(COMPONENT / "scripts"))
for _vendor in (
    COMPONENT / "vendor/modules",
    CLARA_ROOT.parent / "_shared/vendor/modules",
):
    if (_vendor / "reporting_table.py").is_file():
        sys.path.insert(0, str(_vendor))
        break

from management_control_core import (  # noqa: E402
    build_inspection,
    build_management_pack,
    load_json,
    load_source_tables,
    write_json,
)
from management_delivery import write_pack_outputs  # noqa: E402
from management_site import prepare_site  # noqa: E402


def main(argv: list[str] | None = None) -> int:
    """Inspect, calculate or prepare a reviewed report without a Vera client binding."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("action", choices=("inspect", "run", "site"))
    parser.add_argument("--input", type=Path, action="append", required=True)
    parser.add_argument("--recipe", type=Path)
    parser.add_argument("--pack", type=Path)
    parser.add_argument("--commentary", type=Path)
    parser.add_argument("--audience")
    parser.add_argument("--output-dir", type=Path, required=True)
    args = parser.parse_args(argv)
    try:
        if args.output_dir.exists():
            raise ValueError(
                "Use a fresh report output folder; preserve earlier reports."
            )
        if args.action == "inspect":
            inspection, controls, template = build_inspection(
                load_source_tables(args.input)
            )
            args.output_dir.mkdir(parents=True)
            write_json(args.output_dir / "inspection.json", inspection)
            write_json(args.output_dir / "mapping_control.json", controls)
            write_json(args.output_dir / "recipe_template.json", template)
        elif args.recipe is None:
            raise ValueError("A reviewed --recipe is required.")
        elif args.action == "run":
            pack = build_management_pack(
                load_source_tables(args.input), load_json(args.recipe)
            )
            write_pack_outputs(
                pack,
                inputs=args.input,
                recipe_path=args.recipe,
                output_dir=args.output_dir,
                commentary_path=args.commentary,
            )
            return 2 if pack["status"] == "blocked" else 0
        elif args.pack is None or args.audience is None:
            raise ValueError("Sites preparation requires --pack and --audience.")
        else:
            prepare_site(
                inputs=args.input,
                recipe=load_json(args.recipe),
                pack=load_json(args.pack),
                output=args.output_dir,
                audience=args.audience,
                commentary=load_json(args.commentary) if args.commentary else None,
            )
    except (OSError, ValueError) as exc:
        parser.error(str(exc))
    logging.getLogger(__name__).info(
        "Wrote budget report artifacts: %s", args.output_dir
    )
    return 0


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    raise SystemExit(main())

SHA-256: fe69859b4b64c212b2ee4672dcabb73802e8840bc9f5cc312c13542ad7c837dc