From d92b09f54f46a078ae9eb7332761f636ea761539 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 29 Dec 2024 13:29:22 +0100 Subject: [PATCH 1/6] Add error handling utilities --- hyperliquid/utils/error_handling.py | 128 ++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 hyperliquid/utils/error_handling.py diff --git a/hyperliquid/utils/error_handling.py b/hyperliquid/utils/error_handling.py new file mode 100644 index 00000000..1f4d6182 --- /dev/null +++ b/hyperliquid/utils/error_handling.py @@ -0,0 +1,128 @@ +"""Error handling utilities for hyperliquid-python-sdk.""" +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: + # Check if address is valid hex + 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 From 22eb1e416c82d3534a095e96fce460109f6908e6 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 29 Dec 2024 13:34:18 +0100 Subject: [PATCH 2/6] Complete error handling utilities implementation --- hyperliquid/utils/error_handling.py | 129 +--------------------------- 1 file changed, 1 insertion(+), 128 deletions(-) diff --git a/hyperliquid/utils/error_handling.py b/hyperliquid/utils/error_handling.py index 1f4d6182..3f6cc656 100644 --- a/hyperliquid/utils/error_handling.py +++ b/hyperliquid/utils/error_handling.py @@ -1,128 +1 @@ -"""Error handling utilities for hyperliquid-python-sdk.""" -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: - # Check if address is valid hex - 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 +YXQpOiBNYXhpbXVtIGRlbGF5IGJldHdlZW4gcmV0cmllcyBpbiBzZWNvbmRzLgogICAgICAgIGJhY2tvZmZfZmFjdG9yIChmbG9hdCk6IEZhY3RvciB0byBtdWx0aXBseSBkZWxheSBieSBhZnRlciBlYWNoIHJldHJ5LgogICAgICAgIGV4Y2VwdGlvbnMgKHR1cGxlKTogVHVwbGUgb2YgZXhjZXB0aW9ucyB0byByZXRyeSBvbi4KICAgICAgICAKICAgIFJldHVybnM6CiAgICAgICAgQ2FsbGFibGU6IERlY29yYXRlZCBmdW5jdGlvbiB0aGF0IHdpbGwgcmV0cnkgb24gZmFpbHVyZS4KICAgICIiIgogICAgZGVmIGRlY29yYXRvcihmdW5jOiBDYWxsYWJsZVsuLi4sIFRdKSAtPiBDYWxsYWJsZVsuLi4sIFRdOgogICAgICAgIEB3cmFwcyhmdW5jKQogICAgICAgIGRlZiB3cmFwcGVyKCphcmdzOiBBbnksICoqa3dhcmdzOiBBbnkpIC0+IFQ6CiAgICAgICAgICAgIGRlbGF5ID0gaW5pdGlhbF9kZWxheQogICAgICAgICAgICBsYXN0X2V4Y2VwdGlvbiA9IE5vbmUKICAgICAgICAgICAgCiAgICAgICAgICAgIGZvciByZXRyeSBpbiByYW5nZShtYXhfcmV0cmllcyArIDEpOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJldHVybiBmdW5jKCphcmdzLCAqKmt3YXJncykKICAgICAgICAgICAgICAgIGV4Y2VwdCBleGNlcHRpb25zIGFzIGU6CiAgICAgICAgICAgICAgICAgICAgbGFzdF9leGNlcHRpb24gPSBlCiAgICAgICAgICAgICAgICAgICAgaWYgcmV0cnkgPT0gbWF4X3JldHJpZXM6CiAgICAgICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgdGltZS5zbGVlcChkZWxheSkKICAgICAgICAgICAgICAgICAgICBkZWxheSA9IG1pbihkZWxheSAqIGJhY2tvZmZfZmFjdG9yLCBtYXhfZGVsYXkpCiAgICAgICAgICAgIAogICAgICAgICAgICByYWlzZSBsYXN0X2V4Y2VwdGlvbiAgIyB0eXBlOiBpZ25vcmUKICAgICAgICAgICAgCiAgICAgICAgcmV0dXJuIHdyYXBwZXIKICAgIHJldHVybiBkZWNvcmF0b3IKCmRlZiB2YWxpZGF0ZV9udW1lcmljX3BhcmFtKAogICAgdmFsdWU6IFVuaW9uW2ludCwgZmxvYXQsIHN0cl0sCiAgICBwYXJhbV9uYW1lOiBzdHIsCiAgICBtaW5fdmFsdWU6IE9wdGlvbmFsW1VuaW9uW2ludCwgZmxvYXRdXSA9IE5vbmUsCiAgICBtYXhfdmFsdWU6IE9wdGlvbmFsW1VuaW9uW2ludCwgZmxvYXRdXSA9IE5vbmUKKSAtPiBVbmlvbltpbnQsIGZsb2F0XToKICAgICIiIlZhbGlkYXRlIGEgbnVtZXJpYyBwYXJhbWV0ZXIuCiAgICAKICAgIEFyZ3M6CiAgICAgICAgdmFsdWU6IFRoZSB2YWx1ZSB0byB2YWxpZGF0ZS4KICAgICAgICBwYXJhbV9uYW1lOiBOYW1lIG9mIHRoZSBwYXJhbWV0ZXIgKGZvciBlcnJvciBtZXNzYWdlcykuCiAgICAgICAgbWluX3ZhbHVlOiBNaW5pbXVtIGFsbG93ZWQgdmFsdWUgKGluY2x1c2l2ZSkuCiAgICAgICAgbWF4X3ZhbHVlOiBNYXhpbXVtIGFsbG93ZWQgdmFsdWUgKGluY2x1c2l2ZSkuCiAgICAgICAgCiAgICBSZXR1cm5zOgogICAgICAgIFVuaW9uW2ludCwgZmxvYXRdOiBUaGUgdmFsaWRhdGVkIG51bWVyaWMgdmFsdWUuCiAgICAgICAgCiAgICBSYWlzZXM6CiAgICAgICAgSW52YWxpZFBhcmFtZXRlckVycm9yOiBJZiB2YWxpZGF0aW9uIGZhaWxzLgogICAgIiIiCiAgICB0cnk6CiAgICAgICAgaWYgaXNpbnN0YW5jZSh2YWx1ZSwgc3RyKToKICAgICAgICAgICAgIyBUcnkgdG8gY29udmVydCBzdHJpbmcgdG8gZmxvYXQvaW50CiAgICAgICAgICAgIGlmICcuJyBpbiB2YWx1ZToKICAgICAgICAgICAgICAgIG51bV92YWx1ZSA9IGZsb2F0KHZhbHVlKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgbnVtX3ZhbHVlID0gaW50KHZhbHVlKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIG51bV92YWx1ZSA9IHZhbHVlCiAgICBleGNlcHQgKFZhbHVlRXJyb3IsIFR5cGVFcnJvcik6CiAgICAgICAgcmFpc2UgSW52YWxpZFBhcmFtZXRlckVycm9yKGYiSW52YWxpZCB7cGFyYW1fbmFtZX06IG11c3QgYmUgYSB2YWxpZCBudW1iZXIiKQogICAgCiAgICBpZiBtaW5fdmFsdWUgaXMgbm90IE5vbmUgYW5kIG51bV92YWx1ZSA8IG1pbl92YWx1ZToKICAgICAgICByYWlzZSBJbnZhbGlkUGFyYW1ldGVyRXJyb3IoCiAgICAgICAgICAgIGYiSW52YWxpZCB7cGFyYW1fbmFtZX06IHtudW1fdmFsdWV9IGlzIGxlc3MgdGhhbiBtaW5pbXVtIGFsbG93ZWQgdmFsdWUgb2Yge21pbl92YWx1ZX0iCiAgICAgICAgKQogICAgCiAgICBpZiBtYXhfdmFsdWUgaXMgbm90IE5vbmUgYW5kIG51bV92YWx1ZSA+IG1heF92YWx1ZToKICAgICAgICByYWlzZSBJbnZhbGlkUGFyYW1ldGVyRXJyb3IoCiAgICAgICAgICAgIGYiSW52YWxpZCB7cGFyYW1fbmFtZX06IHtudW1fdmFsdWV9IGlzIGdyZWF0ZXIgdGhhbiBtYXhpbXVtIGFsbG93ZWQgdmFsdWUgb2Yge21heF92YWx1ZX0iCiAgICAgICAgKQogICAgCiAgICByZXR1cm4gbnVtX3ZhbHVl \ No newline at end of file From fd51852f749c7cbc8911095c46412f7f371c6560 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 29 Dec 2024 13:35:11 +0100 Subject: [PATCH 3/6] Add base error handling classes and validation functions --- hyperliquid/utils/error_handling.py | 42 ++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/hyperliquid/utils/error_handling.py b/hyperliquid/utils/error_handling.py index 3f6cc656..91d387ac 100644 --- a/hyperliquid/utils/error_handling.py +++ b/hyperliquid/utils/error_handling.py @@ -1 +1,41 @@ -YXQpOiBNYXhpbXVtIGRlbGF5IGJldHdlZW4gcmV0cmllcyBpbiBzZWNvbmRzLgogICAgICAgIGJhY2tvZmZfZmFjdG9yIChmbG9hdCk6IEZhY3RvciB0byBtdWx0aXBseSBkZWxheSBieSBhZnRlciBlYWNoIHJldHJ5LgogICAgICAgIGV4Y2VwdGlvbnMgKHR1cGxlKTogVHVwbGUgb2YgZXhjZXB0aW9ucyB0byByZXRyeSBvbi4KICAgICAgICAKICAgIFJldHVybnM6CiAgICAgICAgQ2FsbGFibGU6IERlY29yYXRlZCBmdW5jdGlvbiB0aGF0IHdpbGwgcmV0cnkgb24gZmFpbHVyZS4KICAgICIiIgogICAgZGVmIGRlY29yYXRvcihmdW5jOiBDYWxsYWJsZVsuLi4sIFRdKSAtPiBDYWxsYWJsZVsuLi4sIFRdOgogICAgICAgIEB3cmFwcyhmdW5jKQogICAgICAgIGRlZiB3cmFwcGVyKCphcmdzOiBBbnksICoqa3dhcmdzOiBBbnkpIC0+IFQ6CiAgICAgICAgICAgIGRlbGF5ID0gaW5pdGlhbF9kZWxheQogICAgICAgICAgICBsYXN0X2V4Y2VwdGlvbiA9IE5vbmUKICAgICAgICAgICAgCiAgICAgICAgICAgIGZvciByZXRyeSBpbiByYW5nZShtYXhfcmV0cmllcyArIDEpOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJldHVybiBmdW5jKCphcmdzLCAqKmt3YXJncykKICAgICAgICAgICAgICAgIGV4Y2VwdCBleGNlcHRpb25zIGFzIGU6CiAgICAgICAgICAgICAgICAgICAgbGFzdF9leGNlcHRpb24gPSBlCiAgICAgICAgICAgICAgICAgICAgaWYgcmV0cnkgPT0gbWF4X3JldHJpZXM6CiAgICAgICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgdGltZS5zbGVlcChkZWxheSkKICAgICAgICAgICAgICAgICAgICBkZWxheSA9IG1pbihkZWxheSAqIGJhY2tvZmZfZmFjdG9yLCBtYXhfZGVsYXkpCiAgICAgICAgICAgIAogICAgICAgICAgICByYWlzZSBsYXN0X2V4Y2VwdGlvbiAgIyB0eXBlOiBpZ25vcmUKICAgICAgICAgICAgCiAgICAgICAgcmV0dXJuIHdyYXBwZXIKICAgIHJldHVybiBkZWNvcmF0b3IKCmRlZiB2YWxpZGF0ZV9udW1lcmljX3BhcmFtKAogICAgdmFsdWU6IFVuaW9uW2ludCwgZmxvYXQsIHN0cl0sCiAgICBwYXJhbV9uYW1lOiBzdHIsCiAgICBtaW5fdmFsdWU6IE9wdGlvbmFsW1VuaW9uW2ludCwgZmxvYXRdXSA9IE5vbmUsCiAgICBtYXhfdmFsdWU6IE9wdGlvbmFsW1VuaW9uW2ludCwgZmxvYXRdXSA9IE5vbmUKKSAtPiBVbmlvbltpbnQsIGZsb2F0XToKICAgICIiIlZhbGlkYXRlIGEgbnVtZXJpYyBwYXJhbWV0ZXIuCiAgICAKICAgIEFyZ3M6CiAgICAgICAgdmFsdWU6IFRoZSB2YWx1ZSB0byB2YWxpZGF0ZS4KICAgICAgICBwYXJhbV9uYW1lOiBOYW1lIG9mIHRoZSBwYXJhbWV0ZXIgKGZvciBlcnJvciBtZXNzYWdlcykuCiAgICAgICAgbWluX3ZhbHVlOiBNaW5pbXVtIGFsbG93ZWQgdmFsdWUgKGluY2x1c2l2ZSkuCiAgICAgICAgbWF4X3ZhbHVlOiBNYXhpbXVtIGFsbG93ZWQgdmFsdWUgKGluY2x1c2l2ZSkuCiAgICAgICAgCiAgICBSZXR1cm5zOgogICAgICAgIFVuaW9uW2ludCwgZmxvYXRdOiBUaGUgdmFsaWRhdGVkIG51bWVyaWMgdmFsdWUuCiAgICAgICAgCiAgICBSYWlzZXM6CiAgICAgICAgSW52YWxpZFBhcmFtZXRlckVycm9yOiBJZiB2YWxpZGF0aW9uIGZhaWxzLgogICAgIiIiCiAgICB0cnk6CiAgICAgICAgaWYgaXNpbnN0YW5jZSh2YWx1ZSwgc3RyKToKICAgICAgICAgICAgIyBUcnkgdG8gY29udmVydCBzdHJpbmcgdG8gZmxvYXQvaW50CiAgICAgICAgICAgIGlmICcuJyBpbiB2YWx1ZToKICAgICAgICAgICAgICAgIG51bV92YWx1ZSA9IGZsb2F0KHZhbHVlKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgbnVtX3ZhbHVlID0gaW50KHZhbHVlKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIG51bV92YWx1ZSA9IHZhbHVlCiAgICBleGNlcHQgKFZhbHVlRXJyb3IsIFR5cGVFcnJvcik6CiAgICAgICAgcmFpc2UgSW52YWxpZFBhcmFtZXRlckVycm9yKGYiSW52YWxpZCB7cGFyYW1fbmFtZX06IG11c3QgYmUgYSB2YWxpZCBudW1iZXIiKQogICAgCiAgICBpZiBtaW5fdmFsdWUgaXMgbm90IE5vbmUgYW5kIG51bV92YWx1ZSA8IG1pbl92YWx1ZToKICAgICAgICByYWlzZSBJbnZhbGlkUGFyYW1ldGVyRXJyb3IoCiAgICAgICAgICAgIGYiSW52YWxpZCB7cGFyYW1fbmFtZX06IHtudW1fdmFsdWV9IGlzIGxlc3MgdGhhbiBtaW5pbXVtIGFsbG93ZWQgdmFsdWUgb2Yge21pbl92YWx1ZX0iCiAgICAgICAgKQogICAgCiAgICBpZiBtYXhfdmFsdWUgaXMgbm90IE5vbmUgYW5kIG51bV92YWx1ZSA+IG1heF92YWx1ZToKICAgICAgICByYWlzZSBJbnZhbGlkUGFyYW1ldGVyRXJyb3IoCiAgICAgICAgICAgIGYiSW52YWxpZCB7cGFyYW1fbmFtZX06IHtudW1fdmFsdWV9IGlzIGdyZWF0ZXIgdGhhbiBtYXhpbXVtIGFsbG93ZWQgdmFsdWUgb2Yge21heF92YWx1ZX0iCiAgICAgICAgKQogICAgCiAgICByZXR1cm4gbnVtX3ZhbHVl \ No newline at end of file +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 \ No newline at end of file From 64048d9e49987f2c11df31812feedc404d3f61aa Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 29 Dec 2024 13:35:41 +0100 Subject: [PATCH 4/6] Add error handling utilities --- hyperliquid/utils/__init__.py | 17 ++++++ hyperliquid/utils/error_handling.py | 87 ++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) 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 index 91d387ac..3a4d6aca 100644 --- a/hyperliquid/utils/error_handling.py +++ b/hyperliquid/utils/error_handling.py @@ -38,4 +38,89 @@ def validate_address(address: str) -> bool: int(address[2:], 16) return True except ValueError: - return False \ No newline at end of file + 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 From 401892440325bfcdf31eaf2a52b66e02c0f3a974 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 29 Dec 2024 13:35:50 +0100 Subject: [PATCH 5/6] Update API class with enhanced error handling --- hyperliquid/api.py | 65 +++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/hyperliquid/api.py b/hyperliquid/api.py index 876244bf..7702e1be 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)}") From b38f8f89e715ceb16e807d73cd29346b6f0fb824 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 29 Dec 2024 13:35:58 +0100 Subject: [PATCH 6/6] Update API implementation with error handling --- hyperliquid/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyperliquid/api.py b/hyperliquid/api.py index 7702e1be..69a262ff 100644 --- a/hyperliquid/api.py +++ b/hyperliquid/api.py @@ -38,4 +38,4 @@ def post(self, endpoint: str, payload: Dict[str, Any]) -> Any: response=getattr(e.response, 'text', None) ) except json.JSONDecodeError as e: - raise APIError(f"Failed to decode API response: {str(e)}") + raise APIError(f"Failed to decode API response: {str(e)}") \ No newline at end of file