"""Shared base for LFX + Langflow async HTTP clients. Consolidates base_url stripping, header building, per-request client construction, and HTTPError → domain-error mapping. Subclasses only declare their error types and endpoint calls. Transport-level failures (connect/timeout, never 4xx) retry with exponential backoff via tenacity. """ from __future__ import annotations import httpx from tenacity import ( retry, retry_if_exception_type, stop_after_attempt, wait_exponential, ) class BaseApiClient: """Thin wrapper around httpx.AsyncClient construction.""" def __init__(self, base_url: str, timeout: float = 60.0, api_key: str | None = None) -> None: self.base_url = base_url.rstrip("/") self.timeout = timeout self.api_key = api_key or "" def _headers(self) -> dict[str, str]: return {"x-api-key": self.api_key} if self.api_key else {} def _client(self) -> httpx.AsyncClient: return httpx.AsyncClient( base_url=self.base_url, timeout=self.timeout, headers=self._headers() ) def transport_retry(func): # type: ignore[no-untyped-def] """Retry transport errors 3x with backoff; HTTP status errors pass through.""" return retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, max=4), retry=retry_if_exception_type((httpx.TransportError, httpx.TimeoutException)), reraise=True, )(func)