"""Конфигурация клиента.
Прямые аргументы конструктора сильнее env: клиент в коде сервиса обычно
создают с url/api_key, а env-фолбэк — путь «развернул по умолчанию».
Имена переменных фиксированы для всей экосистемы: SYNAPSE_URL,
SYNAPSE_API_KEY, SYNAPSE_TIMEOUT, SYNAPSE_DEFAULT_SOURCE.
"""
from __future__ import annotations
import os
from collections.abc import Mapping
from dataclasses import dataclass, field
ENV_URL = "SYNAPSE_URL"
ENV_API_KEY = "SYNAPSE_API_KEY"
ENV_TIMEOUT = "SYNAPSE_TIMEOUT"
ENV_DEFAULT_SOURCE = "SYNAPSE_DEFAULT_SOURCE"
DEFAULT_TIMEOUT = 10.0
@dataclass(frozen=True, slots=True)
class SynapseConfig:
base_url: str # без хвостового "/"
api_key: str | None = None
timeout: float = DEFAULT_TIMEOUT
default_source: str | None = None
env: Mapping[str, str] = field(default_factory=lambda: dict(os.environ), repr=False)
@classmethod
def from_env(cls, env: Mapping[str, str] | None = None) -> SynapseConfig:
env = dict(os.environ) if env is None else dict(env)
return cls(
base_url=env.get(ENV_URL, ""),
api_key=env.get(ENV_API_KEY) or None,
timeout=_timeout(env),
default_source=env.get(ENV_DEFAULT_SOURCE) or None,
env=env,
)
def url(self, path: str) -> str:
"""base_url + path; путь в path — с начальным '/'."""
return f"{self.base_url.rstrip('/')}{path}"
def _timeout(env: Mapping[str, str]) -> float:
raw = (env.get(ENV_TIMEOUT) or "").strip()
if not raw:
return DEFAULT_TIMEOUT
try:
value = float(raw)
except ValueError:
raise ValueError(f"{ENV_TIMEOUT}={raw!r} не числится как число секунд") from None
if value <= 0:
raise ValueError(f"{ENV_TIMEOUT}={raw!r} должен быть > 0")
return value