crypto-ws-api 2.1.0__tar.gz → 2.1.3__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,17 @@
1
+ ## 2.1.3 - 2025-10-16
2
+ ✨ feat(ws_session.py): Add `tasks_cancel()` for structured task cancellation with logging
3
+ ♻️ refactor(ws_session.py): Refactor task cancellation and stop logic to use `tasks_cancel()`
4
+ 🔧 fix(ws_session.py): Update logger references for consistent logging throughout the module
5
+ 🔧 chore(requirements.txt): Upgrade `platformdirs` to 4.5.0, `ujson` to 5.11.0, and `cryptography` to 46.0.3
6
+ 🔧 fix(ws_session.py): _improve memory usage_
7
+
8
+ ## 2.1.2 - 2025-08-10
9
+ ♻️ refactor(demo/demo_loop): Store demo_loop coroutines in variables for reference
10
+
11
+ ## 2.1.1 - 2025-08-10
12
+ ♻️ refactor(ws_session): introduce `tasks_manage` for centralized task handling
13
+ ♻️ chore(deps): update platformdirs and cryptography to latest versions
14
+
1
15
  ## 2.1.0 - 2025-06-22
2
16
  ✨ refactor(ws_session.py): simplify `ws_login` and `request` methods by removing redundant `send_api_key` parameter.
3
17
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: crypto-ws-api
3
- Version: 2.1.0
3
+ Version: 2.1.3
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
@@ -12,11 +12,11 @@ Classifier: Operating System :: Unix
12
12
  Classifier: Operating System :: Microsoft :: Windows
13
13
  Classifier: Operating System :: MacOS
14
14
  Requires-Dist: shortuuid~=1.0.13
15
- Requires-Dist: platformdirs==4.3.6
15
+ Requires-Dist: platformdirs==4.5.0
16
16
  Requires-Dist: toml~=0.10.2
17
17
  Requires-Dist: websockets==15.0.1
18
- Requires-Dist: ujson~=5.10.0
19
- Requires-Dist: cryptography~=44.0.2
18
+ Requires-Dist: ujson~=5.11.0
19
+ Requires-Dist: cryptography~=46.0.3
20
20
  Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
21
21
 
22
22
  <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>
@@ -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.1.0"
16
+ __version__ = "2.1.3"
17
17
 
18
18
  from pathlib import Path
19
19
  import shutil
@@ -22,7 +22,7 @@ from platformdirs import user_config_path
22
22
  VERSION = __version__
23
23
  TIMEOUT = 5 # sec timeout for WSS initialization and get response
24
24
  DELAY = 0.1 # sec delay in keepalive loop
25
- DEBUG_LOG = 'debug' # The name of the exchange for which log files, separated by trade_id with DEBUG level, will be generated
25
+ DEBUG_LOG = 'debug' # The exchange for which log files, separated by trade_id with DEBUG level, will be generated
26
26
  # Maximum str size for unique query ID
27
27
  ID_LEN_LIMIT = {
28
28
  "binance": 36,
@@ -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
 
@@ -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
@@ -74,7 +75,7 @@ def generate_signature(exchange: str, secret: str, data: str) -> str:
74
75
  return signature.hexdigest()
75
76
 
76
77
  def compose_binance_ws_auth(_id: str, api_key: str, api_secret: str) -> dict:
77
- req = {"id": _id, "method": "session.logon"}
78
+ req: dict[str, str | dict[str, str | int]] = {"id": _id, "method": "session.logon"}
78
79
  params = {
79
80
  "apiKey": api_key,
80
81
  "timestamp": int(time.time() * 1000)
@@ -102,6 +103,25 @@ 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
+ async def tasks_cancel(tasks_set: set, _logger=logger):
113
+ tasks = tasks_set.copy()
114
+ for task in tasks:
115
+ task.cancel()
116
+ flag = None
117
+ try:
118
+ await task
119
+ except asyncio.CancelledError: # NOSONAR
120
+ flag = True
121
+ finally:
122
+ _logger.info(f"The task {task.get_name()} was cancelled {'by force' if flag else ''}")
123
+ tasks_set.clear()
124
+
105
125
  # https://binance-docs.github.io/apidocs/websocket_api/en/#rate-limits
106
126
  class RateLimitInterval(Enum):
107
127
  SECOND = 1
@@ -128,7 +148,8 @@ class UserWSS:
128
148
  "_response_pool",
129
149
  "tasks",
130
150
  "ping",
131
- "logger"
151
+ "logger",
152
+ "stopped"
132
153
  )
133
154
 
134
155
  def __init__(self, ws_id, exchange, endpoint, api_key, api_secret, passphrase=None, trade_id=None):
@@ -149,33 +170,27 @@ class UserWSS:
149
170
  self.in_event = asyncio.Event()
150
171
  self.tasks = set()
151
172
  self.ping = 0
173
+ self.stopped = False
152
174
 
153
175
  if self.exchange == DEBUG_LOG:
154
- if trade_id in logging.root.manager.loggerDict:
155
- logger.info(f"Logger {trade_id} already exists")
156
- self.logger = logging.root.manager.loggerDict[trade_id]
176
+ log_key = f"ws_{trade_id}"
177
+ if log_key in logging.root.manager.loggerDict:
178
+ self.logger = logging.root.manager.loggerDict[log_key]
157
179
  else:
158
- self.logger = set_logger(trade_id, Path(LOG_PATH, f"ws_{trade_id}.log"), logging.DEBUG)
180
+ self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
159
181
  else:
160
182
  self.logger = logger
161
183
 
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())
184
+ async def _ws_listener(self): # NOSONAR(S3776)
185
+ tasks_manage(self.tasks, self.ws_login(), f"ws_login-{self.ws_id}")
169
186
  async for msg in self._ws:
