Skip to main content
Glama
by frap129
base.py4.45 kB
"""Base HTTP client with retry logic and error handling. This module provides HTTP communication only. Caching is handled by the repository layer using the cache-aside pattern. **BREAKING CHANGE (Task 2.10)**: Removed entity cache parameters from make_request(). Use repository layer for caching instead. """ import asyncio import logging from typing import Any import httpx from lorekeeper_mcp.api_clients.exceptions import ApiError, NetworkError logger = logging.getLogger(__name__) # HTTP status code threshold for error responses HTTP_ERROR_STATUS_CODE = 400 class BaseHttpClient: """Base HTTP client providing common functionality for API requests.""" def __init__( self, base_url: str, timeout: float = 30.0, max_retries: int = 5, source_api: str = "api_client", ) -> None: """Initialize the base HTTP client. Args: base_url: Base URL for API requests timeout: Request timeout in seconds max_retries: Maximum number of retry attempts source_api: Source API identifier (for logging/tracking) """ self.base_url = base_url.rstrip("/") self.timeout = timeout self.max_retries = max_retries self.source_api = source_api self._client: httpx.AsyncClient | None = None async def _get_client(self) -> httpx.AsyncClient: """Get or create async HTTP client.""" if self._client is None: self._client = httpx.AsyncClient( timeout=self.timeout, headers={"User-Agent": "LoreKeeper-MCP/0.1.0"}, ) return self._client async def make_request( self, endpoint: str, method: str = "GET", **kwargs: Any, ) -> dict[str, Any] | list[dict[str, Any]]: """Make HTTP request with retry logic. Caching is NOT handled here. Use the repository layer for caching via the cache-aside pattern. Args: endpoint: API endpoint path method: HTTP method **kwargs: Additional arguments for httpx request (params, json, etc.) Returns: Parsed JSON response (dict or list of dicts) Raises: NetworkError: For network-related failures ApiError: For API error responses (4xx/5xx) """ url = f"{self.base_url}{endpoint}" retries = self.max_retries client = await self._get_client() for attempt in range(retries + 1): try: response = await client.request(method, url, **kwargs) if response.status_code >= HTTP_ERROR_STATUS_CODE: # Handle 400 validation errors gracefully if response.status_code == HTTP_ERROR_STATUS_CODE: try: error_data = response.json() # Log the validation error for debugging logger.warning(f"API validation error for {endpoint}: {error_data}") except Exception: # If JSON parsing fails, just log the status logger.warning( f"API validation error for {endpoint}: HTTP {HTTP_ERROR_STATUS_CODE}" ) # Return empty result set for filter validation errors return {"results": [], "count": 0} raise ApiError( f"API error: {response.status_code}", status_code=response.status_code, ) result: dict[str, Any] | list[dict[str, Any]] = response.json() return result except httpx.TimeoutException as e: if attempt == retries: raise NetworkError(str(e)) from e await asyncio.sleep(2**attempt) # Exponential backoff except httpx.RequestError as e: if attempt == retries: raise NetworkError(str(e)) from e await asyncio.sleep(2**attempt) # Should not reach here raise NetworkError("Max retries exceeded") async def close(self) -> None: """Close the HTTP client.""" if self._client: await self._client.aclose() self._client = None

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/frap129/lorekeeper-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server