"""Command-line entry point for deterministic database monthly reports.""" from __future__ import annotations import argparse import json import re import sys from datetime import date from pathlib import Path from typing import Optional, Sequence, Tuple from monthly_reports.contracts import ErrorCode, RESULT_SCHEMA_VERSION from arr_web.processing_runtime import compose_object_store from monthly_reports.publishing import OpenpyxlWorkbookBuilder, AtomicReportPublisher from monthly_reports.repository import DatabaseConfig, PostgresReportRepository, RepositoryError from monthly_reports.service import MonthlyReportService, RunRequest, write_run_result PROJECT_ROOT = Path(__file__).resolve().parents[1] MONTH_RE = re.compile(r"^(\d{4})-(\d{2})$") class RequestFailure(ValueError): pass class SafeArgumentParser(argparse.ArgumentParser): def error(self, message: str) -> None: raise RequestFailure(message) def _parse_month(value: str) -> Tuple[int, int]: match = MONTH_RE.fullmatch(value) if not match: raise RequestFailure("month must use YYYY-MM") year, month = int(match.group(1)), int(match.group(2)) if month < 1 or month > 12: raise RequestFailure("month is invalid") return year, month def _parse_date(value: str) -> date: try: return date.fromisoformat(value) except ValueError: raise RequestFailure("as-of date must use YYYY-MM-DD") from None def _parser() -> SafeArgumentParser: parser = SafeArgumentParser(prog="monthly-reports") subparsers = parser.add_subparsers(dest="command", required=True) generate = subparsers.add_parser( "generate", help="generate or explicitly rerun one database-backed monthly report", ) generate.add_argument("--month", required=True, help="report month in YYYY-MM") generate.add_argument("--as-of", required=True, help="latest included business date") generate.add_argument("--output-root", help="controlled output root inside the project") return parser def _failure(code: str, stage: str) -> dict: return { "schema_version": RESULT_SCHEMA_VERSION, "status": "failed", "warnings": [], "errors": [{"code": code, "stage": stage}], } def _emit(payload: dict) -> None: sys.stdout.write(json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n") def main(argv: Optional[Sequence[str]] = None) -> int: try: args = _parser().parse_args(argv) year, month = _parse_month(args.month) as_of_date = _parse_date(args.as_of) request = RunRequest(year, month, as_of_date) request.validate() output_root = ( Path(args.output_root).expanduser() if args.output_root else PROJECT_ROOT / "outputs" / "monthly_reports" ) if not output_root.is_absolute(): output_root = PROJECT_ROOT / output_root output_root = output_root.resolve() repository = PostgresReportRepository(DatabaseConfig.from_environment()) storage_runtime = compose_object_store() try: service = MonthlyReportService( repository, OpenpyxlWorkbookBuilder(), AtomicReportPublisher( PROJECT_ROOT, output_root, object_store=storage_runtime.object_store, ), output_root / ".staging", ) result = service.run(request) finally: storage_runtime.close() result_path = ( output_root / f"{year:04d}" / f"{month:02d}" / "runs" / f"monthly-report-{year:04d}-{month:02d}-{as_of_date.isoformat()}.result.json" ) write_run_result(result_path, result) _emit(result.to_dict()) return result.exit_code except (RequestFailure, ValueError): _emit(_failure(ErrorCode.REQUEST_INVALID, "request")) return 3 except RepositoryError as error: code = ( ErrorCode.REQUEST_INVALID if error.code == ErrorCode.REQUEST_INVALID else ErrorCode.DATABASE_FAILED ) _emit(_failure(code, "configuration")) return 3 if code == ErrorCode.REQUEST_INVALID else 4 except Exception: _emit(_failure(ErrorCode.INTERNAL_ERROR, "internal")) return 4 if __name__ == "__main__": raise SystemExit(main())