170
- # logger.info(f"_ws_listener: ws_id: {self.ws_id} msg: {msg}")
171
187
  if isinstance(msg, str):
172
188
  try:
173
189
  _msg = json.loads(msg)
174
190
  except JSONDecodeError:
175
- logger.warning(f"UserWSS: {self.ws_id}: {msg}")
191
+ self.logger.warning(f"UserWSS JSONDecodeError: {self.ws_id}: {msg}")
176
192
  else:
177
193
  res = await self._handle_msg(_msg)
178
- # logger.info(f"_ws_listener: res: {self.ws_id}: {res}")
179
194
  if res != 'pass':
180
195
  if res is None:
181
196
  self._response_pool[f"NoneResponse{self.ws_id}"] = None
@@ -186,50 +201,70 @@ class UserWSS:
186
201
  elif self.exchange == 'huobi':
187
202
  self._response_pool[res.get('cid') or self.ws_id] = res.get('data')
188
203
  self.in_event.set()
189
- # logger.info(f"_ws_listener: _response_pool: {self._response_pool}")
190
204
  await asyncio.sleep(0)
191
205
  else:
192
- logger.warning(f"UserWSS: {self.ws_id}: {msg}")
193
- await self.stop()
206
+ self.logger.warning(f"UserWSS: {self.ws_id}: {msg}")
207
+ break
208
+ self.logger.info(f"UserWSS listener loop for {self.ws_id} stopped")
209
+
210
+ async def stop(self):
211
+ """
212
+ Stop data stream
213
+ """
214
+ if not self.stopped:
215
+ self.stopped = True
216
+ self.operational_status = None # Not restart and break all loops
217
+ self.order_handling = False
218
+ if self._ws:
219
+ await self._ws.close(code=4000)
220
+ self._ws = None
221
+ await self.cycle_init()
222
+ self.logger.info(f"User WSS for {self.ws_id} stopped")
223
+
224
+ async def cycle_init(self):
225
+ await tasks_cancel(self.tasks, _logger=self.logger)
226
+ self._response_pool.clear()
227
+ gc.collect()
194
228
 
195
229
  async def start_wss(self):
196
- try:
197
- async for self._ws in connect(
198
- self.endpoint,
199
- logger=self.logger,
200
- ping_interval=None if self.exchange in ('binance', 'huobi') else 20
201
- ):
202
- try:
203
- await self._ws_listener()
204
- except ConnectionClosed as ex:
205
- if ex.rcvd and ex.rcvd.code == 4000:
206
- logger.info(f"WSS closed for {self.ws_id}")
207
- break
208
- else:
209
- self.operational_status = False
210
- [task.cancel() for task in self.tasks if not task.done()]
211
- self.tasks.clear()
212
- logger.warning(f"Restart UserWSS for {self.ws_id}")
213
- continue
214
- except Exception as ex:
215
- logger.error(f"UserWSS start other exception: {ex}")
216
- except asyncio.CancelledError:
217
- await self.stop()
218
- raise
230
+ async for self._ws in connect(
231
+ self.endpoint,
232
+ logger=self.logger,
233
+ ping_interval=None if self.exchange in ('binance', 'huobi') else 20
234
+ ):
235
+ try:
236
+ await self._ws_listener()
237
+ except ConnectionClosed as ex:
238
+ self._ws = None
239
+ if ex.rcvd and ex.rcvd.code == 4000:
240
+ self.logger.info(f"WSS closed for {self.ws_id}")
241
+ break
242
+ else:
243
+ self.operational_status = False
244
+ await self.cycle_init()
245
+ self.logger.warning(f"Restart UserWSS for {self.ws_id}")
246
+ continue
247
+ except Exception as ex:
248
+ self.logger.error(f"UserWSS start other exception: {ex}")
249
+ break
250
+ break
251
+ self.logger.info(f"UserWSS start loop for {self.ws_id} stopped")
252
+ await self.stop()
219
253
 
