"""High-level Redmine -> CRM sync orchestrator.""" from __future__ import annotations import logging from typing import Any, Dict, List, Optional from redmine_crm.crm_client import CrmClient from redmine_crm.mapper import enrich, map_contact from redmine_crm.redmine_client import RedmineClient logger = logging.getLogger(__name__) class SyncState: """Minimal sync state tracker.""" def __init__(self, storage: Optional[Dict[str, Any]] = None) -> None: self._data = storage if storage is not None else {} def get_last_sync(self) -> Optional[str]: return self._data.get("last_sync_timestamp") def update(self, timestamp: str, stats: Dict[str, Any]) -> None: self._data["last_sync_timestamp"] = timestamp self._data["last_run_stats"] = stats def run_sync( redmine: RedmineClient, crm: CrmClient, sync_state: SyncState, *, base_url: str, batch_size: int = 100, ) -> Dict[str, Any]: """Run the full ETL pipeline: Extract, Transform, Load.""" last_sync = sync_state.get_last_sync() logger.info("Starting Redmine -> CRM sync; last_sync=%s", last_sync) # Extract contacts = redmine.iter_all_contacts( updated_since=last_sync, batch_size=batch_size, ) logger.info("Extracted %d Redmine contacts", len(contacts)) # Transform records: List[Dict[str, Any]] = [] for contact in contacts: try: mapped = map_contact(contact, base_url=base_url) enriched, _enrich_meta = enrich(mapped, contact) records.append(enriched) except Exception as exc: logger.exception("Failed to map Redmine contact %s: %s", contact.get("id"), exc) # Load stats = crm.sync_batch(records) logger.info("CRM upsert stats: %s", stats) import datetime sync_state.update(datetime.datetime.now(datetime.timezone.utc).isoformat(), stats) return stats