from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any import yaml @dataclass(frozen=True) class OutboundConfig: path: Path data: dict[str, Any] @property def name(self) -> str: return str(self.data["job"]["name"]) @property def source_project(self) -> str: return str(self.data["source"]["project"]) @property def source_query_path(self) -> Path: path = Path(str(self.data["source"]["query_file"])) return path if path.is_absolute() else (self.path.parent / path).resolve() @property def location(self) -> str: return str(self.data["source"].get("location", "US")) @property def state_project(self) -> str: return str(self.data.get("state", {}).get("project", self.source_project)) @property def state_dataset(self) -> str: return str(self.data["state"]["dataset"]) @property def nexuscrm_base_url(self) -> str: return str(self.data["target"]["base_url"]).rstrip("/") @property def nexuscrm_base_url_env(self) -> str: return str(self.data["target"].get("base_url_env", "NEXUSCRM_BASE_URL")) @property def nexuscrm_api_version(self) -> str: return str(self.data["target"].get("api_version", "v63.0")) @property def nexuscrm_object(self) -> str: return str(self.data["target"]["object"]) @property def nexuscrm_external_id_field(self) -> str: return str(self.data["target"]["external_id_field"]) @property def nexuscrm_client_id_env(self) -> str: return str(self.data["target"].get("client_id_env", "NEXUSCRM_CLIENT_ID")) @property def nexuscrm_client_secret_env(self) -> str: return str(self.data["target"].get("client_secret_env", "NEXUSCRM_CLIENT_SECRET")) @property def required_target_fields(self) -> list[str]: return list(self.data["target"].get("required_fields", [])) @property def batch_size(self) -> int: return int(self.data.get("execution", {}).get("batch_size", 200)) @property def request_timeout_seconds(self) -> float: return float(self.data.get("execution", {}).get("request_timeout_seconds", 30)) @property def max_http_attempts(self) -> int: return int(self.data.get("execution", {}).get("max_http_attempts", 4)) @property def lease_seconds(self) -> int: return int(self.data.get("execution", {}).get("lease_seconds", 3600)) def load_outbound_config(path: str | Path) -> OutboundConfig: config_path = Path(path).resolve() config = OutboundConfig(path=config_path, data=yaml.safe_load(config_path.read_text())) _validate(config) return config def _validate(config: OutboundConfig) -> None: if not config.source_query_path.is_file(): raise ValueError(f"source query file does not exist: {config.source_query_path}") if not 1 <= config.batch_size <= 200: raise ValueError("execution.batch_size must be between 1 and 200") if config.max_http_attempts < 1: raise ValueError("execution.max_http_attempts must be at least 1") if config.lease_seconds < 60: raise ValueError("execution.lease_seconds must be at least 60") if config.nexuscrm_external_id_field in config.required_target_fields: raise ValueError("target.external_id_field must not also appear in target.required_fields") if len(set(config.required_target_fields)) != len(config.required_target_fields): raise ValueError("target.required_fields must not contain duplicates")