diff --git a/hyperliquid/api.py b/hyperliquid/api.py index 876244bf..69a262ff 100644 --- a/hyperliquid/api.py +++ b/hyperliquid/api.py @@ -1,42 +1,41 @@ import json -import logging -from json import JSONDecodeError - import requests +from typing import Any, Dict, Optional -from hyperliquid.utils.constants import MAINNET_API_URL -from hyperliquid.utils.error import ClientError, ServerError -from hyperliquid.utils.types import Any - +from .utils.error_handling import APIError, retry_on_failure class API: - def __init__(self, base_url=None): - self.base_url = base_url or MAINNET_API_URL + def __init__(self, base_url: Optional[str] = None): + self.base_url = base_url or "https://api.hyperliquid.xyz" self.session = requests.Session() - self.session.headers.update({"Content-Type": "application/json"}) - self._logger = logging.getLogger(__name__) - def post(self, url_path: str, payload: Any = None) -> Any: - payload = payload or {} - url = self.base_url + url_path - response = self.session.post(url, json=payload) - self._handle_exception(response) + @retry_on_failure(max_retries=3) + def post(self, endpoint: str, payload: Dict[str, Any]) -> Any: + """Make a POST request to the API with retry mechanism. + + Args: + endpoint: API endpoint + payload: Request payload + + Returns: + API response + + Raises: + APIError: If the request fails + """ try: + response = self.session.post( + f"{self.base_url}{endpoint}", + json=payload, + headers={"Content-Type": "application/json"} + ) + response.raise_for_status() return response.json() - except ValueError: - return {"error": f"Could not parse JSON: {response.text}"} - - def _handle_exception(self, response): - status_code = response.status_code - if status_code < 400: - return - if 400 <= status_code < 500: - try: - err = json.loads(response.text) - except JSONDecodeError: - raise ClientError(status_code, None, response.text, None, response.headers) - if err is None: - raise ClientError(status_code, None, response.text, None, response.headers) - error_data = err.get("data") - raise ClientError(status_code, err["code"], err["msg"], response.headers, error_data) - raise ServerError(status_code, response.text) + except requests.exceptions.RequestException as e: + raise APIError( + f"API request failed: {str(e)}", + status_code=getattr(e.response, 'status_code', None), + response=getattr(e.response, 'text', None) + ) + except json.JSONDecodeError as e: + raise APIError(f"Failed to decode API response: {str(e)}") \ No newline at end of file diff --git a/hyperliquid/utils/__init__.py b/hyperliquid/utils/__init__.py index e69de29b..f676a5ac 100644 --- a/hyperliquid/utils/__init__.py +++ b/hyperliquid/utils/__init__.py @@ -0,0 +1,17 @@ +from .error_handling import ( + HyperliquidError, + InvalidParameterError, + APIError, + validate_address, + retry_on_failure, + validate_numeric_param +) + +__all__ = [ + 'HyperliquidError', + 'InvalidParameterError', + 'APIError', + 'validate_address', + 'retry_on_failure', + 'validate_numeric_param' +] \ No newline at end of file diff --git a/hyperliquid/utils/error_handling.py b/hyperliquid/utils/error_handling.py new file mode 100644 index 00000000..3a4d6aca --- /dev/null +++ b/hyperliquid/utils/error_handling.py @@ -0,0 +1,126 @@ +from typing import Any, Callable, Optional, TypeVar, Union +import time +from functools import wraps + +T = TypeVar('T') + +class HyperliquidError(Exception): + """Base exception class for Hyperliquid SDK.""" + pass + +class InvalidParameterError(HyperliquidError): + """Raised when an invalid parameter is provided.""" + pass + +class APIError(HyperliquidError): + """Raised when an API call fails.""" + def __init__(self, message: str, status_code: Optional[int] = None, response: Any = None): + super().__init__(message) + self.status_code = status_code + self.response = response + +def validate_address(address: str) -> bool: + """Validate an Ethereum address. + + Args: + address (str): The address to validate. + + Returns: + bool: True if the address is valid, False otherwise. + """ + if not isinstance(address, str): + return False + if not address.startswith('0x'): + return False + if len(address) != 42: + return False + try: + int(address[2:], 16) + return True + except ValueError: + return False + +def retry_on_failure( + max_retries: int = 3, + initial_delay: float = 1.0, + max_delay: float = 10.0, + backoff_factor: float = 2.0, + exceptions: tuple = (APIError,) +) -> Callable: + """Decorator to retry functions on failure with exponential backoff. + + Args: + max_retries (int): Maximum number of retries before giving up. + initial_delay (float): Initial delay between retries in seconds. + max_delay (float): Maximum delay between retries in seconds. + backoff_factor (float): Factor to multiply delay by after each retry. + exceptions (tuple): Tuple of exceptions to retry on. + + Returns: + Callable: Decorated function that will retry on failure. + """ + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + delay = initial_delay + last_exception = None + + for retry in range(max_retries + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + last_exception = e + if retry == max_retries: + break + + time.sleep(delay) + delay = min(delay * backoff_factor, max_delay) + + raise last_exception # type: ignore + + return wrapper + return decorator + +def validate_numeric_param( + value: Union[int, float, str], + param_name: str, + min_value: Optional[Union[int, float]] = None, + max_value: Optional[Union[int, float]] = None +) -> Union[int, float]: + """Validate a numeric parameter. + + Args: + value: The value to validate. + param_name: Name of the parameter (for error messages). + min_value: Minimum allowed value (inclusive). + max_value: Maximum allowed value (inclusive). + + Returns: + Union[int, float]: The validated numeric value. + + Raises: + InvalidParameterError: If validation fails. + """ + try: + if isinstance(value, str): + # Try to convert string to float/int + if '.' in value: + num_value = float(value) + else: + num_value = int(value) + else: + num_value = value + except (ValueError, TypeError): + raise InvalidParameterError(f"Invalid {param_name}: must be a valid number") + + if min_value is not None and num_value < min_value: + raise InvalidParameterError( + f"Invalid {param_name}: {num_value} is less than minimum allowed value of {min_value}" + ) + + if max_value is not None and num_value > max_value: + raise InvalidParameterError( + f"Invalid {param_name}: {num_value} is greater than maximum allowed value of {max_value}" + ) + + return num_value \ No newline at end of file