66 lines
1.9 KiB
Python
66 lines
1.9 KiB
Python
"""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
|