Skip to content

Commit bc6fbb2

Browse files
authored
Merge pull request #517 from binance/release_common_v3.8.0
2 parents d385f64 + 075d6ac commit bc6fbb2

File tree

12 files changed

+393
-60
lines changed

12 files changed

+393
-60
lines changed

common/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 3.8.0 - 2026-03-26
4+
5+
### Added (2)
6+
7+
- Added `py.typed` file to indicate that the package supports type hints.
8+
- Added clear cache option to `Signer` class to allow clearing cached signatures.
9+
10+
### Updated (4)
11+
12+
- Fix bug with exposing secrets on messages logging.
13+
- Updated `print` statement to be logged instead of printed.
14+
- Updated mutable default parameter to avoid shared state between instances.
15+
- Updated `tox.ini` file
16+
317
## 3.7.0 - 2026-03-16
418

519
### Added (1)

common/pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[tool.poetry]
22
name = "binance-common"
3-
version = "3.7.0"
3+
version = "3.8.0"
44
description = "Binance Common Types and Utilities for Binance Connectors"
55
authors = ["Binance"]
66
license = "MIT"
77
readme = "README.md"
8-
include = ["CHANGELOG.md", "LICENSE", "README.md"]
8+
include = ["CHANGELOG.md", "LICENSE", "README.md", "src/binance_common/py.typed"]
99
packages = [
1010
{ include = "binance_common", from = "src" }
1111
]
@@ -37,3 +37,6 @@ exclude = [".git", ".tox", "build", "dist"]
3737

3838
[tool.ruff.lint]
3939
ignore = ["E741"]
40+
41+
[tool.flake8]
42+
max-line-length = 100

common/src/binance_common/configuration.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ class ConfigurationRestAPI:
2525

