/ cli / export_coaching.py
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()