1+ import copy
12import hashlib
23import hmac
4+ import importlib
35import json
46import re
57import requests
911import uuid
1012
1113from base64 import b64encode
14+ from collections import OrderedDict
1215from Crypto .Hash import SHA256
1316from Crypto .Signature .pkcs1_15 import PKCS115_SigScheme
1417from enum import Enum
1518from pydantic import BaseModel
16- from typing import Dict , List , Optional , Type , TypeVar , Union
19+ from typing import Dict , List , Optional , Type , TypeVar , Union , get_args
1720from urllib .parse import urlencode
1821from urllib3 .util .ssl_ import create_urllib3_context
1922
3033 TooManyRequestsError ,
3134 UnauthorizedError ,
3235)
33- from binance_common .models import ApiResponse , RateLimit , WebsocketApiRateLimit
36+ from binance_common .models import (
37+ ApiResponse ,
38+ RateLimit ,
39+ WebsocketApiOptions ,
40+ WebsocketApiRateLimit
41+ )
3442from binance_common .signature import Signers
3543
3644
@@ -346,8 +354,14 @@ def send_request(
346354 else :
347355 data_function = lambda : response_model .model_validate (parsed )
348356
357+ try :
358+ data_function ()
359+ final_data_function = data_function
360+ except Exception :
361+ final_data_function = lambda : parsed
362+
349363 return ApiResponse [T ](
350- data_function = data_function ,
364+ data_function = final_data_function ,
351365 status = response .status_code ,
352366 headers = response .headers ,
353367 rate_limits = parse_rate_limit_headers (response .headers ),
@@ -506,6 +520,7 @@ def ws_streams_placeholder(stream: str, params: dict = {}) -> str:
506520 stream ,
507521 )
508522
523+
509524def parse_ws_rate_limit_headers (
510525 headers : List [Dict [str , Union [str , int ]]],
511526) -> List [WebsocketApiRateLimit ]:
@@ -522,6 +537,7 @@ def parse_ws_rate_limit_headers(
522537 rate_limits .append (WebsocketApiRateLimit (** header ))
523538 return rate_limits
524539
540+
525541def normalize_query_values (parsed , expected_types = None ):
526542 """Normalizes the values in a parsed query dictionary.
527543
@@ -563,3 +579,144 @@ def convert(val, expected_type=None):
563579 normalized [k ] = converted [0 ] if len (converted ) == 1 else converted
564580
565581 return normalized
582+
583+
584+ def ws_api_payload (config , payload : Dict , websocket_options : WebsocketApiOptions ):
585+ """Generate payload for websocket API
586+
587+ Args:
588+ config: The API configuration.
589+ payload (Dict): The payload to send.
590+ websocket_options (WebsocketApiOptions): The WebSocket API options.
591+
592+ Returns:
593+ Dict: The generated payload for the WebSocket API.
594+ """
595+
596+ payload = copy .copy (payload )
597+
598+ if websocket_options .api_key and websocket_options .skip_auth is False :
599+ payload ["params" ]["apiKey" ] = config .api_key
600+
601+ payload ["id" ] = payload ["id" ] if "id" in payload else get_uuid ()
602+
603+ payload ["params" ] = {
604+ snake_to_camel (k ): json .dumps (make_serializable (v ), separators = ("," , ":" ))
605+ if isinstance (v , (list , dict ))
606+ else make_serializable (v )
607+ for k , v in payload ["params" ].items ()
608+ }
609+
610+ if websocket_options .is_signed :
611+ if websocket_options .skip_auth is True :
612+ payload ["params" ]["timestamp" ] = get_timestamp ()
613+ else :
614+ payload ["params" ] = websocket_api_signature (config , payload ["params" ], websocket_options .signer )
615+
616+ return payload
617+
618+
619+ def websocket_api_signature (config , payload : Optional [Dict ] = {}, signer : Signers = None ) -> dict :
620+ """Generate signature for websocket API
621+
622+ Args:
623+ payload (Optional[Dict]): Payload.
624+ signer (Signers): Signer for the payload.
625+ Returns:
626+ dict: The generated signature for the WebSocket API.
627+ """
628+
629+ payload ["apiKey" ] = config .api_key
630+ payload ["timestamp" ] = get_timestamp ()
631+ parameters = OrderedDict (sorted (payload .items ()))
632+ parameters ["signature" ] = get_signature (
633+ config , urlencode (parameters ), signer
634+ )
635+ return parameters
636+
637+
638+ def get_validator_field_map (response_model_cls : Type [BaseModel ]) -> dict [str , str ]:
639+ """Map User Data response schema class name to oneof_schema_N_validator field dynamically
640+
641+ Args:
642+ response_model_cls (Type[BaseModel]): The response model class.
643+
644+ Returns:
645+ dict[str, str]: The mapping of field names to validator field names.
646+ """
647+
648+ field_map = {}
649+ annotations = getattr (response_model_cls , "__annotations__" , {})
650+
651+ for field_name , annotation in annotations .items ():
652+ if field_name .startswith ("oneof_schema_" ) and field_name .endswith ("_validator" ):
653+ if getattr (annotation , "__origin__" , None ) is Union :
654+ type_ = next ((arg for arg in get_args (annotation ) if arg is not type (None )), None )
655+ else :
656+ type_ = annotation
657+ if isinstance (type_ , str ):
658+ type_ = re .sub (r'^Optional\[(.*)\]$' , r'\1' , type_ )
659+
660+ if type_ :
661+ field_map [type_ ] = field_name
662+
663+ return field_map
664+
665+
666+ def resolve_model_from_event (response_model_cls : Type [BaseModel ], event_name : str ) -> Type [BaseModel ]:
667+ """Resolve the correct model class for the websocket event dynamically.
668+
669+ Args:
670+ response_model_cls (Type[BaseModel]): The response model class.
671+ event_name (str): The name of the event.
672+
673+ Returns:
674+ Type[BaseModel]: The resolved model class or None.
675+ """
676+
677+ if not event_name :
678+ return None
679+
680+ one_of_schemas_field = response_model_cls .__pydantic_fields__ .get ("one_of_schemas" )
681+ if not one_of_schemas_field :
682+ return None
683+
684+ one_of_schemas = list (one_of_schemas_field .default )
685+ event_to_class_map = {name [0 ].lower () + name [1 :]: name for name in one_of_schemas }
686+
687+ class_name = event_to_class_map .get (event_name )
688+ if not class_name :
689+ return None
690+
691+ module = importlib .import_module (response_model_cls .__module__ )
692+ return getattr (module , class_name , None )
693+
694+
695+ def parse_user_event (payload : dict , response_model_cls : Type [BaseModel ]) -> BaseModel :
696+ """Parse user event to response model.
697+
698+ Args:
699+ payload (dict): The payload dictionary containing event data.
700+ response_model_cls (Type[BaseModel]): The response model class.
701+
702+ Returns:
703+ BaseModel: The parsed response model instance.
704+ """
705+
706+ event_name = payload .get ("e" )
707+ model_cls = resolve_model_from_event (response_model_cls , event_name )
708+
709+ if not model_cls :
710+ return response_model_cls (actual_instance = payload )
711+
712+ try :
713+ instance = model_cls .model_validate (payload )
714+ validator_field_map = get_validator_field_map (response_model_cls )
715+ kwargs = {"actual_instance" : instance }
716+ if validator_field := validator_field_map .get (model_cls .__name__ ):
717+ kwargs [validator_field ] = instance
718+ return response_model_cls (** kwargs )
719+
720+ except Exception as e :
721+ print (f"Failed to parse { event_name } : { e } " )
722+ return response_model_cls (actual_instance = payload )
0 commit comments