"""Client HTTP partagé (A42 — reliquat du audit). 51 créations `httpx.AsyncClient(...)` éparpillées dans 15 fichiers = un nouveau pool de connexions par appel (pas de keep-alive). Ici le pool est réutilisé, **closé par boucle d'event** : un `AsyncClient` ne traverse pas de boucle à boucle (les tests en créent une par test → client propre par boucle, collecté avec elle). `async with shared_client(timeout=15) as client:` — même syntaxe que before, l'`__aexit__` est un no-op (on ne ferme pas le client partagé). ponytail: pas d'`aclose` explicite — le client meurt avec sa boucle (WeakKeyDictionary, les sockets sont fermés par le GC) ; plafond : le pool du loop produit n'est pas fermé à la main. Upgrade si besoin : lifespan qui ferme le client du loop principal. """ from __future__ import annotations import asyncio import weakref import httpx _clients: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, dict] = ( weakref.WeakKeyDictionary() ) def _hashable(v) -> bool: try: hash(v) return True except TypeError: return False def _key(kwargs: dict) -> tuple: """Clé de cache = kwargs (timeout/auth/transport/headers…). Valeurs non hashables (dict `headers=`…) → repr, même contrat que la clé.""" return tuple( (k, v if _hashable(v) else repr(v)) for k, v in sorted(kwargs.items()) ) def get_shared_client(**kwargs) -> httpx.AsyncClient: loop = asyncio.get_running_loop() per_loop = _clients.setdefault(loop, {}) k = _key(kwargs) client = per_loop.get(k) if client is None: client = httpx.AsyncClient(**kwargs) per_loop[k] = client return client class shared_client: """Context manager async : yield du client partagé, no-op à la sortie.""" def __init__(self, **kwargs): self._kwargs = kwargs async def __aenter__(self) -> httpx.AsyncClient: return get_shared_client(**self._kwargs) async def __aexit__(self, *exc) -> bool: return False