"""What an adapter is, and how the runner finds one. An adapter does I/O and parsing and nothing else: it yields records, and the runner matches, dedups and writes them. Keeping the split means an adapter can be tested against a captured payload with no database. """ from __future__ import annotations from typing import Iterator class BaseAdapter: """Subclasses implement :meth:`fetch`. The payload type is the service's own — a frame in tdas, an element set in odms. That is the cargo, not the machine, so it is not defined here. """ def __init__(self, source) -> None: self.source = source @property def config(self) -> dict: return self.source.config or {} def api_key(self) -> str | None: """This source's API key, or None. An inline ``api_key`` in the config wins; otherwise ``api_key_secret`` names a Docker secret or env var. Production keeps the key out of the database that way. """ direct = self.config.get("api_key") if direct: return str(direct).strip() secret_name = self.config.get("api_key_secret") if secret_name: from yksa_kit.settings import read_secret value = read_secret(str(secret_name)) return value.strip() if value else None return None def fetch(self) -> Iterator: raise NotImplementedError class AdapterRegistry(dict): """`kind` -> adapter class, with a decorator for declaring one. A kind with no adapter is legitimate — tdas's `internal_push` is fed by an API, odms's `internal_corrected` by a pipeline — and the runner records those as skipped rather than failed. """ def register(self, kind: str): def decorator(cls): self[kind] = cls return cls return decorator