from __future__ import annotations import argparse import json from pathlib import Path from app.db.bootstrap import create_all_tables from app.db.session import SessionLocal from app.mailer.reports.campaigns import CampaignReportError, generate_campaign_report, generate_jobs_csv from app.security.api_keys import authenticate_api_key from app.settings import settings def _print_text_report(report: dict) -> None: campaign = report["campaign"] cards = report["cards"] delivery = report["delivery"] print(f"Campaign: {campaign['name']} ({campaign['id']})") print(f"Status: {campaign['status']}") print(f"Jobs: {cards['jobs_total']} total | {cards['queueable']} queueable | {cards['needs_attention']} need attention") print(f"Sending: {cards['sent']} sent | {cards['failed']} failed") print(f"IMAP: {cards['imap_appended']} appended | {cards['imap_failed']} failed") if delivery.get("rate_limit", {}).get("messages_per_minute"): print( "Rate: " f"{delivery['rate_limit']['messages_per_minute']}/min, concurrency {delivery['rate_limit']['concurrency']}" ) if delivery.get("estimated_remaining_send_human"): print(f"ETA: {delivery['estimated_remaining_send_human']}") print("Validation counts:", report["status_counts"]["validation"]) print("Send counts: ", report["status_counts"]["send"]) print("Issue codes: ", report["issues"]["by_code"]) print("Attachments: ", report["attachments"]) failures = report.get("recent_failures") or [] if failures: print("Recent failures:") for failure in failures[:10]: print( f" - entry={failure['entry_index']} recipient={failure['recipient_email']} " f"send={failure['send_status']} imap={failure['imap_status']} error={failure['last_error']}" ) def main() -> None: parser = argparse.ArgumentParser(description="Generate a campaign status/report payload.") parser.add_argument("--campaign-id", required=True, help="Database campaign UUID") parser.add_argument("--api-key", default=settings.dev_bootstrap_api_key) parser.add_argument("--json", action="store_true", help="Print machine-readable JSON") parser.add_argument("--include-jobs", action="store_true", help="Include per-job rows in JSON output") parser.add_argument("--jobs-csv", help="Write per-job report CSV to this path") args = parser.parse_args() create_all_tables() with SessionLocal() as session: api_key = authenticate_api_key(session, args.api_key) if not api_key: raise SystemExit("Invalid API key") try: report = generate_campaign_report( session, tenant_id=api_key.tenant_id, campaign_id=args.campaign_id, include_jobs=args.include_jobs, ) if args.jobs_csv: csv_text = generate_jobs_csv(session, tenant_id=api_key.tenant_id, campaign_id=args.campaign_id) Path(args.jobs_csv).write_text(csv_text, encoding="utf-8") print(f"Wrote {args.jobs_csv}") if args.json: print(json.dumps(report, indent=2, ensure_ascii=False, default=str)) else: _print_text_report(report) except CampaignReportError as exc: raise SystemExit(str(exc)) from exc if __name__ == "__main__": main()