crypto-ws-api 2.0.20__tar.gz → 2.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,20 @@
1
+ ## 2.1.2 - 2025-08-10
2
+ ♻️ refactor(demo/demo_loop): Store demo_loop coroutines in variables for reference
3
+
4
+ ## 2.1.1 - 2025-08-10
5
+ ♻️ refactor(ws_session): introduce `tasks_manage` for centralized task handling
6
+ ♻️ chore(deps): update platformdirs and cryptography to latest versions
7
+
8
+ ## 2.1.0 - 2025-06-22
9
+ ✨ refactor(ws_session.py): simplify `ws_login` and `request` methods by removing redundant `send_api_key` parameter.
10
+
11
+ ## 2.0.21 - 2025-06-22
12
+ 🔧 fix(README.md): Improve documentation layout for better readability
13
+ 🔧 fix(ws_session.py): Handle `asyncio.CancelledError` gracefully in `start_wss` method
14
+ 🔧 fix(ws_session.py): Replace `asyncio.CancelledError` with `KeyboardInterrupt` exception in `handle_request` method
15
+ 🔧 fix(demo.py): Remove unnecessary `asyncio.CancelledError` handling to clean up error logging
16
+ 🔧 fix(ws_api.toml.template): Update testnet URL for Binance websocket API
17
+
1
18
  ## 2.0.20 - 2025-04-24
2
19
  * On `Binance` now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
3
20
  ✨ feat(ws_session): implement `compose_binance_ws_auth` for Binance WebSocket authentication
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: crypto-ws-api
3
- Version: 2.0.20
3
+ Version: 2.1.2
4
4
  Summary: Crypto WS API connector for ASYNC requests
5
5
  Author-email: Jerry Fedorenko <jerry.fedorenko@yahoo.com>
6
6
  Requires-Python: >=3.10
@@ -11,12 +11,13 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: Unix
12
12
  Classifier: Operating System :: Microsoft :: Windows
13
13
  Classifier: Operating System :: MacOS
14
+ License-File: LICENSE.md
14
15
  Requires-Dist: shortuuid~=1.0.13
15
- Requires-Dist: platformdirs==4.3.6
16
+ Requires-Dist: platformdirs==4.3.8
16
17
  Requires-Dist: toml~=0.10.2
17
18
  Requires-Dist: websockets==15.0.1
18
19
  Requires-Dist: ujson~=5.10.0
19
- Requires-Dist: cryptography~=44.0.2
20
+ Requires-Dist: cryptography~=45.0.6
20
21
  Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
21
22
 
22
23
  <h1 align="center"><img align="center" src="https://user-images.githubusercontent.com/77513676/250364389-cbedc171-a930-4467-a0cd-21627a6a41ed.svg" width="75">Crypto WS API connector for ASYNC requests</h1>
@@ -26,12 +27,13 @@ Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
26
27
  <h3 align="center">Provides of connection management, keepalive and rate limits control</h3>
27
28
 
28
29
  ***
29
- <a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
30
- <a href="https://codeclimate.com/github/DogsTailFarmer/crypto-ws-api/maintainability"><img src="https://api.codeclimate.com/v1/badges/2d2a654ba393eb88d911/maintainability" /></a>
30
+ <h1 align="center"><a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
31
31
  <a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=resolved+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
32
32
  <a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=active+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
33
33
  <a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_crypto-ws-api" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_crypto-ws-api&metric=alert_status"/></a>
34
34
  <a href="https://pepy.tech/project/crypto-ws-api" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/crypto-ws-api"/></a>
35
+ </h1>
36
+
35
37
  ***
