import httpx from cachetools import TTLCache from tenacity import retry, stop_after_attempt, wait_exponential import urllib.parse from backend.logger import setup_logger from configs import ( JUSTICE_API_BASE, HTTP_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_KEEPALIVE, CACHE_TTL, CACHE_MAX_SIZE, RETRY_ATTEMPTS, RETRY_MAX_WAIT, RETRY_MIN_WAIT, RETRY_MULTIPLIER, ) WAIT_EXPONENTIAL = wait_exponential( multiplier=RETRY_MULTIPLIER, min=RETRY_MIN_WAIT, max=RETRY_MAX_WAIT ) STOP_AFTER_ATTEMPT = stop_after_attempt(RETRY_ATTEMPTS) CACHE = TTLCache(maxsize=CACHE_MAX_SIZE, ttl=CACHE_TTL) TIMEOUT = httpx.Timeout(HTTP_TIMEOUT) LIMITS = httpx.Limits( max_connections=HTTP_MAX_CONNECTIONS, max_keepalive_connections=HTTP_MAX_KEEPALIVE ) logger = setup_logger(__name__) def get_client() -> httpx.AsyncClient: return httpx.AsyncClient(timeout=TIMEOUT, limits=LIMITS) @retry(stop=STOP_AFTER_ATTEMPT, wait=WAIT_EXPONENTIAL) async def http_request(route: str, params: dict | None, remove_keys: list[str] | None = None) -> dict: """Sends an HTTP GET request and returns the parsed JSON response.""" url = f"{JUSTICE_API_BASE}{route}" params_tuple = tuple(sorted(params.items())) if params else () cache_key = f"{url}:{params_tuple}" if cache_key in CACHE: return CACHE[cache_key] returned_url = url try: logger.info(f"[HTTP REQUEST] | {url} | params={params}") async with get_client() as client: if params: query_string = urllib.parse.urlencode( params, doseq=True, quote_via=urllib.parse.quote ) final_url = f"{url}?{query_string}" response = await client.get(final_url) else: response = await client.get(url) response.raise_for_status() logger.info(f"[HTTP SUCCESS] | {url} | status={response.status_code}") returned_url = str(response.url) data = response.json() if remove_keys and isinstance(data, dict): for key in remove_keys: data.pop(key, None) result = {"url": returned_url, "data": data} CACHE[cache_key] = result return result except httpx.HTTPError as e: logger.error(f"[HTTP ERROR] | {returned_url} | {str(e)}") return {"error": "http_error", "detail": str(e)} except Exception as e: logger.error(f"[UNEXPECTED ERROR] | {returned_url} | {str(e)}") return {"error": "unexpected_error", "detail": str(e)}