220
254
  async def ws_login(self):
221
255
  res = await self.request(CONST_WS_START)
222
256
  if res is None:
223
- logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
257
+ self.logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
224
258
  await self.stop()
225
259
  else:
226
- if self.exchange == 'huobi':
227
- self.tasks_manage(self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
228
-
229
260
  self.operational_status = True
230
261
  self.order_handling = True
231
- self.tasks_manage(self._keepalive(), f"keepalive-{self.ws_id}")
232
- logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
262
+
263
+ if self.exchange == 'huobi':
264
+ tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
265
+
266
+ tasks_manage(self.tasks, self._keepalive(), f"keepalive-{self.ws_id}")
267
+ self.logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
233
268
 
234
269
  async def request(self, method, _params=None, _signed=False):
235
270
  """
@@ -238,16 +273,16 @@ class UserWSS:
238
273
  :return: result: {} or None if temporary Out-of-Service state
239
274
  """
240
275
  if self.request_limit_reached:
241
- logger.warning(f"UserWSS {self.ws_id}: request limit reached, try later")
276
+ self.logger.warning(f"UserWSS {self.ws_id}: request limit reached, try later")
242
277
  return None
243
278
  if method != CONST_WS_START and not self.operational_status:
244
- logger.warning("UserWSS temporary in Out-of-Service state")
279
+ self.logger.warning("UserWSS temporary in Out-of-Service state")
245
280
  return None
246
281
  if method in ('order.place', 'order.cancelReplace', 'order', 'create-order') and not self.order_handling:
247
- logger.warning("UserWSS: exceeded order placement limit, try later")
282
+ self.logger.warning("UserWSS: exceeded order placement limit, try later")
248
283
  return None
249
284
  params = _params.copy() if _params else None
250
- r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}"
285
+ r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}" #NOSONAR
251
286
  if self.exchange in ("okx", "bitfinex", "huobi") and method == CONST_WS_START:
252
287
  _id = self.ws_id
253
288
  else:
@@ -259,7 +294,7 @@ class UserWSS:
259
294
  try:
260
295
  res = await asyncio.wait_for(self._response_distributor(_id), timeout=TIMEOUT)
261
296
  except asyncio.exceptions.TimeoutError:
262
- logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
297
+ self.logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
263
298
  await self.stop()
264
299
  return None
265
300
  else:
@@ -340,37 +375,24 @@ class UserWSS:
340
375
  while self.operational_status is not None:
341
376
  if self.request_limit_reached and (int(time.time() * 1000) - self._retry_after >= 0):
342
377
  self.request_limit_reached = False
343
- logger.info(f"UserWSS: request limit reached restored for {self.ws_id}")
378
+ self.logger.info(f"UserWSS: request limit reached restored for {self.ws_id}")
344
379
  if not self.order_handling and (int(time.time() * 1000) - self._retry_after >= 0):
345
380
  self.order_handling = True
346
- logger.info(f"UserWSS order handling status restored for {self.ws_id}")
381
+ self.logger.info(f"UserWSS order handling status restored for {self.ws_id}")
347
382
  await asyncio.sleep(interval)
348
383
 
349
384
  async def htx_keepalive(self, interval=60):
350
385
  await asyncio.sleep(interval * 10)
351
- while True:
386
+ while self.operational_status is not None:
352
387
  await asyncio.sleep(interval)
353
388
  if self.ping:
389
+ self.logger.warning("From HTX server PING timeout exceeded")
354
390
  break
355
391
  else:
356
392
  self.ping = 1
357
- logger.warning("From HTX server PING timeout exceeded")
393
+ self.logger.info(f"UserWSS htx_keepalive loop for {self.ws_id} stopped")
358
394
  await self.stop()
359
395
 