36
38
  For :heavy_check_mark:[Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O), :heavy_check_mark:[OKX](https://okx.com/join/2607649), :heavy_check_mark:[Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP), :heavy_check_mark:[HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223)
37
39
  ***
@@ -5,12 +5,13 @@
5
5
  <h3 align="center">Provides of connection management, keepalive and rate limits control</h3>
6
6
 
7
7
  ***
8
- <a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
9
- <a href="https://codeclimate.com/github/DogsTailFarmer/crypto-ws-api/maintainability"><img src="https://api.codeclimate.com/v1/badges/2d2a654ba393eb88d911/maintainability" /></a>
8
+ <h1 align="center"><a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
10
9
  <a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=resolved+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
11
10
  <a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=active+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
12
11
  <a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_crypto-ws-api" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_crypto-ws-api&metric=alert_status"/></a>
13
12
  <a href="https://pepy.tech/project/crypto-ws-api" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/crypto-ws-api"/></a>
13
+ </h1>
14
+
14
15
  ***
15
16
  For :heavy_check_mark:[Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O), :heavy_check_mark:[OKX](https://okx.com/join/2607649), :heavy_check_mark:[Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP), :heavy_check_mark:[HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223)
16
17
  ***
@@ -13,7 +13,7 @@ __maintainer__ = "Jerry Fedorenko"
13
13
  __contact__ = "https://github.com/DogsTailFarmer"
14
14
  __email__ = "jerry.fedorenko@yahoo.com"
15
15
  __credits__ = ["https://github.com/DanyaSWorlD"]
16
- __version__ = "2.0.20"
16
+ __version__ = "2.1.2"
17
17
 
18
18
  from pathlib import Path
19
19
  import shutil
@@ -43,8 +43,8 @@ async def main(account_name):
43
43
 
44
44
  # Demo method's calling
45
45
  await account_information(user_session, trade_id)
46
- asyncio.ensure_future(demo_loop(user_session, get_time, trade_id, 2))
47
- asyncio.ensure_future(demo_loop(user_session, current_average_price, trade_id, 3))
46
+ _t1 = asyncio.ensure_future(demo_loop(user_session, get_time, trade_id, 2))
47
+ _t2 = asyncio.ensure_future(demo_loop(user_session, current_average_price, trade_id, 3))
48
48
 
49
49
  await asyncio.sleep(20)
50
50
 
@@ -66,8 +66,6 @@ async def get_time(user_session: UserWSSession, _trade_id):
66
66
  )
67
67
  if res is None:
68
68
  logger.warning(CONST_1)
69
- except asyncio.CancelledError:
70
- pass # Task cancellation should not be logged as an error
71
69
  except Exception as _ex:
72
70
  logger.error(CONST_2, _ex)
73
71
  else:
@@ -87,8 +85,6 @@ async def current_average_price(user_session: UserWSSession, _trade_id):
87
85
  )
88
86
  if res is None:
89
87
  logger.warning(CONST_1)
90
- except asyncio.CancelledError:
91
- pass # Task cancellation should not be logged as an error
92
88
  except Exception as _ex:
93
89
  logger.error(CONST_2, _ex)
94
90
  else:
@@ -105,8 +101,6 @@ async def account_information(user_session: UserWSSession, _trade_id):
105
101
  )
106
102
  if res is None:
107
103
  logger.warning(CONST_1)
108
- except asyncio.CancelledError:
109
- pass # Task cancellation should not be logged as an error
110
104
  except Exception as _ex:
111
105
  logger.error(CONST_2, _ex)
112
106
  else:
@@ -1,12 +1,12 @@
1
1
  # Parameters for crypto-ws-api connector
2
2
  # Copyright © 2023-2025 Jerry Fedorenko aka VM
3
- # __version__ = "2.0.20"
3
+ # __version__ = "2.0.21"
4
4
 
5
5
  # region endpoint
6
6
  [endpoint]
7
7
  [endpoint.binance]
8
8
  ws_api = 'wss://ws-api.binance.com:443/ws-api/v3'
9
- ws_api_test = 'wss://testnet.binance.vision/ws-api/v3'
9
+ ws_api_test = 'wss://ws-api.testnet.binance.vision:9443/ws-api/v3'
10
10
  # endregion
11
11
 
12
12
  # region Binance accounts
@@ -16,6 +16,7 @@ import random
16
16
  from datetime import datetime, timezone
17
17
  from urllib.parse import urlencode, urlparse
18
18
  from cryptography.hazmat.primitives.serialization import load_pem_private_key
19
+ import inspect
19
20
 
20
21
  from websockets.asyncio.client import connect
21
22
  from websockets import ConnectionClosed
@@ -102,6 +103,13 @@ def compose_htx_ws_auth(endpoint, exchange, api_key, api_secret):
102
103
  "params": _params
103
104
  }
104
105
 
