"""Switch-preset contract. A preset tells the portal: * what human-readable label to show in the dropdown * which credential fields to render * what CDR format adapter to hook the output through * what cron cadence is appropriate by default And provides two methods to the cdr_puller worker: * validate(profile) → (ok, detail) — "test connection" button * fetch(profile, since) → Iterator[(remote_path, bytes)] — pull new files Credentials on ``profile`` are loaded at the puller layer from the ERPNext Sensitive ID record linked by ``profile.pull_sensitive_id``. """ from __future__ import annotations import logging from dataclasses import dataclass from datetime import datetime from typing import Iterable, Optional logger = logging.getLogger(__name__) @dataclass class CredentialField: """One field the portal should render in the preset credential form.""" key: str # JSON key stored under profile.preset_config / Sensitive ID label: str # shown on the form kind: str # 'text' | 'password' | 'ssh_key' | 'select' | 'number' required: bool = True sensitive: bool = False # if True, stored encrypted in Sensitive ID not profile_config help: str = "" options: Optional[list[str]] = None # for kind='select' @dataclass class FetchedFile: remote_path: str mtime: datetime content: bytes size_bytes: int class BasePreset: """Abstract preset. Subclasses set class attributes + implement methods.""" PRESET_SLUG: str = "" # matches cdr_ingestion_profiles.switch_preset LABEL: str = "" # shown in the portal dropdown CDR_FORMAT: str = "generic_csv" # matches cdr_ingestion_profiles.format TRANSPORT_METHOD: str = "" # 'api' | 'scrape' | 'sftp' | 'ftps' DEFAULT_CRON: str = "0 2 * * *" CREDENTIAL_FIELDS: tuple[CredentialField, ...] = () def validate(self, profile_config: dict, secrets: dict) -> tuple[bool, str]: """Test-connection hook for the portal. Returns (ok, detail).""" raise NotImplementedError def fetch( self, profile_config: dict, secrets: dict, since: Optional[datetime], ) -> Iterable[FetchedFile]: """Yield new CDR files since `since`. Puller streams them to MinIO.""" raise NotImplementedError