360
- async def stop(self):
361
- """
362
- Stop data stream
363
- """
364
- self.operational_status = None # Not restart and break all loops
365
- self.order_handling = False
366
- self.init = True
367
- [task.cancel() for task in self.tasks if not task.done()]
368
- self.tasks.clear()
369
- if self._ws:
370
- await self._ws.close(code=4000)
371
- gc.collect()
372
- logger.info(f"User WSS for {self.ws_id} stopped")
373
-
374
396
  async def _handle_msg(self, msg):
375
397
  if self.exchange == 'binance':
376
398
  self._handle_rate_limits(msg.pop('rateLimits', []))
@@ -405,13 +427,13 @@ class UserWSS:
405
427
 
406
428
  def htx_error_handle(self, msg):
407
429
  if msg.get('code') == 500:
408
- logger.warning(f"An issue occurred on exchange's side: {msg}")
430
+ self.logger.warning(f"An issue occurred on exchange's side: {msg}")
409
431
  elif msg.get('code') in {429, 4000}:
410
432
  self._retry_after = int((time.time() + TIMEOUT) * 1000)
411
433
  self.request_limit_reached = True
412
- logger.warning(f"HTX WSS exceed limit: {msg}")
434
+ self.logger.warning(f"HTX WSS exceed limit: {msg}")
413
435
  else:
414
- logger.warning(f"Malformed request: status: {msg}")
436
+ self.logger.warning(f"Malformed request: status: {msg}")
415
437
 
416
438
  # region BitfinexErrorHandle
417
439
  async def bitfinex_error_handle(self, msg):
@@ -434,20 +456,20 @@ class UserWSS:
434
456
 
435
457
  async def _handle_info_event(self, msg):
436
458
  if not msg.get('platform', {}).get('status'):
437
- logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
459
+ self.logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
438
460
  await self.stop()
439
461
  elif msg.get('version') != 2:
440
- logger.critical('Bitfinex WSS platform: version change detected')
462
+ self.logger.critical('Bitfinex WSS platform: version change detected')
441
463
  return 'pass'
442
464
 
443
465
  def _handle_error_code(self, msg):
444
466
  code = msg.get('code')
445
467
  if code == 10305:
446
- logger.warning('UserWSS Bitfinex: Reached limit of open channels')
468
+ self.logger.warning('UserWSS Bitfinex: Reached limit of open channels')
447
469
  self._retry_after = int((time.time() + TIMEOUT) * 1000)
448
470
  self.request_limit_reached = True
449
471
  else:
450
- logger.warning(f"Malformed request for {self.ws_id}: {msg}")
472
+ self.logger.warning(f"Malformed request for {self.ws_id}: {msg}")
451
473
 
452
474
  @staticmethod
453
475
  def _transform_list_message(msg):
@@ -468,18 +490,18 @@ class UserWSS:
468
490
 
469
491
  def okx_error_handle(self, msg):
470
492
  if msg.get('code') == '1':
471
- logger.warning(f"OKX User WSS operation failed: {msg}")
493
+ self.logger.warning(f"OKX User WSS operation failed: {msg}")
472
494
  elif msg.get('code') == '63999':
473
- logger.warning(f"An issue occurred on exchange's side: {msg}")
495
+ self.logger.warning(f"An issue occurred on exchange's side: {msg}")
474
496
  elif msg.get('code') == '60014':
475
497
  self._retry_after = int((time.time() + TIMEOUT) * 1000)
476
498
  self.request_limit_reached = True
477
499
  else:
478
- logger.warning(f"Malformed request: status: {msg}")
500
+ self.logger.warning(f"Malformed request: status: {msg}")
479
501
 
480
502
  async def binance_error_handle(self, msg):
481
503
  error_msg = msg.get('error')
482
- logger.error(f"Malformed request: status: {error_msg}")
504
+ self.logger.error(f"Malformed request: status: {error_msg}")
483
505
  if msg.get('status') in (401, 403):
484
506
  await self.stop()
485
507
  if msg.get('status') in (418, 429):
@@ -508,6 +530,7 @@ class UserWSSession:
508
530
  "_passphrase",
509
531
  "user_wss",
510
532
  "tasks_wss",
533
+ "logger",
511
534
  )
512
535
 
513
536
  def __init__(self, exchange, endpoint, api_key, api_secret, passphrase=None):
@@ -521,8 +544,9 @@ class UserWSSession:
521
544
  self._passphrase = passphrase
522
545
  self.user_wss = {}
523
546
  self.tasks_wss = set()
547
+ self.logger = logger
524
548
 
