"""CDEK OAuth2 authentication adapter.""" import asyncio import time from collections.abc import Callable from typing import Any import httpx class CDEKAuthError(RuntimeError): """Raised when CDEK OAuth2 flow fails.""" class CDEKAuthClient: def __init__( self, http_client: httpx.AsyncClient, *, base_url: str, client_id: str, client_secret: str, timeout_seconds: float = 10.0, expiry_skew_seconds: float = 30.0, clock: Callable[[], float] | None = None, ) -> None: self._http_client = http_client self._token_url = f"{base_url.rstrip('/')}/oauth/token" self._client_id = client_id self._client_secret = client_secret self._timeout_seconds = timeout_seconds self._expiry_skew_seconds = expiry_skew_seconds self._clock = clock or time.monotonic self._token: str | None = None self._token_expires_at: float = 0.0 self._refresh_lock = asyncio.Lock() async def get_access_token(self) -> str: if self._has_valid_token(): return self._cached_token() async with self._refresh_lock: if self._has_valid_token(): return self._cached_token() return await self._fetch_access_token() def _has_valid_token(self) -> bool: return self._token is not None and self._clock() < self._token_expires_at def _cached_token(self) -> str: if self._token is None: raise CDEKAuthError("CDEK OAuth token is unexpectedly missing.") return self._token async def _fetch_access_token(self) -> str: try: response = await self._http_client.post( self._token_url, data={ "grant_type": "client_credentials", "client_id": self._client_id, "client_secret": self._client_secret, }, timeout=self._timeout_seconds, ) response.raise_for_status() payload = response.json() token = self._extract_access_token(payload) expires_in = self._extract_expires_in(payload) except (httpx.HTTPError, TypeError, ValueError) as exc: raise CDEKAuthError("Failed to obtain CDEK OAuth token.") from exc self._token = token ttl_seconds = max(expires_in - self._expiry_skew_seconds, 0.0) self._token_expires_at = self._clock() + ttl_seconds return self._token @staticmethod def _extract_access_token(payload: Any) -> str: if not isinstance(payload, dict): raise ValueError("CDEK OAuth payload must be a JSON object.") token = payload.get("access_token") if not isinstance(token, str) or not token.strip(): raise ValueError("CDEK OAuth payload has no valid access_token.") return token @staticmethod def _extract_expires_in(payload: Any) -> float: if not isinstance(payload, dict): raise ValueError("CDEK OAuth payload must be a JSON object.") expires_in = payload.get("expires_in") if expires_in is None: raise ValueError("CDEK OAuth payload has no expires_in.") parsed_expires_in = float(expires_in) if parsed_expires_in < 0: raise ValueError("CDEK OAuth expires_in cannot be negative.") return parsed_expires_in