"""Python 3.10+, standard library only. Server-side synchronous helper. Create ONE TokenProvider per credential/audience at application startup. """ import json import re import threading import time from datetime import datetime from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen class TokenProvider: def __init__(self, client_id: str, client_secret: str, audience: str): if not all(isinstance(x, str) and x.strip() for x in (client_id, client_secret, audience)): raise ValueError("Client ID, client secret and audience are required.") self._credentials = dict(clientId=client_id, clientSecret=client_secret, audience=audience) self._lock = threading.Lock() self._token = None self._refresh_at = 0.0 def get_token(self) -> str: # Check AFTER acquiring the lock: waiting callers reuse the replacement. with self._lock: if self._token and time.time() < self._refresh_at: return self._token request = Request( "https://identity.vinquery.com/connect/token", data=json.dumps(self._credentials).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST") try: with urlopen(request, timeout=30) as response: result = json.load(response) except HTTPError as error: status = error.code error.close() raise RuntimeError(f"VINquery token request failed (HTTP {status}).") from None except URLError: raise RuntimeError("VINquery token request failed.") from None except (ValueError, UnicodeError): raise RuntimeError("VINquery returned an invalid token response.") from None try: token = result["jwtToken"] # .NET can return seven fractional digits; Python 3.10 accepts # three or six. Truncate/pad to microseconds before parsing. raw_expiry = re.sub(r"\.(\d+)(?=Z$|[+-]\d{2}:\d{2}$)", lambda m: "." + m[1].ljust(6, "0")[:6], result["expiresUtc"]) expiry = datetime.fromisoformat(raw_expiry.replace("Z", "+00:00")) now = time.time() expires_at = expiry.timestamp() if (not isinstance(token, str) or not token.strip() or expiry.tzinfo is None or expires_at <= now): raise ValueError() except (KeyError, TypeError, AttributeError, ValueError, OverflowError, OSError): raise RuntimeError("VINquery returned an empty token or invalid expiry.") from None self._token = token self._refresh_at = expires_at - min(60.0, (expires_at - now) * 0.1) return token def invalidate(self, rejected_token: str) -> None: with self._lock: # A late 401 for an old token must not remove a newer token. if self._token == rejected_token: self._token = None self._refresh_at = 0.0