525
- async def handle_request(
549
+ async def handle_request( # NOSONAR(S3776)
526
550
  self,
527
551
  trade_id: str,
528
552
  method: str,
@@ -530,15 +554,29 @@ class UserWSSession:
530
554
  _signed=False,
531
555
  ):
532
556
 
557
+ if self.exchange == DEBUG_LOG:
558
+ log_key = f"ws_{trade_id}"
559
+ if log_key in logging.root.manager.loggerDict:
560
+ self.logger = logging.root.manager.loggerDict[log_key]
561
+ else:
562
+ self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
563
+
533
564
  ws_id = f"{self.exchange}-{trade_id}"
534
565
  if self.exchange in ('binance', 'vertex'):
535
566
  ws_id = f"{ws_id}{'_ST' if _signed else '_SF'}"
536
567
  else:
537
568
  ws_id = f"{ws_id}-{method}"
538
569
 
570
+
571
+ init_session = True
539
572
  if ws_id in self.user_wss:
540
573
  user_wss = self.user_wss[ws_id]
541
- else:
574
+ if user_wss.stopped:
575
+ await self.stop(ws_id)
576
+ else:
577
+ init_session = False
578
+
579
+ if init_session:
542
580
  user_wss = UserWSS(
543
581
  ws_id,
544
582
  self.exchange,
@@ -550,18 +588,17 @@ class UserWSSession:
550
588
  )
551
589
  self.user_wss[ws_id] = user_wss
552
590
 
591
+ # noinspection PyUnboundLocalVariable
553
592
  if user_wss.init:
554
593
  user_wss.init = False
555
594
  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)
595
+ tasks_manage(self.tasks_wss, user_wss.start_wss(), f"start_wss-{ws_id}")
559
596
 
560
597
  duration = 0
561
598
  while not (user_wss.operational_status and user_wss.order_handling):
562
599
  await asyncio.sleep(DELAY)
563
600
  if duration > TIMEOUT:
564
- logger.warning(f"{trade_id}: Register timeout for method '{method}'")
601
+ self.logger.warning(f"{trade_id}: Register timeout for method '{method}'")
565
602
  return None
566
603
  duration += DELAY
567
604
 
@@ -570,14 +607,24 @@ class UserWSSession:
570
607
  except KeyboardInterrupt:
571
608
  pass # Task cancellation should not be logged as an error
572
609
  except Exception as ex:
573
- logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
574
- logger.warning(f"{trade_id}: {method}: None response")
610
+ self.logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
611
+ self.logger.warning(f"{trade_id}: {method}: None response")
575
612
  return None
576
613
 
577
- async def stop(self):
578
- user_wss_copy = dict(self.user_wss)
579
- for ws in user_wss_copy.values():
580
- await ws.stop()
581
- self.user_wss.clear()
582
- [task.cancel() for task in self.tasks_wss if not task.done()]
583
- self.tasks_wss.clear()
614
+ async def stop(self, _trade_id):
615
+ user_wss_to_stop = []
616
+ for key in list(self.user_wss.keys()): # NOSONAR
617
+ if _trade_id in key:
618
+ user_wss_to_stop.append((key, self.user_wss.pop(key)))
619
+
620
+ # Stop each UserWSS instance and clean up
621
+ await asyncio.gather(*[ws.stop() for _, ws in user_wss_to_stop])
622
+
623
+ # Remove related tasks
624
+ to_cancel = set()
625
+ for task in self.tasks_wss:
626
+ if _trade_id in task.get_name():
627
+ to_cancel.add(task)
628
+
629
+ self.tasks_wss.difference_update(to_cancel)
630
+ await tasks_cancel(to_cancel, _logger=self.logger)
@@ -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.5.0",
22
22
  "toml~=0.10.2",
23
23
  "websockets==15.0.1",
24
- "ujson~=5.10.0",
25
- "cryptography~=44.0.2"
24
+ "ujson~=5.11.0",
25
+ "cryptography~=46.0.3"
26
26
  ]
27
27
 
28
28
  [tool.flit.module]
@@ -0,0 +1,6 @@
1
+ shortuuid~=1.0.13
2
+ platformdirs==4.5.0
3
+ toml~=0.10.2
4
+ websockets==15.0.1
5
+ ujson~=5.11.0
6
+ cryptography~=46.0.3
@@ -1,6 +0,0 @@
1
- shortuuid~=1.0.13
2
- platformdirs==4.3.6
3
- toml~=0.10.2
4
- websockets==15.0.1
5
- ujson~=5.10.0
6
- cryptography~=44.0.2
File without changes
File without changes