"""Shared aiohttp session with connection pooling and exponential-backoff retry.""" import asyncio from typing import Optional, Dict, Any import aiohttp from core.logger import logger _session: Optional[aiohttp.ClientSession] = None def _new_session() -> aiohttp.ClientSession: connector = aiohttp.TCPConnector(limit=100, limit_per_host=20, keepalive_timeout=30) return aiohttp.ClientSession( connector=connector, timeout=aiohttp.ClientTimeout(total=30), ) async def get_session() -> aiohttp.ClientSession: global _session if _session is None or _session.closed: _session = _new_session() return _session async def close_session(): global _session if _session and not _session.closed: await _session.close() _session = None async def api_get(url: str, **kwargs) -> Optional[Dict[str, Any]]: return await _request("get", url, **kwargs) async def api_post(url: str, **kwargs) -> Optional[Dict[str, Any]]: return await _request("post", url, **kwargs) async def api_patch(url: str, **kwargs) -> Optional[Dict[str, Any]]: return await _request("patch", url, **kwargs) async def _request(method: str, url: str, **kwargs) -> Optional[Dict[str, Any]]: """Execute an HTTP request with up to 3 attempts and exponential backoff.""" max_retries = 3 backoff = 1.0 session = await get_session() for attempt in range(max_retries): try: async with getattr(session, method)(url, **kwargs) as resp: if resp.content_type == "application/json": return await resp.json() return {"status_code": resp.status} except aiohttp.ClientError as exc: if attempt < max_retries - 1: logger.warning( f"{method.upper()} {url} attempt {attempt + 1} failed: {exc} " f"— retrying in {backoff:.0f}s" ) await asyncio.sleep(backoff) backoff *= 2 else: logger.error(f"{method.upper()} {url} failed after {max_retries} attempts: {exc}") except Exception as exc: logger.error(f"{method.upper()} {url} unexpected error: {exc}") break return None