106
+ def tasks_manage(tasks_set: set, coro, name=None, add_done_callback=True):
107
+ _t = asyncio.create_task(coro, name=name or inspect.stack()[1][3])
108
+ tasks_set.add(_t)
109
+ if add_done_callback:
110
+ _t.add_done_callback(tasks_set.discard)
111
+
112
+
105
113
  # https://binance-docs.github.io/apidocs/websocket_api/en/#rate-limits
106
114
  class RateLimitInterval(Enum):
107
115
  SECOND = 1
@@ -159,13 +167,8 @@ class UserWSS:
159
167
  else:
160
168
  self.logger = logger
161
169
 
162
- def tasks_manage(self, coro, name=None):
163
- _t = asyncio.create_task(coro, name=name)
164
- self.tasks.add(_t)
165
- _t.add_done_callback(self.tasks.discard)
166
-
167
- async def _ws_listener(self):
168
- self.tasks_manage(self.ws_login())
170
+ async def _ws_listener(self): #NOSONAR
171
+ tasks_manage(self.tasks, self.ws_login(), f"ws_login-{self.ws_id}")
169
172
  async for msg in self._ws:
170
173
  # logger.info(f"_ws_listener: ws_id: {self.ws_id} msg: {msg}")
171
174
  if isinstance(msg, str):
@@ -191,45 +194,47 @@ class UserWSS:
191
194
  else:
192
195
  logger.warning(f"UserWSS: {self.ws_id}: {msg}")
193
196
  await self.stop()
194
- logger.warning(f"UserWSS: {self.ws_id}: _ws_listener stopped")
195
- await self.stop()
196
197
 
197
198
  async def start_wss(self):
198
- async for self._ws in connect(
199
- self.endpoint,
200
- logger=self.logger,
201
- ping_interval=None if self.exchange in ('binance', 'huobi') else 20
202
- ):
203
- try:
204
- await self._ws_listener()
205
- except ConnectionClosed as ex:
206
- if ex.rcvd and ex.rcvd.code == 4000:
207
- logger.info(f"WSS closed for {self.ws_id}")
208
- break
209
- else:
210
- self.operational_status = False
211
- [task.cancel() for task in self.tasks if not task.done()]
212
- self.tasks.clear()
213
- logger.warning(f"Restart UserWSS for {self.ws_id}")
214
- continue
215
- except Exception as ex:
216
- logger.error(f"UserWSS start other exception: {ex}")
199
+ try:
200
+ async for self._ws in connect(
201
+ self.endpoint,
202
+ logger=self.logger,
203
+ ping_interval=None if self.exchange in ('binance', 'huobi') else 20
204
+ ):
205
+ try:
206
+ await self._ws_listener()
207
+ except ConnectionClosed as ex:
208
+ if ex.rcvd and ex.rcvd.code == 4000:
209
+ logger.info(f"WSS closed for {self.ws_id}")
210
+ break
211
+ else:
212
+ self.operational_status = False
213
+ [task.cancel() for task in self.tasks if not task.done()]
214
+ self.tasks.clear()
215
+ logger.warning(f"Restart UserWSS for {self.ws_id}")
216
+ continue
217
+ except Exception as ex:
218
+ logger.error(f"UserWSS start other exception: {ex}")
219
+ except asyncio.CancelledError:
220
+ await self.stop()
221
+ raise
217
222
 
218
223
  async def ws_login(self):
219
- res = await self.request(CONST_WS_START, send_api_key=True)
224
+ res = await self.request(CONST_WS_START)
220
225
  if res is None:
221
226
  logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
222
227
  await self.stop()
223
228
  else:
224
229
  if self.exchange == 'huobi':
