From 5fff06e96963a4ff1e3f8a48aefad3658233c8f6 Mon Sep 17 00:00:00 2001 From: mobias17 Date: Sun, 25 Jan 2026 19:33:44 +0100 Subject: [PATCH 1/4] fix candidate #478, #477, #432, #405 --- .../rest_api/models/asset_filters.py | 35 +- .../rest_api/models/exchange_filters.py | 40 +- .../rest_api/models/symbol_filters.py | 52 +-- .../websocket_api/models/asset_filters.py | 51 +-- .../websocket_api/models/exchange_filters.py | 83 ++-- .../websocket_api/models/symbol_filters.py | 167 ++----- .../user_data_stream_events_response.py | 93 ++-- .../tests/unit/rest_api/test_general_api.py | 408 ++++++++++++++++++ .../websocket_api/test_general_api_ws_api.py | 408 ++++++++++++++++++ .../test_user_data_stream_api_ws_api.py | 252 +++++++++++ 10 files changed, 1210 insertions(+), 379 deletions(-) diff --git a/clients/spot/src/binance_sdk_spot/rest_api/models/asset_filters.py b/clients/spot/src/binance_sdk_spot/rest_api/models/asset_filters.py index b97c95cd..de34f01d 100644 --- a/clients/spot/src/binance_sdk_spot/rest_api/models/asset_filters.py +++ b/clients/spot/src/binance_sdk_spot/rest_api/models/asset_filters.py @@ -67,6 +67,11 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" @@ -85,33 +90,9 @@ def from_dict(cls, parsed) -> Self: instance.actual_instance = target_cls.from_dict(parsed) return instance - instance = cls.model_construct() - error_messages = [] - match = 0 - is_list = isinstance(parsed, list) - - for subcls in ["MaxAssetFilter"]: - if is_list == subcls.is_array(): - try: - instance.actual_instance = subcls.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into AssetFilters with oneOf schemas: MaxAssetFilter. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into AssetFilters with oneOf schemas: MaxAssetFilter. Details: " - + ", ".join(error_messages) - ) - else: - return instance + raise ValueError( + f"Unable to deserialize into AssetFilters: 'filterType' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_filters.py b/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_filters.py index 4d6189a4..67bbf423 100644 --- a/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_filters.py +++ b/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_filters.py @@ -101,6 +101,11 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" @@ -124,38 +129,9 @@ def from_dict(cls, parsed) -> Self: instance.actual_instance = target_cls.from_dict(parsed) return instance - instance = cls.model_construct() - error_messages = [] - match = 0 - is_list = isinstance(parsed, list) - - for subcls in [ - "ExchangeMaxNumAlgoOrdersFilter", - "ExchangeMaxNumIcebergOrdersFilter", - "ExchangeMaxNumOrderListsFilter", - "ExchangeMaxNumOrdersFilter", - ]: - if is_list == subcls.is_array(): - try: - instance.actual_instance = subcls.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into ExchangeFilters with oneOf schemas: ExchangeMaxNumAlgoOrdersFilter, ExchangeMaxNumIcebergOrdersFilter, ExchangeMaxNumOrderListsFilter, ExchangeMaxNumOrdersFilter. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into ExchangeFilters with oneOf schemas: ExchangeMaxNumAlgoOrdersFilter, ExchangeMaxNumIcebergOrdersFilter, ExchangeMaxNumOrderListsFilter, ExchangeMaxNumOrdersFilter. Details: " - + ", ".join(error_messages) - ) - else: - return instance + raise ValueError( + f"Unable to deserialize into ExchangeFilters: 'filterType' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/src/binance_sdk_spot/rest_api/models/symbol_filters.py b/clients/spot/src/binance_sdk_spot/rest_api/models/symbol_filters.py index e5da27c2..a7628aa2 100644 --- a/clients/spot/src/binance_sdk_spot/rest_api/models/symbol_filters.py +++ b/clients/spot/src/binance_sdk_spot/rest_api/models/symbol_filters.py @@ -175,6 +175,11 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" @@ -210,50 +215,9 @@ def from_dict(cls, parsed) -> Self: instance.actual_instance = target_cls.from_dict(parsed) return instance - instance = cls.model_construct() - error_messages = [] - match = 0 - is_list = isinstance(parsed, list) - - for subcls in [ - "IcebergPartsFilter", - "LotSizeFilter", - "MarketLotSizeFilter", - "MaxNumAlgoOrdersFilter", - "MaxNumIcebergOrdersFilter", - "MaxNumOrderAmendsFilter", - "MaxNumOrderListsFilter", - "MaxNumOrdersFilter", - "MaxPositionFilter", - "MinNotionalFilter", - "NotionalFilter", - "PercentPriceBySideFilter", - "PercentPriceFilter", - "PriceFilter", - "TPlusSellFilter", - "TrailingDeltaFilter", - ]: - if is_list == subcls.is_array(): - try: - instance.actual_instance = subcls.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into SymbolFilters with oneOf schemas: IcebergPartsFilter, LotSizeFilter, MarketLotSizeFilter, MaxNumAlgoOrdersFilter, MaxNumIcebergOrdersFilter, MaxNumOrderAmendsFilter, MaxNumOrderListsFilter, MaxNumOrdersFilter, MaxPositionFilter, MinNotionalFilter, NotionalFilter, PercentPriceBySideFilter, PercentPriceFilter, PriceFilter, TPlusSellFilter, TrailingDeltaFilter. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into SymbolFilters with oneOf schemas: IcebergPartsFilter, LotSizeFilter, MarketLotSizeFilter, MaxNumAlgoOrdersFilter, MaxNumIcebergOrdersFilter, MaxNumOrderAmendsFilter, MaxNumOrderListsFilter, MaxNumOrdersFilter, MaxPositionFilter, MinNotionalFilter, NotionalFilter, PercentPriceBySideFilter, PercentPriceFilter, PriceFilter, TPlusSellFilter, TrailingDeltaFilter. Details: " - + ", ".join(error_messages) - ) - else: - return instance + raise ValueError( + f"Unable to deserialize into SymbolFilters: 'filterType' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/asset_filters.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/asset_filters.py index 705ef8ac..b51c01f7 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/asset_filters.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/asset_filters.py @@ -67,37 +67,32 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" - instance = cls.model_construct() - error_messages = [] - match = 0 - - is_list = isinstance(parsed, list) - - # deserialize data into MaxAssetFilter - if is_list == MaxAssetFilter.is_array(): - try: - instance.actual_instance = MaxAssetFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into AssetFilters with oneOf schemas: MaxAssetFilter. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into AssetFilters with oneOf schemas: MaxAssetFilter. Details: " - + ", ".join(error_messages) - ) - else: - return instance + if parsed is None: + return None + + if isinstance(parsed, dict) and "filterType" in parsed: + filter_type_map = {"MAX_ASSET": MaxAssetFilter} + + ft = parsed.get("filterType") + target_cls = filter_type_map.get(ft) + + if target_cls is not None: + # Deserialize directly into the proper schema + instance = cls.model_construct() + instance.actual_instance = target_cls.from_dict(parsed) + return instance + + raise ValueError( + f"Unable to deserialize into AssetFilters: 'filterType' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_filters.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_filters.py index b3065da1..2f5c5115 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_filters.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_filters.py @@ -101,64 +101,37 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" - instance = cls.model_construct() - error_messages = [] - match = 0 - - is_list = isinstance(parsed, list) - - # deserialize data into ExchangeMaxNumOrdersFilter - if is_list == ExchangeMaxNumOrdersFilter.is_array(): - try: - instance.actual_instance = ExchangeMaxNumOrdersFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into ExchangeMaxNumAlgoOrdersFilter - if is_list == ExchangeMaxNumAlgoOrdersFilter.is_array(): - try: - instance.actual_instance = ExchangeMaxNumAlgoOrdersFilter.from_dict( - parsed - ) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into ExchangeMaxNumIcebergOrdersFilter - if is_list == ExchangeMaxNumIcebergOrdersFilter.is_array(): - try: - instance.actual_instance = ExchangeMaxNumIcebergOrdersFilter.from_dict( - parsed - ) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into ExchangeMaxNumOrderListsFilter - if is_list == ExchangeMaxNumOrderListsFilter.is_array(): - try: - instance.actual_instance = ExchangeMaxNumOrderListsFilter.from_dict( - parsed - ) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into ExchangeFilters with oneOf schemas: ExchangeMaxNumAlgoOrdersFilter, ExchangeMaxNumIcebergOrdersFilter, ExchangeMaxNumOrderListsFilter, ExchangeMaxNumOrdersFilter. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into ExchangeFilters with oneOf schemas: ExchangeMaxNumAlgoOrdersFilter, ExchangeMaxNumIcebergOrdersFilter, ExchangeMaxNumOrderListsFilter, ExchangeMaxNumOrdersFilter. Details: " - + ", ".join(error_messages) - ) - else: - return instance + if parsed is None: + return None + + if isinstance(parsed, dict) and "filterType" in parsed: + filter_type_map = { + "EXCHANGE_MAX_NUM_ORDERS": ExchangeMaxNumOrdersFilter, + "EXCHANGE_MAX_NUM_ALGO_ORDERS": ExchangeMaxNumAlgoOrdersFilter, + "EXCHANGE_MAX_NUM_ICEBERG_ORDERS": ExchangeMaxNumIcebergOrdersFilter, + "EXCHANGE_MAX_NUM_ORDER_LISTS": ExchangeMaxNumOrderListsFilter, + } + + ft = parsed.get("filterType") + target_cls = filter_type_map.get(ft) + + if target_cls is not None: + # Deserialize directly into the proper schema + instance = cls.model_construct() + instance.actual_instance = target_cls.from_dict(parsed) + return instance + + raise ValueError( + f"Unable to deserialize into ExchangeFilters: 'filterType' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/symbol_filters.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/symbol_filters.py index 03529d2b..ddd24ebd 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/symbol_filters.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/symbol_filters.py @@ -185,142 +185,49 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" - instance = cls.model_construct() - error_messages = [] - match = 0 + if parsed is None: + return None - is_list = isinstance(parsed, list) + if isinstance(parsed, dict) and "filterType" in parsed: + filter_type_map = { + "PRICE_FILTER": PriceFilter, + "PERCENT_PRICE": PercentPriceFilter, + "PERCENT_PRICE_BY_SIDE": PercentPriceBySideFilter, + "LOT_SIZE": LotSizeFilter, + "MIN_NOTIONAL": MinNotionalFilter, + "NOTIONAL": NotionalFilter, + "ICEBERG_PARTS": IcebergPartsFilter, + "MARKET_LOT_SIZE": MarketLotSizeFilter, + "MAX_NUM_ORDERS": MaxNumOrdersFilter, + "MAX_NUM_ALGO_ORDERS": MaxNumAlgoOrdersFilter, + "MAX_NUM_ICEBERG_ORDERS": MaxNumIcebergOrdersFilter, + "MAX_POSITION": MaxPositionFilter, + "TRAILING_DELTA": TrailingDeltaFilter, + "T_PLUS_SELL": TPlusSellFilter, + "MAX_NUM_ORDER_LISTS": MaxNumOrderListsFilter, + "MAX_NUM_ORDER_AMENDS": MaxNumOrderAmendsFilter, + } - # deserialize data into PriceFilter - if is_list == PriceFilter.is_array(): - try: - instance.actual_instance = PriceFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into PercentPriceFilter - if is_list == PercentPriceFilter.is_array(): - try: - instance.actual_instance = PercentPriceFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into PercentPriceBySideFilter - if is_list == PercentPriceBySideFilter.is_array(): - try: - instance.actual_instance = PercentPriceBySideFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into LotSizeFilter - if is_list == LotSizeFilter.is_array(): - try: - instance.actual_instance = LotSizeFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MinNotionalFilter - if is_list == MinNotionalFilter.is_array(): - try: - instance.actual_instance = MinNotionalFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into NotionalFilter - if is_list == NotionalFilter.is_array(): - try: - instance.actual_instance = NotionalFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into IcebergPartsFilter - if is_list == IcebergPartsFilter.is_array(): - try: - instance.actual_instance = IcebergPartsFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MarketLotSizeFilter - if is_list == MarketLotSizeFilter.is_array(): - try: - instance.actual_instance = MarketLotSizeFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MaxNumOrdersFilter - if is_list == MaxNumOrdersFilter.is_array(): - try: - instance.actual_instance = MaxNumOrdersFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MaxNumAlgoOrdersFilter - if is_list == MaxNumAlgoOrdersFilter.is_array(): - try: - instance.actual_instance = MaxNumAlgoOrdersFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MaxNumIcebergOrdersFilter - if is_list == MaxNumIcebergOrdersFilter.is_array(): - try: - instance.actual_instance = MaxNumIcebergOrdersFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MaxPositionFilter - if is_list == MaxPositionFilter.is_array(): - try: - instance.actual_instance = MaxPositionFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into TrailingDeltaFilter - if is_list == TrailingDeltaFilter.is_array(): - try: - instance.actual_instance = TrailingDeltaFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into TPlusSellFilter - if is_list == TPlusSellFilter.is_array(): - try: - instance.actual_instance = TPlusSellFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MaxNumOrderListsFilter - if is_list == MaxNumOrderListsFilter.is_array(): - try: - instance.actual_instance = MaxNumOrderListsFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into MaxNumOrderAmendsFilter - if is_list == MaxNumOrderAmendsFilter.is_array(): - try: - instance.actual_instance = MaxNumOrderAmendsFilter.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) + ft = parsed.get("filterType") + target_cls = filter_type_map.get(ft) - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into SymbolFilters with oneOf schemas: IcebergPartsFilter, LotSizeFilter, MarketLotSizeFilter, MaxNumAlgoOrdersFilter, MaxNumIcebergOrdersFilter, MaxNumOrderAmendsFilter, MaxNumOrderListsFilter, MaxNumOrdersFilter, MaxPositionFilter, MinNotionalFilter, NotionalFilter, PercentPriceBySideFilter, PercentPriceFilter, PriceFilter, TPlusSellFilter, TrailingDeltaFilter. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into SymbolFilters with oneOf schemas: IcebergPartsFilter, LotSizeFilter, MarketLotSizeFilter, MaxNumAlgoOrdersFilter, MaxNumIcebergOrdersFilter, MaxNumOrderAmendsFilter, MaxNumOrderListsFilter, MaxNumOrdersFilter, MaxPositionFilter, MinNotionalFilter, NotionalFilter, PercentPriceBySideFilter, PercentPriceFilter, PriceFilter, TPlusSellFilter, TrailingDeltaFilter. Details: " - + ", ".join(error_messages) - ) - else: - return instance + if target_cls is not None: + # Deserialize directly into the proper schema + instance = cls.model_construct() + instance.actual_instance = target_cls.from_dict(parsed) + return instance + + raise ValueError( + f"Unable to deserialize into SymbolFilters: 'filterType' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/user_data_stream_events_response.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/user_data_stream_events_response.py index c8c89375..48cb1f36 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/user_data_stream_events_response.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/user_data_stream_events_response.py @@ -111,72 +111,39 @@ def __init__(self, *args, **kwargs) -> None: def is_oneof_model(cls) -> bool: return True + @classmethod + def model_validate(cls, obj: dict) -> Self: + """Validate and deserialize a dict into the appropriate oneOf model.""" + return cls.from_dict(obj) + @classmethod def from_dict(cls, parsed) -> Self: """Returns the object represented by the json string""" - instance = cls.model_construct() - error_messages = [] - match = 0 - - is_list = isinstance(parsed, list) - - # deserialize data into OutboundAccountPosition - if is_list == OutboundAccountPosition.is_array(): - try: - instance.actual_instance = OutboundAccountPosition.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into BalanceUpdate - if is_list == BalanceUpdate.is_array(): - try: - instance.actual_instance = BalanceUpdate.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into ExecutionReport - if is_list == ExecutionReport.is_array(): - try: - instance.actual_instance = ExecutionReport.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into ListStatus - if is_list == ListStatus.is_array(): - try: - instance.actual_instance = ListStatus.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into EventStreamTerminated - if is_list == EventStreamTerminated.is_array(): - try: - instance.actual_instance = EventStreamTerminated.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into ExternalLockUpdate - if is_list == ExternalLockUpdate.is_array(): - try: - instance.actual_instance = ExternalLockUpdate.from_dict(parsed) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError( - "Multiple matches found when deserializing the JSON string into UserDataStreamEventsResponse with oneOf schemas: BalanceUpdate, EventStreamTerminated, ExecutionReport, ExternalLockUpdate, ListStatus, OutboundAccountPosition. Details: " - + ", ".join(error_messages) - ) - elif match == 0: - # no match - raise ValueError( - "No match found when deserializing the JSON string into UserDataStreamEventsResponse with oneOf schemas: BalanceUpdate, EventStreamTerminated, ExecutionReport, ExternalLockUpdate, ListStatus, OutboundAccountPosition. Details: " - + ", ".join(error_messages) - ) - else: - return instance + if parsed is None: + return None + + if isinstance(parsed, dict) and "e" in parsed: + event_type_map = { + "outboundAccountPosition": OutboundAccountPosition, + "balanceUpdate": BalanceUpdate, + "executionReport": ExecutionReport, + "listStatus": ListStatus, + "eventStreamTerminated": EventStreamTerminated, + "externalLockUpdate": ExternalLockUpdate, + } + + event_type = parsed.get("e") + target_cls = event_type_map.get(event_type) + + if target_cls is not None: + # Deserialize directly into the proper schema + instance = cls.model_construct() + instance.actual_instance = target_cls.from_dict(parsed) + return instance + + raise ValueError( + f"Unable to deserialize into UserDataStreamEventsResponse: 'e' field missing or unrecognized. Data: {parsed}" + ) def to_json(self) -> str: """Returns the JSON representation of the actual instance""" diff --git a/clients/spot/tests/unit/rest_api/test_general_api.py b/clients/spot/tests/unit/rest_api/test_general_api.py index 41ced2ec..fc08e73e 100644 --- a/clients/spot/tests/unit/rest_api/test_general_api.py +++ b/clients/spot/tests/unit/rest_api/test_general_api.py @@ -26,6 +26,9 @@ from binance_sdk_spot.rest_api.models import ExchangeInfoResponse from binance_sdk_spot.rest_api.models import TimeResponse +from binance_sdk_spot.rest_api.models import SymbolFilters +from binance_sdk_spot.rest_api.models import ExchangeFilters +from binance_sdk_spot.rest_api.models import AssetFilters from binance_sdk_spot.rest_api.models import ExchangeInfoSymbolStatusEnum @@ -326,3 +329,408 @@ def test_time_server_error(self): with pytest.raises(Exception, match="ResponseError"): self.client.time() + + def test_symbol_filters_price_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.price_filter import ( + PriceFilter + ) + + example_data = { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, PriceFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "PRICE_FILTER" + assert instance_data.min_price == "0.00000100" + assert instance_data.max_price == "100000.00000000" + assert instance_data.tick_size == "0.00000100" + + def test_symbol_filters_percent_price_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.percent_price_filter import ( + PercentPriceFilter + ) + + example_data = { + "filterType": "PERCENT_PRICE", + "multiplierUp": "1.3000", + "multiplierDown": "0.7000", + "avgPriceMins": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, PercentPriceFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "PERCENT_PRICE" + assert instance_data.multiplier_up == "1.3000" + assert instance_data.multiplier_down == "0.7000" + assert instance_data.avg_price_mins == 5 + + def test_symbol_filters_percent_price_by_side_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.percent_price_by_side_filter import ( + PercentPriceBySideFilter + ) + + example_data = { + "filterType": "PERCENT_PRICE_BY_SIDE", + "bidMultiplierUp": "1.2", + "bidMultiplierDown": "0.2", + "askMultiplierUp": "5", + "askMultiplierDown": "0.8", + "avgPriceMins": 1 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, PercentPriceBySideFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "PERCENT_PRICE_BY_SIDE" + assert instance_data.bid_multiplier_up == "1.2" + assert instance_data.bid_multiplier_down == "0.2" + assert instance_data.ask_multiplier_up == "5" + assert instance_data.ask_multiplier_down == "0.8" + assert instance_data.avg_price_mins == 1 + + def test_symbol_filters_lot_size_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.lot_size_filter import ( + LotSizeFilter + ) + + example_data = { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, LotSizeFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "LOT_SIZE" + assert instance_data.min_qty == "0.00100000" + assert instance_data.max_qty == "100000.00000000" + assert instance_data.step_size == "0.00100000" + + def test_symbol_filters_min_notional_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.min_notional_filter import ( + MinNotionalFilter + ) + + example_data = { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00100000", + "applyToMarket": True, + "avgPriceMins": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MinNotionalFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MIN_NOTIONAL" + assert instance_data.min_notional == "0.00100000" + assert instance_data.apply_to_market is True + assert instance_data.avg_price_mins == 5 + + def test_symbol_filters_notional_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.notional_filter import ( + NotionalFilter + ) + + example_data = { + "filterType": "NOTIONAL", + "minNotional": "10.00000000", + "applyMinToMarket": False, + "maxNotional": "10000.00000000", + "applyMaxToMarket": False, + "avgPriceMins": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, NotionalFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "NOTIONAL" + assert instance_data.min_notional == "10.00000000" + assert instance_data.apply_min_to_market is False + assert instance_data.max_notional == "10000.00000000" + assert instance_data.apply_max_to_market is False + assert instance_data.avg_price_mins == 5 + + def test_symbol_filters_iceberg_parts_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.iceberg_parts_filter import ( + IcebergPartsFilter + ) + + example_data = { + "filterType": "ICEBERG_PARTS", + "limit": 10 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, IcebergPartsFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "ICEBERG_PARTS" + assert instance_data.limit == 10 + + def test_symbol_filters_market_lot_size_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.market_lot_size_filter import ( + MarketLotSizeFilter + ) + + example_data = { + "filterType": "MARKET_LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MarketLotSizeFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MARKET_LOT_SIZE" + assert instance_data.min_qty == "0.00100000" + assert instance_data.max_qty == "100000.00000000" + assert instance_data.step_size == "0.00100000" + + def test_symbol_filters_max_num_orders_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.max_num_orders_filter import ( + MaxNumOrdersFilter + ) + + example_data = { + "filterType": "MAX_NUM_ORDERS", + "maxNumOrders": 25 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ORDERS" + assert instance_data.max_num_orders == 25 + + def test_symbol_filters_max_num_algo_orders_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.max_num_algo_orders_filter import ( + MaxNumAlgoOrdersFilter + ) + + example_data = { + "filterType": "MAX_NUM_ALGO_ORDERS", + "maxNumAlgoOrders": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumAlgoOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ALGO_ORDERS" + assert instance_data.max_num_algo_orders == 5 + + def test_symbol_filters_max_num_iceberg_orders_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.max_num_iceberg_orders_filter import ( + MaxNumIcebergOrdersFilter + ) + + example_data = { + "filterType": "MAX_NUM_ICEBERG_ORDERS", + "maxNumIcebergOrders": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumIcebergOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ICEBERG_ORDERS" + assert instance_data.max_num_iceberg_orders == 5 + + def test_symbol_filters_max_position_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.max_position_filter import ( + MaxPositionFilter + ) + + example_data = { + "filterType": "MAX_POSITION", + "maxPosition": "10.00000000" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxPositionFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_POSITION" + assert instance_data.max_position == "10.00000000" + + def test_symbol_filters_trailing_delta_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.trailing_delta_filter import ( + TrailingDeltaFilter + ) + + example_data = { + "filterType": "TRAILING_DELTA", + "minTrailingAboveDelta": 10, + "maxTrailingAboveDelta": 2000, + "minTrailingBelowDelta": 10, + "maxTrailingBelowDelta": 2000 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, TrailingDeltaFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "TRAILING_DELTA" + assert instance_data.min_trailing_above_delta == 10 + assert instance_data.max_trailing_above_delta == 2000 + assert instance_data.min_trailing_below_delta == 10 + assert instance_data.max_trailing_below_delta == 2000 + + def test_symbol_filters_max_num_order_lists_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.max_num_order_lists_filter import ( + MaxNumOrderListsFilter + ) + + example_data = { + "filterType": "MAX_NUM_ORDER_LISTS", + "maxNumOrderLists": 20 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumOrderListsFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ORDER_LISTS" + assert instance_data.max_num_order_lists == 20 + + def test_exchange_filters_exchange_max_num_orders_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.exchange_max_num_orders_filter import ( + ExchangeMaxNumOrdersFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ORDERS", + "maxNumOrders": 1000 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ORDERS" + assert instance_data.max_num_orders == 1000 + + def test_exchange_filters_exchange_max_num_algo_orders_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.exchange_max_num_algo_orders_filter import ( + ExchangeMaxNumAlgoOrdersFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ALGO_ORDERS", + "maxNumAlgoOrders": 200 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumAlgoOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ALGO_ORDERS" + assert instance_data.max_num_algo_orders == 200 + + def test_exchange_filters_exchange_max_num_iceberg_orders_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.exchange_max_num_iceberg_orders_filter import ( + ExchangeMaxNumIcebergOrdersFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ICEBERG_ORDERS", + "maxNumIcebergOrders": 10000 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumIcebergOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ICEBERG_ORDERS" + assert instance_data.max_num_iceberg_orders == 10000 + + def test_exchange_filters_exchange_max_num_order_lists_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.exchange_max_num_order_lists_filter import ( + ExchangeMaxNumOrderListsFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ORDER_LISTS", + "maxNumOrderLists": 20 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrderListsFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ORDER_LISTS" + assert instance_data.max_num_order_lists == 20 + + def test_asset_filters_max_asset_filter(self): + """Test AssetFilter oneOf deserialization.""" + + from binance_sdk_spot.rest_api.models.max_asset_filter import ( + MaxAssetFilter + ) + + example_data = { + "filterType": "MAX_ASSET", + "asset": "USDC", + "limit": "42.00000000" + } + + parsed_data = AssetFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxAssetFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_ASSET" + assert instance_data.asset == "USDC" + assert instance_data.limit == "42.00000000" diff --git a/clients/spot/tests/unit/websocket_api/test_general_api_ws_api.py b/clients/spot/tests/unit/websocket_api/test_general_api_ws_api.py index 10402bdc..cd9da843 100644 --- a/clients/spot/tests/unit/websocket_api/test_general_api_ws_api.py +++ b/clients/spot/tests/unit/websocket_api/test_general_api_ws_api.py @@ -26,6 +26,9 @@ from binance_sdk_spot.websocket_api.models import ExchangeInfoResponse from binance_sdk_spot.websocket_api.models import PingResponse from binance_sdk_spot.websocket_api.models import TimeResponse +from binance_sdk_spot.websocket_api.models import SymbolFilters +from binance_sdk_spot.websocket_api.models import ExchangeFilters +from binance_sdk_spot.websocket_api.models import AssetFilters class TestWebSocketGeneralApi: @@ -537,3 +540,408 @@ async def test_time_server_error(self): with pytest.raises(Exception, match="ResponseError"): await self.websocket_api.time() + + def test_symbol_filters_price_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.price_filter import ( + PriceFilter + ) + + example_data = { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, PriceFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "PRICE_FILTER" + assert instance_data.min_price == "0.00000100" + assert instance_data.max_price == "100000.00000000" + assert instance_data.tick_size == "0.00000100" + + def test_symbol_filters_percent_price_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.percent_price_filter import ( + PercentPriceFilter + ) + + example_data = { + "filterType": "PERCENT_PRICE", + "multiplierUp": "1.3000", + "multiplierDown": "0.7000", + "avgPriceMins": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, PercentPriceFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "PERCENT_PRICE" + assert instance_data.multiplier_up == "1.3000" + assert instance_data.multiplier_down == "0.7000" + assert instance_data.avg_price_mins == 5 + + def test_symbol_filters_percent_price_by_side_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.percent_price_by_side_filter import ( + PercentPriceBySideFilter + ) + + example_data = { + "filterType": "PERCENT_PRICE_BY_SIDE", + "bidMultiplierUp": "1.2", + "bidMultiplierDown": "0.2", + "askMultiplierUp": "5", + "askMultiplierDown": "0.8", + "avgPriceMins": 1 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, PercentPriceBySideFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "PERCENT_PRICE_BY_SIDE" + assert instance_data.bid_multiplier_up == "1.2" + assert instance_data.bid_multiplier_down == "0.2" + assert instance_data.ask_multiplier_up == "5" + assert instance_data.ask_multiplier_down == "0.8" + assert instance_data.avg_price_mins == 1 + + def test_symbol_filters_lot_size_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.lot_size_filter import ( + LotSizeFilter + ) + + example_data = { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, LotSizeFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "LOT_SIZE" + assert instance_data.min_qty == "0.00100000" + assert instance_data.max_qty == "100000.00000000" + assert instance_data.step_size == "0.00100000" + + def test_symbol_filters_min_notional_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.min_notional_filter import ( + MinNotionalFilter + ) + + example_data = { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00100000", + "applyToMarket": True, + "avgPriceMins": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MinNotionalFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MIN_NOTIONAL" + assert instance_data.min_notional == "0.00100000" + assert instance_data.apply_to_market is True + assert instance_data.avg_price_mins == 5 + + def test_symbol_filters_notional_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.notional_filter import ( + NotionalFilter + ) + + example_data = { + "filterType": "NOTIONAL", + "minNotional": "10.00000000", + "applyMinToMarket": False, + "maxNotional": "10000.00000000", + "applyMaxToMarket": False, + "avgPriceMins": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, NotionalFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "NOTIONAL" + assert instance_data.min_notional == "10.00000000" + assert instance_data.apply_min_to_market is False + assert instance_data.max_notional == "10000.00000000" + assert instance_data.apply_max_to_market is False + assert instance_data.avg_price_mins == 5 + + def test_symbol_filters_iceberg_parts_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.iceberg_parts_filter import ( + IcebergPartsFilter + ) + + example_data = { + "filterType": "ICEBERG_PARTS", + "limit": 10 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, IcebergPartsFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "ICEBERG_PARTS" + assert instance_data.limit == 10 + + def test_symbol_filters_market_lot_size_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.market_lot_size_filter import ( + MarketLotSizeFilter + ) + + example_data = { + "filterType": "MARKET_LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MarketLotSizeFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MARKET_LOT_SIZE" + assert instance_data.min_qty == "0.00100000" + assert instance_data.max_qty == "100000.00000000" + assert instance_data.step_size == "0.00100000" + + def test_symbol_filters_max_num_orders_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.max_num_orders_filter import ( + MaxNumOrdersFilter + ) + + example_data = { + "filterType": "MAX_NUM_ORDERS", + "maxNumOrders": 25 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ORDERS" + assert instance_data.max_num_orders == 25 + + def test_symbol_filters_max_num_algo_orders_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.max_num_algo_orders_filter import ( + MaxNumAlgoOrdersFilter + ) + + example_data = { + "filterType": "MAX_NUM_ALGO_ORDERS", + "maxNumAlgoOrders": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumAlgoOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ALGO_ORDERS" + assert instance_data.max_num_algo_orders == 5 + + def test_symbol_filters_max_num_iceberg_orders_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.max_num_iceberg_orders_filter import ( + MaxNumIcebergOrdersFilter + ) + + example_data = { + "filterType": "MAX_NUM_ICEBERG_ORDERS", + "maxNumIcebergOrders": 5 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumIcebergOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ICEBERG_ORDERS" + assert instance_data.max_num_iceberg_orders == 5 + + def test_symbol_filters_max_position_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.max_position_filter import ( + MaxPositionFilter + ) + + example_data = { + "filterType": "MAX_POSITION", + "maxPosition": "10.00000000" + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxPositionFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_POSITION" + assert instance_data.max_position == "10.00000000" + + def test_symbol_filters_trailing_delta_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.trailing_delta_filter import ( + TrailingDeltaFilter + ) + + example_data = { + "filterType": "TRAILING_DELTA", + "minTrailingAboveDelta": 10, + "maxTrailingAboveDelta": 2000, + "minTrailingBelowDelta": 10, + "maxTrailingBelowDelta": 2000 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, TrailingDeltaFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "TRAILING_DELTA" + assert instance_data.min_trailing_above_delta == 10 + assert instance_data.max_trailing_above_delta == 2000 + assert instance_data.min_trailing_below_delta == 10 + assert instance_data.max_trailing_below_delta == 2000 + + def test_symbol_filters_max_num_order_lists_filter(self): + """Test SymbolFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.max_num_order_lists_filter import ( + MaxNumOrderListsFilter + ) + + example_data = { + "filterType": "MAX_NUM_ORDER_LISTS", + "maxNumOrderLists": 20 + } + + parsed_data = SymbolFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxNumOrderListsFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_NUM_ORDER_LISTS" + assert instance_data.max_num_order_lists == 20 + + def test_exchange_filters_exchange_max_num_orders_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.exchange_max_num_orders_filter import ( + ExchangeMaxNumOrdersFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ORDERS", + "maxNumOrders": 1000 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ORDERS" + assert instance_data.max_num_orders == 1000 + + def test_exchange_filters_exchange_max_num_algo_orders_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.exchange_max_num_algo_orders_filter import ( + ExchangeMaxNumAlgoOrdersFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ALGO_ORDERS", + "maxNumAlgoOrders": 200 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumAlgoOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ALGO_ORDERS" + assert instance_data.max_num_algo_orders == 200 + + def test_exchange_filters_exchange_max_num_iceberg_orders_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.exchange_max_num_iceberg_orders_filter import ( + ExchangeMaxNumIcebergOrdersFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ICEBERG_ORDERS", + "maxNumIcebergOrders": 10000 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumIcebergOrdersFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ICEBERG_ORDERS" + assert instance_data.max_num_iceberg_orders == 10000 + + def test_exchange_filters_exchange_max_num_order_lists_filter(self): + """Test ExchangeFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.exchange_max_num_order_lists_filter import ( + ExchangeMaxNumOrderListsFilter + ) + + example_data = { + "filterType": "EXCHANGE_MAX_NUM_ORDER_LISTS", + "maxNumOrderLists": 20 + } + + parsed_data = ExchangeFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrderListsFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "EXCHANGE_MAX_NUM_ORDER_LISTS" + assert instance_data.max_num_order_lists == 20 + + def test_asset_filters_max_asset_filter(self): + """Test AssetFilter oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.max_asset_filter import ( + MaxAssetFilter + ) + + example_data = { + "filterType": "MAX_ASSET", + "asset": "USDC", + "limit": "42.00000000" + } + + parsed_data = AssetFilters.model_validate(example_data) + + assert isinstance(parsed_data.actual_instance, MaxAssetFilter) + instance_data = parsed_data.actual_instance + assert instance_data.filter_type == "MAX_ASSET" + assert instance_data.asset == "USDC" + assert instance_data.limit == "42.00000000" \ No newline at end of file diff --git a/clients/spot/tests/unit/websocket_api/test_user_data_stream_api_ws_api.py b/clients/spot/tests/unit/websocket_api/test_user_data_stream_api_ws_api.py index 3e857723..1e9acb1e 100644 --- a/clients/spot/tests/unit/websocket_api/test_user_data_stream_api_ws_api.py +++ b/clients/spot/tests/unit/websocket_api/test_user_data_stream_api_ws_api.py @@ -24,6 +24,7 @@ from binance_sdk_spot.websocket_api.models import SessionSubscriptionsResponse from binance_sdk_spot.websocket_api.models import UserDataStreamSubscribeResponse +from binance_sdk_spot.websocket_api.models import UserDataStreamEventsResponse from binance_sdk_spot.websocket_api.models import ( UserDataStreamSubscribeSignatureResponse, ) @@ -426,3 +427,254 @@ async def test_user_data_stream_unsubscribe_server_error(self): with pytest.raises(Exception, match="ResponseError"): await self.websocket_api.user_data_stream_unsubscribe() + + def test_user_data_stream_events_response_balance_update(self): + """Test UserDataStreamEventsResponse oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.balance_update import BalanceUpdate + + example_data = { + "subscriptionId": 0, + "event": { + "e": "balanceUpdate", # Event Type + "E": 1573200697110, # Event Time + "a": "BTC", # Asset + "d": "100.00000000", # Balance Delta + "T": 1573200697068 # Clear Time + } + } + + parsed_data = UserDataStreamEventsResponse.model_validate(example_data["event"]) + + assert isinstance(parsed_data.actual_instance, BalanceUpdate) + instance_data = parsed_data.actual_instance + assert instance_data.E == 1573200697110 + assert instance_data.a == "BTC" + assert instance_data.d == "100.00000000" + assert instance_data.T == 1573200697068 + + + def test_user_data_stream_events_response_outbound_account_position(self): + """Test UserDataStreamEventsResponse oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.outbound_account_position import ( + OutboundAccountPosition, + ) + example_data = { + "subscriptionId": 0, + "event": { + "e": "outboundAccountPosition", # Event type + "E": 1564034571105, # Event Time + "u": 1564034571073, # Time of last account update + # Balances Array + "B": [ + { + "a": "ETH", # Asset + "f": "10000.000000", # Free + "l": "0.000000" # Locked + } + ] + } + } + + parsed_data = UserDataStreamEventsResponse.model_validate(example_data["event"]) + + assert isinstance(parsed_data.actual_instance, OutboundAccountPosition) + instance_data = parsed_data.actual_instance + assert instance_data.E == 1564034571105 + assert instance_data.u == 1564034571073 + assert instance_data.B is not None + assert len(instance_data.B) == 1 + assert instance_data.B[0].a == "ETH" + assert instance_data.B[0].f == "10000.000000" + assert instance_data.B[0].l == "0.000000" + + def test_user_data_stream_events_response_execution_report(self): + """Test UserDataStreamEventsResponse oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.execution_report import ExecutionReport + + example_data = { + "subscriptionId": 0, + "event": { + "e": "executionReport", # Event type + "E": 1499405658658, # Event time + "s": "ETHBTC", # Symbol + "c": "mUvoqJxFIILMdfAW5iGSOW", # Client order ID + "S": "BUY", # Side + "o": "LIMIT", # Order type + "f": "GTC", # Time in force + "q": "1.00000000", # Order quantity + "p": "0.10264410", # Order price + "P": "0.00000000", # Stop price + "F": "0.00000000", # Iceberg quantity + "g": -1, # OrderListId + "C": "", # Original client order ID; This is the ID of the order being canceled + "x": "NEW", # Current execution type + "X": "CANCELLED", # Current order status + "r": "NONE", # Order reject reason; Please see Order Reject Reason (below) for more information. + "i": 4293153, # Order ID + "l": "1.00000000", # Last executed quantity + "z": "0.00000000", # Cumulative filled quantity + "L": "0.00000000", # Last executed price + "n": "0", # Commission amount + "N": None, # Commission asset + "T": 1499405658657, # Transaction time + "t": -1, # Trade ID + "v": 3, # Prevented Match Id; This is only visible if the order expired due to STP + "I": 8641984, # Execution Id + "w": True, # Is the order on the book? + "m": False, # Is this trade the maker side? + "M": True, # Ignore + "O": 1499405658657, # Order creation time + "Z": "0.00000000", # Cumulative quote asset transacted quantity + "Y": "0.00000000", # Last quote asset transacted quantity (i.e. lastPrice * lastQty) + "Q": "0.00000000", # Quote Order Quantity + "W": 1499405658657, # Working Time; This is only visible if the order has been placed on the book. + "V": "NONE" # SelfTradePreventionMode + } + } + + parsed_data = UserDataStreamEventsResponse.model_validate(example_data["event"]) + + assert isinstance(parsed_data.actual_instance, ExecutionReport) + instance_data = parsed_data.actual_instance + assert instance_data.E == 1499405658658 + assert instance_data.s == "ETHBTC" + assert instance_data.c == "mUvoqJxFIILMdfAW5iGSOW" + assert instance_data.S == "BUY" + assert instance_data.o == "LIMIT" + assert instance_data.f == "GTC" + assert instance_data.q == "1.00000000" + assert instance_data.p == "0.10264410" + assert instance_data.P == "0.00000000" + assert instance_data.F == "0.00000000" + assert instance_data.g == -1 + assert instance_data.C == "" + assert instance_data.x == "NEW" + assert instance_data.X == "CANCELLED" + assert instance_data.r == "NONE" + assert instance_data.i == 4293153 + assert instance_data.l == "1.00000000" + assert instance_data.z == "0.00000000" + assert instance_data.L == "0.00000000" + assert instance_data.n == "0" + assert instance_data.N is None + assert instance_data.T == 1499405658657 + assert instance_data.t == -1 + assert instance_data.v == 3 + assert instance_data.I == 8641984 + assert instance_data.w is True + assert instance_data.m is False + assert instance_data.M is True + assert instance_data.O == 1499405658657 + assert instance_data.Z == "0.00000000" + assert instance_data.Y == "0.00000000" + assert instance_data.Q == "0.00000000" + assert instance_data.W == 1499405658657 + assert instance_data.V == "NONE" + + def test_user_data_stream_events_response_event_stream_terminated(self): + """Test UserDataStreamEventsResponse oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.event_stream_terminated import ( + EventStreamTerminated, + ) + + example_data = { + "subscriptionId": 0, + "event": { + "e": "eventStreamTerminated", # Event Type + "E": 1728973001334 # Event Time + } + } + + parsed_data = UserDataStreamEventsResponse.model_validate(example_data["event"]) + + assert isinstance(parsed_data.actual_instance, EventStreamTerminated) + instance_data = parsed_data.actual_instance + assert instance_data.E == 1728973001334 + + + def test_user_data_stream_events_response_list_status(self): + """Test UserDataStreamEventsResponse oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.list_status import ListStatus + + example_data = { + "subscriptionId": 0, + "event": { + "e": "listStatus", # Event Type + "E": 1564035303637, # Event Time + "s": "ETHBTC", # Symbol + "g": 2, # OrderListId + "c": "OCO", # Contingency Type + "l": "EXEC_STARTED", # List Status Type + "L": "EXECUTING", # List Order Status + "r": "NONE", # List Reject Reason + "C": "F4QN4G8DlFATFlIUQ0cjdD", # List Client Order ID + "T": 1564035303625, # Transaction Time + # An array of objects + "O": [ + { + "s": "ETHBTC", # Symbol + "i": 17, # OrderId + "c": "AJYsMjErWJesZvqlJCTUgL" # ClientOrderId + }, + { + "s": "ETHBTC", + "i": 18, + "c": "bfYPSQdLoqAJeNrOr9adzq" + } + ] + } + } + + parsed_data = UserDataStreamEventsResponse.model_validate(example_data["event"]) + + assert isinstance(parsed_data.actual_instance, ListStatus) + instance_data = parsed_data.actual_instance + assert instance_data.E == 1564035303637 + assert instance_data.s == "ETHBTC" + assert instance_data.g == 2 + assert instance_data.c == "OCO" + assert instance_data.l == "EXEC_STARTED" + assert instance_data.L == "EXECUTING" + assert instance_data.r == "NONE" + assert instance_data.C == "F4QN4G8DlFATFlIUQ0cjdD" + assert instance_data.T == 1564035303625 + assert instance_data.O is not None + assert len(instance_data.O) == 2 + assert instance_data.O[0].s == "ETHBTC" + assert instance_data.O[0].i == 17 + assert instance_data.O[0].c == "AJYsMjErWJesZvqlJCTUgL" + assert instance_data.O[1].s == "ETHBTC" + assert instance_data.O[1].i == 18 + assert instance_data.O[1].c == "bfYPSQdLoqAJeNrOr9adzq" + + def test_user_data_stream_events_response_external_lock_update(self): + """Test UserDataStreamEventsResponse oneOf deserialization.""" + + from binance_sdk_spot.websocket_api.models.external_lock_update import ( + ExternalLockUpdate, + ) + + example_data = { + "subscriptionId": 0, + "event": { + "e": "externalLockUpdate", # Event Type + "E": 1581557507324, # Event Time + "a": "NEO", # Asset + "d": "10.00000000", # Delta + "T": 1581557507268 # Transaction Time + } + } + + parsed_data = UserDataStreamEventsResponse.model_validate(example_data["event"]) + + assert isinstance(parsed_data.actual_instance,ExternalLockUpdate) + instance_data = parsed_data.actual_instance + assert instance_data.E == 1581557507324 + assert instance_data.a == "NEO" + assert instance_data.d == "10.00000000" + assert instance_data.T == 1581557507268 From adb8834a1edb2d2f0bf9f561249c5e385cfc8383 Mon Sep 17 00:00:00 2001 From: mobias17 Date: Mon, 26 Jan 2026 19:53:15 +0100 Subject: [PATCH 2/4] Additional model deserialization fix candidates --- clients/spot/pyproject.toml | 4 +- .../rest_api/models/exchange_info_response.py | 14 ++++++- .../exchange_info_response_symbols_inner.py | 14 ++++++- .../rest_api/models/rate_limits.py | 14 ++++++- .../models/exchange_info_response_result.py | 14 ++++++- ...xchange_info_response_result_sors_inner.py | 15 ++++++-- ...ange_info_response_result_symbols_inner.py | 14 ++++++- .../websocket_api/models/rate_limits.py | 14 ++++++- .../tests/unit/rest_api/test_general_api.py | 38 +++++++++---------- common/pyproject.toml | 2 +- 10 files changed, 106 insertions(+), 37 deletions(-) diff --git a/clients/spot/pyproject.toml b/clients/spot/pyproject.toml index 0a420c83..c60feec4 100644 --- a/clients/spot/pyproject.toml +++ b/clients/spot/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "binance-sdk-spot" -version = "6.3.0" +version = "6.3.0+fork.1" description = "Official Binance Spot SDK - A lightweight library that provides a convenient interface to Binance's Spot REST API, WebSocket API and WebSocket Streams." authors = ["Binance"] license = "MIT" @@ -20,7 +20,7 @@ black = "^25.1.0" ruff = "^0.12.0" pycryptodome = "^3.17" aiohttp = "^3.9" -binance-common = "3.4.1" +binance-common = "3.4.1+fork.1" pytest = { version = ">=6.2.5", optional = true } [tool.poetry.extras] diff --git a/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response.py b/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response.py index 3d5a8b1f..22900468 100644 --- a/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response.py +++ b/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response.py @@ -77,6 +77,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of ExchangeInfoResponse from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -134,9 +143,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "timezone": obj.get("timezone"), "serverTime": obj.get("serverTime"), diff --git a/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response_symbols_inner.py b/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response_symbols_inner.py index c15201a5..c2239a3e 100644 --- a/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response_symbols_inner.py +++ b/clients/spot/src/binance_sdk_spot/rest_api/models/exchange_info_response_symbols_inner.py @@ -139,6 +139,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of ExchangeInfoResponseSymbolsInner from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -182,9 +191,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "symbol": obj.get("symbol"), "status": obj.get("status"), diff --git a/clients/spot/src/binance_sdk_spot/rest_api/models/rate_limits.py b/clients/spot/src/binance_sdk_spot/rest_api/models/rate_limits.py index 66d4f96e..37fefe05 100644 --- a/clients/spot/src/binance_sdk_spot/rest_api/models/rate_limits.py +++ b/clients/spot/src/binance_sdk_spot/rest_api/models/rate_limits.py @@ -70,6 +70,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of RateLimits from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -106,9 +115,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "rateLimitType": obj.get("rateLimitType"), "interval": obj.get("interval"), diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result.py index 4ed8e86e..aa190204 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result.py @@ -90,6 +90,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of ExchangeInfoResponseResult from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -154,9 +163,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "timezone": obj.get("timezone"), "serverTime": obj.get("serverTime"), diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_sors_inner.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_sors_inner.py index f0def90d..bd112460 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_sors_inner.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_sors_inner.py @@ -62,6 +62,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of ExchangeInfoResponseResultSorsInner from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -98,10 +107,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( - {"baseAsset": obj.get("baseAsset"), "symbols": obj.get("symbols")} + _obj = BaseModel.model_validate.__func__( + cls, {"baseAsset": obj.get("baseAsset"), "symbols": obj.get("symbols")} ) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_symbols_inner.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_symbols_inner.py index 8e9e6705..1f0cf7e4 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_symbols_inner.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response_result_symbols_inner.py @@ -144,6 +144,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of ExchangeInfoResponseResultSymbolsInner from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -187,9 +196,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "symbol": obj.get("symbol"), "status": obj.get("status"), diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/rate_limits.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/rate_limits.py index d302784c..b63c848b 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/rate_limits.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/rate_limits.py @@ -70,6 +70,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of RateLimits from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -106,9 +115,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "rateLimitType": obj.get("rateLimitType"), "interval": obj.get("interval"), diff --git a/clients/spot/tests/unit/rest_api/test_general_api.py b/clients/spot/tests/unit/rest_api/test_general_api.py index fc08e73e..a226e709 100644 --- a/clients/spot/tests/unit/rest_api/test_general_api.py +++ b/clients/spot/tests/unit/rest_api/test_general_api.py @@ -344,7 +344,7 @@ def test_symbol_filters_price_filter(self): "tickSize": "0.00000100" } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, PriceFilter) instance_data = parsed_data.actual_instance @@ -367,7 +367,7 @@ def test_symbol_filters_percent_price_filter(self): "avgPriceMins": 5 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, PercentPriceFilter) instance_data = parsed_data.actual_instance @@ -392,7 +392,7 @@ def test_symbol_filters_percent_price_by_side_filter(self): "avgPriceMins": 1 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, PercentPriceBySideFilter) instance_data = parsed_data.actual_instance @@ -417,7 +417,7 @@ def test_symbol_filters_lot_size_filter(self): "stepSize": "0.00100000" } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, LotSizeFilter) instance_data = parsed_data.actual_instance @@ -440,7 +440,7 @@ def test_symbol_filters_min_notional_filter(self): "avgPriceMins": 5 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MinNotionalFilter) instance_data = parsed_data.actual_instance @@ -465,7 +465,7 @@ def test_symbol_filters_notional_filter(self): "avgPriceMins": 5 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, NotionalFilter) instance_data = parsed_data.actual_instance @@ -488,7 +488,7 @@ def test_symbol_filters_iceberg_parts_filter(self): "limit": 10 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, IcebergPartsFilter) instance_data = parsed_data.actual_instance @@ -509,7 +509,7 @@ def test_symbol_filters_market_lot_size_filter(self): "stepSize": "0.00100000" } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MarketLotSizeFilter) instance_data = parsed_data.actual_instance @@ -530,7 +530,7 @@ def test_symbol_filters_max_num_orders_filter(self): "maxNumOrders": 25 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MaxNumOrdersFilter) instance_data = parsed_data.actual_instance @@ -549,7 +549,7 @@ def test_symbol_filters_max_num_algo_orders_filter(self): "maxNumAlgoOrders": 5 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MaxNumAlgoOrdersFilter) instance_data = parsed_data.actual_instance @@ -568,7 +568,7 @@ def test_symbol_filters_max_num_iceberg_orders_filter(self): "maxNumIcebergOrders": 5 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MaxNumIcebergOrdersFilter) instance_data = parsed_data.actual_instance @@ -587,7 +587,7 @@ def test_symbol_filters_max_position_filter(self): "maxPosition": "10.00000000" } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MaxPositionFilter) instance_data = parsed_data.actual_instance @@ -609,7 +609,7 @@ def test_symbol_filters_trailing_delta_filter(self): "maxTrailingBelowDelta": 2000 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, TrailingDeltaFilter) instance_data = parsed_data.actual_instance @@ -631,7 +631,7 @@ def test_symbol_filters_max_num_order_lists_filter(self): "maxNumOrderLists": 20 } - parsed_data = SymbolFilters.model_validate(example_data) + parsed_data = SymbolFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MaxNumOrderListsFilter) instance_data = parsed_data.actual_instance @@ -650,7 +650,7 @@ def test_exchange_filters_exchange_max_num_orders_filter(self): "maxNumOrders": 1000 } - parsed_data = ExchangeFilters.model_validate(example_data) + parsed_data = ExchangeFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrdersFilter) instance_data = parsed_data.actual_instance @@ -669,7 +669,7 @@ def test_exchange_filters_exchange_max_num_algo_orders_filter(self): "maxNumAlgoOrders": 200 } - parsed_data = ExchangeFilters.model_validate(example_data) + parsed_data = ExchangeFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumAlgoOrdersFilter) instance_data = parsed_data.actual_instance @@ -688,7 +688,7 @@ def test_exchange_filters_exchange_max_num_iceberg_orders_filter(self): "maxNumIcebergOrders": 10000 } - parsed_data = ExchangeFilters.model_validate(example_data) + parsed_data = ExchangeFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumIcebergOrdersFilter) instance_data = parsed_data.actual_instance @@ -707,7 +707,7 @@ def test_exchange_filters_exchange_max_num_order_lists_filter(self): "maxNumOrderLists": 20 } - parsed_data = ExchangeFilters.model_validate(example_data) + parsed_data = ExchangeFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrderListsFilter) instance_data = parsed_data.actual_instance @@ -727,7 +727,7 @@ def test_asset_filters_max_asset_filter(self): "limit": "42.00000000" } - parsed_data = AssetFilters.model_validate(example_data) + parsed_data = AssetFilters.from_dict(example_data) assert isinstance(parsed_data.actual_instance, MaxAssetFilter) instance_data = parsed_data.actual_instance diff --git a/common/pyproject.toml b/common/pyproject.toml index afd0cfff..839e3582 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "binance-common" -version = "3.4.1" +version = "3.4.1+fork.1" description = "Binance Common Types and Utilities for Binance Connectors" authors = ["Binance"] license = "MIT" From 4de55385ec973d4f2fc639324ab35f94abcdf946 Mon Sep 17 00:00:00 2001 From: mobias17 Date: Mon, 26 Jan 2026 19:53:36 +0100 Subject: [PATCH 3/4] adding test --- .../tests/unit/rest_api/test_general_api.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/clients/spot/tests/unit/rest_api/test_general_api.py b/clients/spot/tests/unit/rest_api/test_general_api.py index a226e709..fc08e73e 100644 --- a/clients/spot/tests/unit/rest_api/test_general_api.py +++ b/clients/spot/tests/unit/rest_api/test_general_api.py @@ -344,7 +344,7 @@ def test_symbol_filters_price_filter(self): "tickSize": "0.00000100" } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, PriceFilter) instance_data = parsed_data.actual_instance @@ -367,7 +367,7 @@ def test_symbol_filters_percent_price_filter(self): "avgPriceMins": 5 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, PercentPriceFilter) instance_data = parsed_data.actual_instance @@ -392,7 +392,7 @@ def test_symbol_filters_percent_price_by_side_filter(self): "avgPriceMins": 1 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, PercentPriceBySideFilter) instance_data = parsed_data.actual_instance @@ -417,7 +417,7 @@ def test_symbol_filters_lot_size_filter(self): "stepSize": "0.00100000" } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, LotSizeFilter) instance_data = parsed_data.actual_instance @@ -440,7 +440,7 @@ def test_symbol_filters_min_notional_filter(self): "avgPriceMins": 5 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MinNotionalFilter) instance_data = parsed_data.actual_instance @@ -465,7 +465,7 @@ def test_symbol_filters_notional_filter(self): "avgPriceMins": 5 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, NotionalFilter) instance_data = parsed_data.actual_instance @@ -488,7 +488,7 @@ def test_symbol_filters_iceberg_parts_filter(self): "limit": 10 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, IcebergPartsFilter) instance_data = parsed_data.actual_instance @@ -509,7 +509,7 @@ def test_symbol_filters_market_lot_size_filter(self): "stepSize": "0.00100000" } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MarketLotSizeFilter) instance_data = parsed_data.actual_instance @@ -530,7 +530,7 @@ def test_symbol_filters_max_num_orders_filter(self): "maxNumOrders": 25 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MaxNumOrdersFilter) instance_data = parsed_data.actual_instance @@ -549,7 +549,7 @@ def test_symbol_filters_max_num_algo_orders_filter(self): "maxNumAlgoOrders": 5 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MaxNumAlgoOrdersFilter) instance_data = parsed_data.actual_instance @@ -568,7 +568,7 @@ def test_symbol_filters_max_num_iceberg_orders_filter(self): "maxNumIcebergOrders": 5 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MaxNumIcebergOrdersFilter) instance_data = parsed_data.actual_instance @@ -587,7 +587,7 @@ def test_symbol_filters_max_position_filter(self): "maxPosition": "10.00000000" } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MaxPositionFilter) instance_data = parsed_data.actual_instance @@ -609,7 +609,7 @@ def test_symbol_filters_trailing_delta_filter(self): "maxTrailingBelowDelta": 2000 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, TrailingDeltaFilter) instance_data = parsed_data.actual_instance @@ -631,7 +631,7 @@ def test_symbol_filters_max_num_order_lists_filter(self): "maxNumOrderLists": 20 } - parsed_data = SymbolFilters.from_dict(example_data) + parsed_data = SymbolFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MaxNumOrderListsFilter) instance_data = parsed_data.actual_instance @@ -650,7 +650,7 @@ def test_exchange_filters_exchange_max_num_orders_filter(self): "maxNumOrders": 1000 } - parsed_data = ExchangeFilters.from_dict(example_data) + parsed_data = ExchangeFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrdersFilter) instance_data = parsed_data.actual_instance @@ -669,7 +669,7 @@ def test_exchange_filters_exchange_max_num_algo_orders_filter(self): "maxNumAlgoOrders": 200 } - parsed_data = ExchangeFilters.from_dict(example_data) + parsed_data = ExchangeFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumAlgoOrdersFilter) instance_data = parsed_data.actual_instance @@ -688,7 +688,7 @@ def test_exchange_filters_exchange_max_num_iceberg_orders_filter(self): "maxNumIcebergOrders": 10000 } - parsed_data = ExchangeFilters.from_dict(example_data) + parsed_data = ExchangeFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumIcebergOrdersFilter) instance_data = parsed_data.actual_instance @@ -707,7 +707,7 @@ def test_exchange_filters_exchange_max_num_order_lists_filter(self): "maxNumOrderLists": 20 } - parsed_data = ExchangeFilters.from_dict(example_data) + parsed_data = ExchangeFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, ExchangeMaxNumOrderListsFilter) instance_data = parsed_data.actual_instance @@ -727,7 +727,7 @@ def test_asset_filters_max_asset_filter(self): "limit": "42.00000000" } - parsed_data = AssetFilters.from_dict(example_data) + parsed_data = AssetFilters.model_validate(example_data) assert isinstance(parsed_data.actual_instance, MaxAssetFilter) instance_data = parsed_data.actual_instance From 1b5647bf019eb8c745d01f8b3a4e8d00f611bdab Mon Sep 17 00:00:00 2001 From: mobias17 Date: Wed, 28 Jan 2026 08:33:55 +0100 Subject: [PATCH 4/4] Updated Exchange Response Model on Websocket side --- clients/spot/pyproject.toml | 4 ++-- .../websocket_api/models/exchange_info_response.py | 14 ++++++++++++-- common/pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/clients/spot/pyproject.toml b/clients/spot/pyproject.toml index c60feec4..ea7778ce 100644 --- a/clients/spot/pyproject.toml +++ b/clients/spot/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "binance-sdk-spot" -version = "6.3.0+fork.1" +version = "6.3.0+fork.2" description = "Official Binance Spot SDK - A lightweight library that provides a convenient interface to Binance's Spot REST API, WebSocket API and WebSocket Streams." authors = ["Binance"] license = "MIT" @@ -20,7 +20,7 @@ black = "^25.1.0" ruff = "^0.12.0" pycryptodome = "^3.17" aiohttp = "^3.9" -binance-common = "3.4.1+fork.1" +binance-common = "3.4.1+fork.2" pytest = { version = ">=6.2.5", optional = true } [tool.poetry.extras] diff --git a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response.py b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response.py index 3b8bc3b8..05c493ba 100644 --- a/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response.py +++ b/clients/spot/src/binance_sdk_spot/websocket_api/models/exchange_info_response.py @@ -68,6 +68,15 @@ def from_json(cls, json_str: str) -> Optional[Self]: """Create an instance of ExchangeInfoResponse from a JSON string""" return cls.from_dict(json.loads(json_str)) + @classmethod + def model_validate(cls, obj: Any) -> Self: + """Validate and deserialize using custom from_dict logic.""" + # If obj is a dict, use from_dict to handle nested oneOf models properly + if isinstance(obj, dict): + return cls.from_dict(obj) + # Otherwise use Pydantic's default validation + return super().model_validate(obj) + def to_dict(self) -> Dict[str, Any]: """Return the dictionary representation of the model using alias. @@ -114,9 +123,10 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return None if not isinstance(obj, dict): - return cls.model_validate(obj) + return BaseModel.model_validate.__func__(cls, obj) - _obj = cls.model_validate( + _obj = BaseModel.model_validate.__func__( + cls, { "id": obj.get("id"), "status": obj.get("status"), diff --git a/common/pyproject.toml b/common/pyproject.toml index 839e3582..43dffc74 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "binance-common" -version = "3.4.1+fork.1" +version = "3.4.1+fork.2" description = "Binance Common Types and Utilities for Binance Connectors" authors = ["Binance"] license = "MIT"