import httpx import logging import json from pydantic import BaseModel from cachetools import TTLCache from typing import Callable from tenacity import retry, stop_after_attempt, wait_exponential from api.config import HTTP_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_KEEPALIVE, CACHE_TTL, CACHE_MAX_SIZE logger = logging.getLogger(__name__) logger.handlers.clear() logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter( fmt='%(asctime)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S' )) logger.addHandler(handler) logger.propagate = False httpx_logger = logging.getLogger("httpx") httpx_logger.handlers.clear() httpx_logger.setLevel(logging.INFO) httpx_logger.addHandler(handler) httpx_logger.propagate = False _cache = TTLCache(maxsize=CACHE_MAX_SIZE, ttl=CACHE_TTL) _client = httpx.AsyncClient( timeout=httpx.Timeout(HTTP_TIMEOUT), limits=httpx.Limits(max_connections=HTTP_MAX_CONNECTIONS, max_keepalive_connections=HTTP_MAX_KEEPALIVE), ) def docstring_from_model(model: type[BaseModel]): def decorator(func): if func.__doc__: func.__doc__ = func.__doc__.format( params="\n".join( f"\t\t- {name}: {field.description or 'No description'}" for name, field in model.model_fields.items() ) ) return func return decorator _log_callback: Callable[[str], None] | None = None def set_log_callback(cb: Callable[[str], None] | None): global _log_callback _log_callback = cb def _log(msg: str): logger.info(msg) if _log_callback is not None: _log_callback(msg) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=5)) async def fetch_api_data(icon: str, url: str, params: dict, remove_keys: list = None) -> dict: try: cache_key = f"{url}:{sorted(params.items())}" if cache_key in _cache: _log(f"💾 {icon} Cache hit") return _cache[cache_key] _log(f"🔨 {icon} Input parameters:\n{json.dumps(params, ensure_ascii=False, indent=2)}") response = await _client.get(url, params=params) response.raise_for_status() _log(f"🖇️ {icon} Request URL: {response.url}") data = response.json() if remove_keys and isinstance(data, dict): for key in remove_keys: data.pop(key, None) _cache[cache_key] = data _log(f"✅ {icon} Success: {url}") return data except httpx.HTTPStatusError as e: _log(f"❌ {icon}HTTP error: {e.response.status_code} - {e.response.text}") return {"error": "http_error", "status_code": e.response.status_code} except httpx.RequestError as e: _log(f"❌ {icon}Request error: {str(e)}") return {"error": "request_error", "status_code": str(e)} except Exception as e: _log(f"❌ {icon}Unexpected error: {str(e)}") return {"error": "unexpected_error", "status_code": str(e)}