225
- self.tasks_manage(self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
230
+ tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
226
231
 
227
232
  self.operational_status = True
228
233
  self.order_handling = True
229
- self.tasks_manage(self._keepalive(), f"keepalive-{self.ws_id}")
234
+ tasks_manage(self.tasks, self._keepalive(), f"keepalive-{self.ws_id}")
230
235
  logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
231
236
 
232
- async def request(self, method, _params=None, send_api_key=False, _signed=False):
237
+ async def request(self, method, _params=None, _signed=False):
233
238
  """
234
239
  Construct and handling request/response to WS API endpoint, use a description of the methods on
235
240
  https://developers.binance.com/docs/binance-spot-api-docs/websocket-api
@@ -245,13 +250,13 @@ class UserWSS:
245
250
  logger.warning("UserWSS: exceeded order placement limit, try later")
246
251
  return None
247
252
  params = _params.copy() if _params else None
248
- r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}"
253
+ r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}" #NOSONAR
249
254
  if self.exchange in ("okx", "bitfinex", "huobi") and method == CONST_WS_START:
250
255
  _id = self.ws_id
251
256
  else:
252
257
  _id = ''.join(e for e in r_id if e.isalnum())[-ID_LEN_LIMIT.get(self.exchange, 64):]
253
258
  await self._ws.send(
254
- json.dumps(self.compose_request(_id, send_api_key, method, params, _signed))
259
+ json.dumps(self.compose_request(_id, method, params, _signed))
255
260
  )
256
261
  await asyncio.sleep(0)
257
262
  try:
@@ -260,8 +265,6 @@ class UserWSS:
260
265
  logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
261
266
  await self.stop()
262
267
  return None
263
- except asyncio.CancelledError:
264
- return None
265
268
  else:
266
269
  return res
267
270
 
@@ -275,7 +278,7 @@ class UserWSS:
275
278
  return self._response_pool.pop(f"NoneResponse{self.ws_id}", None)
276
279
  return None
277
280
 
278
- def compose_request(self, _id, send_api_key, method, params, signed):
281
+ def compose_request(self, _id, method, params, signed):
279
282
  if self.exchange == "binance":
280
283
  return self._compose_binance_request(_id, method, params, signed)
281
284
  elif self.exchange == "okx":
@@ -400,9 +403,10 @@ class UserWSS:
400
403
  if msg.get('code') == 200 or msg.get('status') == 'ok':
401
404
  return msg
402
405
 
403
- await self.htx_error_handle(msg)
406
+ self.htx_error_handle(msg)
407
+ return None
404
408
 
405
- async def htx_error_handle(self, msg):
409
+ def htx_error_handle(self, msg):
406
410
  if msg.get('code') == 500:
407
411
  logger.warning(f"An issue occurred on exchange's side: {msg}")
408
412
  elif msg.get('code') in {429, 4000}:
@@ -526,7 +530,6 @@ class UserWSSession:
526
530
  trade_id: str,
527
531
  method: str,
528
532
  _params=None,
529
- send_api_key=False,
530
533
  _signed=False,
531
534
  ):
532
535
 
@@ -553,9 +556,7 @@ class UserWSSession:
553
556
  if user_wss.init:
554
557
  user_wss.init = False
555
558
  user_wss.operational_status = False
556
- _t = asyncio.create_task(user_wss.start_wss())
557
- self.tasks_wss.add(_t)
558
- _t.add_done_callback(self.tasks_wss.discard)
559
+ tasks_manage(self.tasks_wss, user_wss.start_wss(), f"start_wss-{ws_id}")
559
560
 
560
561
  duration = 0
561
562
  while not (user_wss.operational_status and user_wss.order_handling):
@@ -566,8 +567,8 @@ class UserWSSession:
566
567
  duration += DELAY
567
568
 
568
569
  try:
569
- return await user_wss.request(method=method, _params=_params, send_api_key=send_api_key, _signed=_signed)
570
- except asyncio.CancelledError:
570
+ return await user_wss.request(method=method, _params=_params, _signed=_signed)
571
+ except KeyboardInterrupt:
571
572
  pass # Task cancellation should not be logged as an error
572
573
  except Exception as ex:
573
574
  logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
@@ -18,11 +18,11 @@ requires-python = ">=3.10"
18
18
 
19
19
  dependencies = [
20
20
  "shortuuid~=1.0.13",
21
- "platformdirs==4.3.6",
21
+ "platformdirs==4.3.8",
22
22
  "toml~=0.10.2",
23
23
  "websockets==15.0.1",
24
24
  "ujson~=5.10.0",
25
- "cryptography~=44.0.2"
25
+ "cryptography~=45.0.6"
26
26
  ]
27
27
 
28
28
  [tool.flit.module]
@@ -1,6 +1,6 @@
1
1
  shortuuid~=1.0.13
2
- platformdirs==4.3.6
2
+ platformdirs==4.3.8
3
3
  toml~=0.10.2
4
4
  websockets==15.0.1
5
5
  ujson~=5.10.0
6
- cryptography~=44.0.2
6
+ cryptography~=45.0.6
File without changes