"""Shared `httpx.Client` helper. Creating short-lived httpx clients disables connection pooling and costs extra CPU. This module provides a small keyed client cache for callers that just need basic GETs without the full HTTPClient wrapper. """ from __future__ import annotations import atexit from collections import OrderedDict import threading from typing import Any, Dict, Optional, Tuple import httpx from API.ssl_certs import resolve_verify_value _DEFAULT_USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36" ) _lock = threading.Lock() _MAX_SHARED_CLIENTS = 8 _shared_clients: "OrderedDict[Tuple[float, Tuple[str, str], Tuple[Tuple[str, str], ...]], httpx.Client]" = OrderedDict() def _normalize_headers(headers: Optional[Dict[str, str]]) -> Dict[str, str]: normalized: Dict[str, str] = {"User-Agent": _DEFAULT_USER_AGENT} if headers: normalized.update({str(k): str(v) for k, v in headers.items()}) return normalized def _verify_key(verify_value: Any) -> Tuple[str, str]: if isinstance(verify_value, bool): return ("bool", "1" if verify_value else "0") if isinstance(verify_value, str): return ("str", verify_value) return ("obj", str(id(verify_value))) def _client_key( *, timeout: float, verify_value: Any, merged_headers: Dict[str, str], ) -> Tuple[float, Tuple[str, str], Tuple[Tuple[str, str], ...]]: header_items = tuple( sorted((str(k).lower(), str(v)) for k, v in merged_headers.items()) ) return (float(timeout), _verify_key(verify_value), header_items) def get_shared_httpx_client( *, timeout: float = 30.0, verify_ssl: bool | str = True, headers: Optional[Dict[str, str]] = None, ) -> httpx.Client: """Return a shared synchronous httpx.Client for a specific config key.""" verify_value = resolve_verify_value(verify_ssl) merged_headers = _normalize_headers(headers) key = _client_key( timeout=timeout, verify_value=verify_value, merged_headers=merged_headers, ) with _lock: existing = _shared_clients.get(key) if existing is not None: _shared_clients.move_to_end(key) return existing client = httpx.Client( timeout=timeout, verify=verify_value, headers=merged_headers, ) _shared_clients[key] = client while len(_shared_clients) > _MAX_SHARED_CLIENTS: _, old_client = _shared_clients.popitem(last=False) try: old_client.close() except Exception: pass return client def close_shared_httpx_client() -> None: with _lock: clients = list(_shared_clients.values()) _shared_clients.clear() for client in clients: try: client.close() except Exception: pass atexit.register(close_shared_httpx_client)