"""eBay OAuth2 client credentials token manager.""" from __future__ import annotations import base64 import time from typing import Optional import requests EBAY_OAUTH_URLS = { "production": "https://api.ebay.com/identity/v1/oauth2/token", "sandbox": "https://api.sandbox.ebay.com/identity/v1/oauth2/token", } class EbayTokenManager: """Fetches and caches eBay app-level OAuth tokens. Thread-safe for single process.""" def __init__(self, client_id: str, client_secret: str, env: str = "production"): self._client_id = client_id self._client_secret = client_secret self._token_url = EBAY_OAUTH_URLS[env] self._token: Optional[str] = None self._expires_at: float = 0.0 @property def client_id(self) -> str: return self._client_id def get_token(self) -> str: """Return a valid access token, fetching or refreshing as needed.""" if self._token and time.time() < self._expires_at - 60: return self._token self._fetch_token() return self._token # type: ignore[return-value] def _fetch_token(self) -> None: credentials = base64.b64encode( f"{self._client_id}:{self._client_secret}".encode() ).decode() resp = requests.post( self._token_url, headers={ "Authorization": f"Basic {credentials}", "Content-Type": "application/x-www-form-urlencoded", }, data={"grant_type": "client_credentials", "scope": "https://api.ebay.com/oauth/api_scope"}, ) resp.raise_for_status() data = resp.json() self._token = data["access_token"] self._expires_at = time.time() + data["expires_in"]