2626
def __init__(
2727
self,
28-
api_key: str = None,
28+
api_key: Optional[str] = None,
2929
api_secret: Optional[str] = None,
30-
base_path: str = None,
30+
base_path: Optional[str] = None,
3131
timeout: int = 1000,
3232
proxy: Optional[Dict[str, Union[str, int, Dict[str, str]]]] = None,
3333
keep_alive: bool = True,
@@ -38,15 +38,15 @@ def __init__(
3838
time_unit: Optional[str] = None,
3939
private_key: Optional[Union[bytes, str]] = None,
4040
private_key_passphrase: Optional[str] = None,
41-
custom_headers: Optional[dict[str, Union[str, List[str]]]] = {},
41+
custom_headers: Optional[dict[str, Union[str, List[str]]]] = None,
4242
):
4343
"""
4444
Initialize the API configuration.
4545
4646
Args:
47-
api_key (str): API key for authentication.
47+
api_key (Optional[str]): API key for authentication (default: None).
4848
api_secret (Optional[str]): API secret for authentication (default: None).
49-
base_path (str): Base API URL (default: None).
49+
base_path (Optional[str]): Base API URL (default: None).
5050
timeout (int): Request timeout in milliseconds (default: 1000).
5151
proxy (Optional[Dict[str, Union[str, int, Dict[str, str]]]]): Proxy settings (default: None).
5252
keep_alive (bool): Enable Keep-Alive (default: True).
@@ -77,7 +77,7 @@ def __init__(
7777
self.base_headers = {
7878
"Accept": "application/json",
7979
"X-MBX-APIKEY": str(self.api_key) if self.api_key else "",
80-
**parse_custom_headers(custom_headers),
80+
**parse_custom_headers(custom_headers if custom_headers else {}),
8181
}
8282

8383

@@ -99,18 +99,18 @@ class ConfigurationWebSocketAPI:
9999

100100
def __init__(
101101
self,
102-
api_key: str = None,
102+
api_key: Optional[str] = None,
103103
api_secret: Optional[str] = None,
104104
private_key: Optional[Union[bytes, str]] = None,
105105
private_key_passphrase: Optional[str] = None,
106-
stream_url: str = "wss://ws-api.binance.com/ws-api/v3",
106+
stream_url: Optional[str] = None,
107107
timeout: int = 5000,
108108
reconnect_delay: int = 5000,
109109
compression: int = 0,
110110
proxy: Optional[Dict[str, Union[str, int, Dict[str, str]]]] = None,
111111
mode: WebsocketMode = WebsocketMode.SINGLE,
112112
pool_size: int = 2,
113-
time_unit: TimeUnit = None,
113+
time_unit: Optional[TimeUnit] = None,
114114
https_agent: Optional[ssl.SSLContext] = None,
115115
session_re_logon: Optional[bool] = True,
116116
return_rate_limits: Optional[bool] = True,
@@ -119,11 +119,11 @@ def __init__(
119119
Initialize the API configuration.
120120
121121
Args:
122-
api_key (str): API key for authentication.
122+
api_key (Optional[str]): API key for authentication (default: None).
123123
api_secret (Optional[str]): API secret for authentication (default: None).
124124
private_key (Optional[Union[bytes, str]]): Private key for authentication (default: None).
125125
private_key_passphrase (Optional[str]): Passphrase for private key (default: None).
126-
stream_url (str): Base WebSocket API URL (default: "wss://ws-api.binance.com/ws-api/v3").
126+
stream_url (Optional[str]): Base WebSocket API URL (default: None).
127127
timeout (int): Request timeout in milliseconds (default: 5000).
128128
reconnect_delay (int): Delay (ms) between reconnections (default: 5000).
129129
compression (int): Compression level (default: 0).
@@ -168,20 +168,20 @@ class ConfigurationWebSocketStreams:
168168

169169
def __init__(
170170
self,
171-
stream_url: str = "wss://stream.binance.com:9443/stream",
171+
stream_url: Optional[str] = None,
172172
reconnect_delay: int = 5000,
173173
compression: int = 0,
174174
proxy: Optional[Dict[str, Union[str, int, Dict[str, str]]]] = None,
175175
mode: WebsocketMode = WebsocketMode.SINGLE,
176176
pool_size: int = 2,
177-
time_unit: TimeUnit = None,
177+
time_unit: Optional[TimeUnit] = None,
178178
https_agent: Optional[ssl.SSLContext] = None,
179179
):
180180
"""
181181
Initialize the Websocket Stream configuration.
182182
183183
Args:
184-
stream_url (str): Base WebSocket Stream URL (default: "wss://stream.binance.com:9443").
184+
stream_url (Optional[str]): Base WebSocket Stream URL (default: None).
185185
reconnect_delay (int): Delay (ms) between reconnections (default: 5000).
186186
compression (int): Compression level (default: 0).
187187
proxy (Optional[Dict[str, Union[str, int, Dict[str, str]]]]): Proxy settings (default: None).

common/src/binance_common/constants.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class WebsocketMode(Enum):
5959
DERIVATIVES_TRADING_USDS_FUTURES_REST_API_TESTNET_URL = (
6060
"https://testnet.binancefuture.com"
6161
)
62-
DERIVATIVES_TRADING_USDS_FUTURES_REST_API_DEMO_URL = 'https://demo-fapi.binance.com'
62+
DERIVATIVES_TRADING_USDS_FUTURES_REST_API_DEMO_URL = "https://demo-fapi.binance.com"
6363
DERIVATIVES_TRADING_USDS_FUTURES_WS_API_PROD_URL = (
6464
"wss://ws-fapi.binance.com/ws-fapi/v1"
6565
)
@@ -125,13 +125,13 @@ class WebsocketMode(Enum):
125125
# Spot Constants
126126
SPOT_REST_API_PROD_URL = "https://api.binance.com"
127127
SPOT_REST_API_TESTNET_URL = "https://testnet.binance.vision"
128-
SPOT_REST_API_DEMO_URL = 'https://demo-api.binance.com'
128+
SPOT_REST_API_DEMO_URL = "https://demo-api.binance.com"
129129
SPOT_WS_API_PROD_URL = "wss://ws-api.binance.com:443/ws-api/v3"
130130
SPOT_WS_API_TESTNET_URL = "wss://ws-api.testnet.binance.vision/ws-api/v3"
131-
SPOT_WS_API_DEMO_URL = 'wss://demo-ws-api.binance.com/ws-api/v3'
131+
SPOT_WS_API_DEMO_URL = "wss://demo-ws-api.binance.com/ws-api/v3"
132132
SPOT_WS_STREAMS_PROD_URL = "wss://stream.binance.com:9443"
133133
SPOT_WS_STREAMS_TESTNET_URL = "wss://stream.testnet.binance.vision"
134-
SPOT_WS_STREAMS_DEMO_URL = 'wss://demo-stream.binance.com:9443'
134+
SPOT_WS_STREAMS_DEMO_URL = "wss://demo-stream.binance.com:9443"
135135
SPOT_REST_API_MARKET_URL = "https://data-api.binance.vision"
136136
SPOT_WS_STREAMS_MARKET_URL = "wss://data-stream.binance.vision"
137137

common/src/binance_common/models.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(
3939
data_function: Callable[[], T],
4040
status: int,
4141
headers: dict,
42-
rate_limits: List[RateLimit] = None,
42+
rate_limits: Optional[List[RateLimit]] = None,
4343
):
4444
self._data_function = data_function
4545
self.status = status
@@ -82,8 +82,8 @@ class WebsocketApiResponse(Generic[T]):
8282

8383
def __init__(
8484
self,
85-
data_function: T = None,
86-
rate_limits: List[WebsocketApiRateLimit] = None,
85+
data_function: Optional[T] = None,
86+
rate_limits: Optional[List[WebsocketApiRateLimit]] = None,
8787
):
8888
self._data_function = data_function
8989
self.rate_limits = rate_limits or []
@@ -93,6 +93,8 @@ def data(self) -> T:
9393
9494
:return: The parsed data of type T.
9595
"""
96+
if self._data_function is None:
97+
raise ValueError("No data function provided for this response.")
9698
return self._data_function()
9799

98100

common/src/binance_common/py.typed

Whitespace-only changes.

common/src/binance_common/signature.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ def get_rsa_signer(
3838
cls._rsa_signers[cache_key] = pkcs1_15.new(rsa_key)
3939
return cls._rsa_signers[cache_key]
4040

41+
@classmethod
42+
def clear_rsa_cache(cls):
43+
cls._rsa_keys.clear()
44+
cls._rsa_signers.clear()
45+
4146
@classmethod
4247
def get_ed25519_key(cls, key: str, passphrase: Optional[str]) -> ECC.EccKey:
4348
key_data = cls._load_private_key_data(key)
@@ -56,6 +61,11 @@ def get_ed25519_signer(cls, key: str, passphrase: Optional[str]) -> object:
5661
cls._ed25519_signers[cache_key] = eddsa.new(ed_key, "rfc8032")
5762
return cls._ed25519_signers[cache_key]
5863

64+
@classmethod
65+
def clear_ed25519_cache(cls):
66+
cls._ed25519_keys.clear()
67+
cls._ed25519_signers.clear()
68+
5969
@classmethod
6070
def get_signer(
6171
cls, private_key: str, passphrase: Optional[str] = None

common/src/binance_common/utils.py

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hmac
44
import importlib
55
import json
6+
import logging
67
import re
78
import requests
89
import requests.adapters
@@ -48,7 +49,7 @@
4849
class CustomHTTPSAdapter(requests.adapters.HTTPAdapter):
4950
"""A custom HTTPS adapter that supports SSLContext (including certificate pinning)."""
5051

51-
def __init__(self, ssl_context: ssl.SSLContext = None, **kwargs):
52+
def __init__(self, ssl_context: Optional[ssl.SSLContext] = None, **kwargs):
5253
self.ssl_context = ssl_context or create_urllib3_context()
5354
super().__init__(**kwargs)
5455

@@ -262,7 +263,9 @@ def get_signature(
262263
)
263264

264265

265-
def should_retry_request(error, method: str = None, retries_left: int = None) -> bool:
266+
def should_retry_request(
267+
error, method: Optional[str] = None, retries_left: Optional[int] = None
268+
) -> bool:
266269
"""Determines whether a request should be retried based on the error.
267270
268271
:param error: The error object to check.
@@ -295,14 +298,15 @@ def send_request(
295298
payload: Optional[dict] = None,
296299
body: Optional[dict] = None,
297300
time_unit: Optional[str] = None,
298-
response_model: Type[T] = None,
301+
response_model: Optional[Type[T]] = None,
299302
is_signed: bool = False,
300303
signer: Optional[Signers] = None,
301304
) -> ApiResponse[T]:
302305
"""Sends an HTTP request with the specified configuration, method, path, and
303306
optional payload and time unit.
304307
305-
The `send_request` function is responsible for sending an HTTP request with the provided parameters. It handles retries, error handling, and response processing. The function takes the following parameters:
308+
The `send_request` function is responsible for sending an HTTP request with the provided parameters.
309+
It handles retries, error handling, and response processing. The function takes the following parameters:
306310
307311
- `configuration`: The configuration object containing the necessary information for sending the request.
308312
- `method`: The HTTP method to use (e.g. "GET", "POST", etc.).
@@ -424,17 +428,27 @@ def send_request(
424428
or (not isinstance(parsed[0], list) if is_list else False)
425429
)
426430
if (is_list and not is_flat_list) or not response_model:
427-
data_function = lambda: parsed
431+
432+
def data_function():
433+
return parsed
434+
428435
elif is_oneof or is_list or hasattr(response_model, "from_dict"):
429-
data_function = lambda: response_model.from_dict(parsed)
436+
437+
def data_function():
438+
return response_model.from_dict(parsed)
439+
430440
else:
431-
data_function = lambda: response_model.model_validate(parsed)
441+
442+
def data_function():
443+
return response_model.model_validate(parsed)
432444

433445
try:
434446
data_function()
435447
final_data_function = data_function
436448
except Exception:
437-
final_data_function = lambda: parsed
449+
450+
def final_data_function():
451+
return parsed
438452

439453
return ApiResponse[T](
440454
data_function=final_data_function,
@@ -558,16 +572,19 @@ def parse_proxies(
558572
return {"https": proxy_url, "http": proxy_url}
559573

560574

561-
def ws_streams_placeholder(stream: str, params: dict = {}) -> str:
575+
def ws_streams_placeholder(stream: str, params: Optional[dict] = None) -> str:
562576
"""Replaces placeholders in the stream string with values from the params dictionary.
563577
564578
Args:
565579
stream (str): The stream string with placeholders.
566-
params (dict): A dictionary containing values to replace the placeholders.
580+
params (Optional[dict]): A dictionary containing values to replace the placeholders. Defaults to None.
567581
568582
Returns:
569583
str: The stream string with placeholders replaced by actual values.
570584
"""
585+
if params is None:
586+
params = {}
587+
571588
params = {k: v for k, v in params.items() if v is not None}
572589

573590
normalized_variables = {
@@ -698,17 +715,19 @@ def ws_api_payload(config, payload: Dict, websocket_options: WebsocketApiOptions
698715

699716

700717
def websocket_api_signature(
701-
config, payload: Optional[Dict] = {}, signer: Signers = None
718+
config, payload: Optional[Dict] = None, signer: Optional[Signers] = None
702719
) -> dict:
703720
"""Generate signature for websocket API
704721
705722
Args:
706723
payload (Optional[Dict]): Payload.
707-
signer (Signers): Signer for the payload.
724+
signer (Optional[Signers]): Signer for the payload.
708725
Returns:
709726
dict: The generated signature for the WebSocket API.
710727
"""
711728

729+
if payload is None:
730+
payload = {}
712731
payload["apiKey"] = config.api_key
713732
payload["timestamp"] = get_timestamp()
714733
parameters = OrderedDict(sorted(payload.items()))
@@ -803,5 +822,31 @@ def parse_user_event(payload: dict, response_model_cls: Type[BaseModel]) -> Base
803822
return response_model_cls(**kwargs)
804823

805824
except Exception as e:
806-
print(f"Failed to parse {event_name}: {e}")
825+
logging.warning(f"Failed to parse {event_name}: {e}")
807826
return response_model_cls(actual_instance=payload)
827+
828+
829+
def redact_sensitive_info(config: dict) -> dict:
830+
"""Redacts sensitive information from the configuration for logging purposes.
831+
832+
Args:
833+
config (Union[ConfigurationWebSocketAPI, ConfigurationRestAPI]): The configuration object to redact.
834+
Returns:
835+
dict: A dictionary representation of the configuration with sensitive information redacted.
836+
"""
837+
redacted_config = copy.deepcopy(config)
838+
SENSITIVE_KEYS = ["apiKey", "signature"]
839+
840+
if isinstance(redacted_config, dict) or isinstance(redacted_config, OrderedDict):
841+
for key, value in redacted_config.items():
842+
if key in SENSITIVE_KEYS:
843+
redacted_config[key] = "[REDACTED]"
844+
else:
845+
if isinstance(value, (dict, OrderedDict, list)):
846+
redacted_config[key] = redact_sensitive_info(value)
847+
elif isinstance(redacted_config, list):
848+
for i, item in enumerate(redacted_config):
849+
if isinstance(item, (dict, OrderedDict, list)):
850+
redacted_config[i] = redact_sensitive_info(item)
851+
852+
return redacted_config

0 commit comments

Comments
 (0)