export_coaching.py
1 """CLI entry point for exporting coaching reports to Notion.""" 2 3 __all__ = ["main"] 4 5 import argparse 6 import json 7 import logging 8 import sys 9 from pathlib import Path 10 11 from pydantic import ValidationError 12 13 from cli._logging import setup_logging 14 from config import settings 15 from exceptions import APIError, ConfigurationError, DataError 16 from integrations.notion import NotionClient 17 from models.coaching import CandidateProfile 18 from pipeline._coach_cache import CACHE_DIR, safe_id 19 from pipeline.export_coaching import export_coaching_to_notion 20 21 logger = logging.getLogger(__name__) 22 23 24 def _parse_args() -> argparse.Namespace: 25 parser = argparse.ArgumentParser( 26 description=( 27 "Export coaching reports from cache to Notion pages.\n\n" 28 "Without --job-id, targets all uncoached pages in 'Ready to apply' status." 29 ) 30 ) 31 parser.add_argument( 32 "-p", 33 "--profile", 34 type=Path, 35 required=True, 36 help="Candidate profile JSON (dossier)", 37 ) 38 parser.add_argument( 39 "--job-id", 40 dest="job_ids", 41 action="append", 42 metavar="ID", 43 default=None, 44 help="Job ID to export (repeatable). If omitted, targets all uncoached pages.", 45 ) 46 parser.add_argument( 47 "--database-id", 48 default=None, 49 help="Notion database ID override (default: settings.notion_jobs_database_id)", 50 ) 51 return parser.parse_args() 52 53 54 def main() -> None: 55 """Entry point for the export_coaching CLI.""" 56 setup_logging("export_coaching") 57 args = _parse_args() 58 59 try: 60 try: 61 profile = CandidateProfile.model_validate( 62 json.loads(args.profile.read_text(encoding="utf-8")) 63 ) 64 except (OSError, json.JSONDecodeError, ValidationError) as exc: 65 raise DataError(f"Cannot load profile {args.profile}: {exc}") from exc 66 67 if not settings.notion_token: 68 raise ConfigurationError("NOTION_TOKEN") 69 notion_client = NotionClient(settings.notion_token) 70 notion_client.ping() 71 72 effective_db_id = args.database_id or settings.notion_jobs_database_id 73 if effective_db_id is None: 74 raise ConfigurationError("NOTION_JOBS_DATABASE_ID") 75 76 page_index = notion_client.get_uncoached_pages( 77 effective_db_id, 78 settings.notion_properties, 79 status_filter=settings.notion_statuses.manual_status, 80 ) 81 82 if args.job_ids: 83 target_ids = args.job_ids 84 else: 85 target_ids = list(page_index.keys()) 86 87 if not target_ids: 88 logger.warning("No target jobs found.") 89 return 90 91 exported = 0 92 failed: list[str] = [] 93 for job_id in target_ids: 94 if job_id not in page_index: 95 logger.warning( 96 "Job %r skipped -- not in Notion or already coached.", 97 job_id, 98 ) 99 continue 100 cache_path = CACHE_DIR / f"coach_{safe_id(job_id)}.json" 101 try: 102 export_coaching_to_notion( 103 cache_path=cache_path, 104 page_id=page_index[job_id], 105 profile=profile, 106 notion_client=notion_client, 107 coached_property=settings.notion_properties.coached, 108 ) 109 exported += 1 110 except (DataError, APIError) as exc: 111 logger.error("Failed to export %r: %s", job_id, exc) 112 failed.append(job_id) 113 114 logger.info("Exported %d coaching report(s) to Notion.", exported) 115 if failed: 116 logger.warning("Failed %d job(s): %s", len(failed), failed) 117 sys.exit(1) 118 119 except (ConfigurationError, DataError, APIError) as exc: 120 logger.error("%s", exc) 121 sys.exit(1) 122 123 124 if __name__ == "__main__": 125 main()