""" client.py – Base HTTP client for CoinDesk API. This module provides the BaseClient class that handles HTTP requests to the CoinDesk API with proper authentication and error handling. """ import requests import json from typing import Dict, Any, Optional from urllib.parse import urljoin, urlencode import config class APIError(Exception): """Custom exception for API errors.""" def __init__(self, message: str, status_code: int = None, response: Any = None): self.message = message self.status_code = status_code self.response = response super().__init__(self.message) class BaseClient: """ Base HTTP client for CoinDesk API requests. Handles authentication, request formatting, and error handling. """ def __init__(self, base_url: str = None, headers: Dict[str, str] = None): """ Initialize the base client. Args: base_url: Base URL for the API (defaults to config.BASE_URL) headers: Default headers (defaults to config.HEADERS) """ self.base_url = base_url or config.BASE_URL self.headers = headers or config.HEADERS.copy() self.session = requests.Session() self.session.headers.update(self.headers) def _make_request(self, method: str, endpoint: str, params: Dict[str, Any] = None, data: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: """ Make an HTTP request to the API. Args: method: HTTP method (GET, POST, PUT, DELETE) endpoint: API endpoint path params: URL parameters data: Request body data **kwargs: Additional arguments for requests Returns: dict: JSON response from the API Raises: APIError: If the request fails or returns an error status """ # Construct full URL url = urljoin(self.base_url, endpoint.lstrip('/')) # Clean up parameters (remove None values) if params: params = {k: v for k, v in params.items() if v is not None} try: # Make the request response = self.session.request( method=method, url=url, params=params, json=data, **kwargs ) # Log the request for debugging print(f"[DEBUG] {method} {url}") if params: print(f"[DEBUG] Params: {params}") print(f"[DEBUG] Status: {response.status_code}") # Check if request was successful if response.status_code == 200: try: return response.json() except json.JSONDecodeError: # If response is not JSON, return the text return {"data": response.text, "status": "success"} else: # Handle different error status codes error_message = f"API request failed with status {response.status_code}" try: error_data = response.json() if 'error' in error_data: error_message = error_data['error'] elif 'message' in error_data: error_message = error_data['message'] except json.JSONDecodeError: error_message = f"{error_message}: {response.text}" raise APIError( message=error_message, status_code=response.status_code, response=response ) except requests.exceptions.RequestException as e: raise APIError(f"Request failed: {str(e)}") def get(self, endpoint: str, params: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: """ Make a GET request. Args: endpoint: API endpoint path params: URL parameters **kwargs: Additional arguments for requests Returns: dict: JSON response from the API """ return self._make_request('GET', endpoint, params=params, **kwargs) def post(self, endpoint: str, data: Dict[str, Any] = None, params: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: """ Make a POST request. Args: endpoint: API endpoint path data: Request body data params: URL parameters **kwargs: Additional arguments for requests Returns: dict: JSON response from the API """ return self._make_request('POST', endpoint, params=params, data=data, **kwargs) def put(self, endpoint: str, data: Dict[str, Any] = None, params: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: """ Make a PUT request. Args: endpoint: API endpoint path data: Request body data params: URL parameters **kwargs: Additional arguments for requests Returns: dict: JSON response from the API """ return self._make_request('PUT', endpoint, params=params, data=data, **kwargs) def delete(self, endpoint: str, params: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: """ Make a DELETE request. Args: endpoint: API endpoint path params: URL parameters **kwargs: Additional arguments for requests Returns: dict: JSON response from the API """ return self._make_request('DELETE', endpoint, params=params, **kwargs) def close(self): """Close the HTTP session.""" self.session.close() def __enter__(self): """Context manager entry.""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.close() # Convenience function to create a client instance def create_client(base_url: str = None, headers: Dict[str, str] = None) -> BaseClient: """ Create a new BaseClient instance. Args: base_url: Base URL for the API headers: Default headers Returns: BaseClient: Configured client instance """ return BaseClient(base_url=base_url, headers=headers) # Test function to verify the client works def test_client(): """Test the base client functionality.""" try: with create_client() as client: # Test a simple endpoint (you might need to adjust this based on your API) response = client.get("/index/cc/v1/markets") print("Client test successful!") print(f"Response keys: {list(response.keys()) if isinstance(response, dict) else 'Not a dict'}") return True except Exception as e: print(f"Client test failed: {e}") return False if __name__ == "__main__": test_client()