d
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
"""Shared `httpx.Client` helper.
|
||||
|
||||
Creating short-lived httpx clients disables connection pooling and costs extra CPU.
|
||||
This module provides a small singleton client for callers that just need basic
|
||||
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 Dict, Optional
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -20,39 +22,85 @@ _DEFAULT_USER_AGENT = (
|
||||
)
|
||||
|
||||
_lock = threading.Lock()
|
||||
_shared_client: Optional[httpx.Client] = None
|
||||
_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 = True,
|
||||
verify_ssl: bool | str = True,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> httpx.Client:
|
||||
"""Return a process-wide shared synchronous httpx.Client."""
|
||||
"""Return a shared synchronous httpx.Client for a specific config key."""
|
||||
|
||||
global _shared_client
|
||||
if _shared_client is None:
|
||||
with _lock:
|
||||
if _shared_client is None:
|
||||
base_headers = {"User-Agent": _DEFAULT_USER_AGENT}
|
||||
if headers:
|
||||
base_headers.update({str(k): str(v) for k, v in headers.items()})
|
||||
_shared_client = httpx.Client(
|
||||
timeout=timeout,
|
||||
verify=resolve_verify_value(verify_ssl),
|
||||
headers=base_headers,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
return _shared_client
|
||||
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:
|
||||
global _shared_client
|
||||
client = _shared_client
|
||||
_shared_client = None
|
||||
if client is not 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)
|
||||
|
||||
Reference in New Issue
Block a user