crypto-ws-api 2.1.2__tar.gz → 2.1.4__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,16 @@
1
+ ## 2.1.4 - 2025-12-02
2
+ - Add optional root logger configuration to set_logger()
3
+ - Introduce malloc_trim() wrapper for C library memory management
4
+ - Invoke malloc_trim() in WSSession cleanup for memory optimization
5
+ - Update __init__.py and CHANGELOG.md for version 2.1.4
6
+
7
+ ## 2.1.3 - 2025-10-16
8
+ ✨ feat(ws_session.py): Add `tasks_cancel()` for structured task cancellation with logging
9
+ ♻️ refactor(ws_session.py): Refactor task cancellation and stop logic to use `tasks_cancel()`
10
+ 🔧 fix(ws_session.py): Update logger references for consistent logging throughout the module
11
+ 🔧 chore(requirements.txt): Upgrade `platformdirs` to 4.5.0, `ujson` to 5.11.0, and `cryptography` to 46.0.3
12
+ 🔧 fix(ws_session.py): _improve memory usage_
13
+
1
14
  ## 2.1.2 - 2025-08-10
2
15
  ♻️ refactor(demo/demo_loop): Store demo_loop coroutines in variables for reference
3
16
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: crypto-ws-api
3
- Version: 2.1.2
3
+ Version: 2.1.4
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,13 +11,12 @@ 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
15
14
  Requires-Dist: shortuuid~=1.0.13
16
- Requires-Dist: platformdirs==4.3.8
15
+ Requires-Dist: platformdirs==4.5.0
17
16
  Requires-Dist: toml~=0.10.2
18
17
  Requires-Dist: websockets==15.0.1
19
- Requires-Dist: ujson~=5.10.0
20
- Requires-Dist: cryptography~=45.0.6
18
+ Requires-Dist: ujson~=5.11.0
19
+ Requires-Dist: cryptography~=46.0.3
21
20
  Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
22
21
 
23
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.2"
16
+ __version__ = "2.1.4"
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,
@@ -17,6 +17,7 @@ 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
19
  import inspect
20
+ import ctypes, ctypes.util
20
21
 
21
22
  from websockets.asyncio.client import connect
22
23
  from websockets import ConnectionClosed
@@ -31,7 +32,7 @@ sys.tracebacklimit = 0
31
32
  ALPHABET = string.ascii_letters + string.digits
32
33
  CONST_WS_START = "userDataStream.start"
33
34
 
34
- def set_logger(name, log_file, file_level=logging.INFO, propagate=False):
35
+ def set_logger(name, log_file, file_level=logging.INFO, propagate=False, set_root_logger=False):
35
36
  formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
36
37
  #
37
38
  fh = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10)
@@ -47,10 +48,18 @@ def set_logger(name, log_file, file_level=logging.INFO, propagate=False):
47
48
  _logger.addHandler(sh)
48
49
  _logger.propagate = propagate
49
50
 
51
+ if set_root_logger:
52
+ root_logger = logging.getLogger()
53
+ root_logger.setLevel(min([fh.level, sh.level]))
54
+ root_logger.addHandler(fh)
55
+ root_logger.addHandler(sh)
56
+
50
57
  return _logger
51
58
 
52
59
  logger = set_logger(__name__, Path(LOG_PATH, 'ws_session.log'))
53
60
 
61
+ def malloc_trim(trim_type: int = 0):
62
+ ctypes.CDLL(ctypes.util.find_library('c')).malloc_trim(trim_type)
54
63
 
55
64
  def generate_signature(exchange: str, secret: str, data: str) -> str:
56
65
  encoding = "utf-8"
@@ -75,7 +84,7 @@ def generate_signature(exchange: str, secret: str, data: str) -> str:
75
84
  return signature.hexdigest()
76
85
 
77
86
  def compose_binance_ws_auth(_id: str, api_key: str, api_secret: str) -> dict:
78
- req = {"id": _id, "method": "session.logon"}
87
+ req: dict[str, str | dict[str, str | int]] = {"id": _id, "method": "session.logon"}
79
88
  params = {
80
89
  "apiKey": api_key,
81
90
  "timestamp": int(time.time() * 1000)
@@ -109,6 +118,18 @@ def tasks_manage(tasks_set: set, coro, name=None, add_done_callback=True):
109
118
  if add_done_callback:
110
119
  _t.add_done_callback(tasks_set.discard)
111
120
 
121
+ async def tasks_cancel(tasks_set: set, _logger=logger):
122
+ tasks = tasks_set.copy()
123
+ for task in tasks:
124
+ task.cancel()
125
+ flag = None
126
+ try:
127
+ await task
128
+ except asyncio.CancelledError: # NOSONAR
129
+ flag = True
130
+ finally:
131
+ _logger.info(f"The task {task.get_name()} was cancelled {'by force' if flag else ''}")
132
+ tasks_set.clear()
112
133
 
113
134
  # https://binance-docs.github.io/apidocs/websocket_api/en/#rate-limits
114
135
  class RateLimitInterval(Enum):
@@ -136,7 +157,8 @@ class UserWSS:
136
157
  "_response_pool",
137
158
  "tasks",
138
159
  "ping",
139
- "logger"
160
+ "logger",
161
+ "stopped"
140
162
  )
141
163
 
142
164
  def __init__(self, ws_id, exchange, endpoint, api_key, api_secret, passphrase=None, trade_id=None):
@@ -157,28 +179,27 @@ class UserWSS:
157
179
  self.in_event = asyncio.Event()
158
180
  self.tasks = set()
159
181
  self.ping = 0
182
+ self.stopped = False
160
183
 
161
184
  if self.exchange == DEBUG_LOG:
162
- if trade_id in logging.root.manager.loggerDict:
163
- logger.info(f"Logger {trade_id} already exists")
164
- self.logger = logging.root.manager.loggerDict[trade_id]
185
+ log_key = f"ws_{trade_id}"
186
+ if log_key in logging.root.manager.loggerDict:
187
+ self.logger = logging.root.manager.loggerDict[log_key]
165
188
  else:
166
- self.logger = set_logger(trade_id, Path(LOG_PATH, f"ws_{trade_id}.log"), logging.DEBUG)
189
+ self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
167
190
  else:
168
191
  self.logger = logger
169
192
 
170
- async def _ws_listener(self): #NOSONAR
193
+ async def _ws_listener(self): # NOSONAR(S3776)
171
194
  tasks_manage(self.tasks, self.ws_login(), f"ws_login-{self.ws_id}")
172
195
  async for msg in self._ws:
173
- # logger.info(f"_ws_listener: ws_id: {self.ws_id} msg: {msg}")
174
196
  if isinstance(msg, str):
175
197
  try:
176
198
  _msg = json.loads(msg)
177
199
  except JSONDecodeError:
178
- logger.warning(f"UserWSS: {self.ws_id}: {msg}")
200
+ self.logger.warning(f"UserWSS JSONDecodeError: {self.ws_id}: {msg}")
179
201
  else:
180
202
  res = await self._handle_msg(_msg)
181
- # logger.info(f"_ws_listener: res: {self.ws_id}: {res}")
182
203
  if res != 'pass':
183
204
  if res is None:
184
205
  self._response_pool[f"NoneResponse{self.ws_id}"] = None
@@ -189,50 +210,71 @@ class UserWSS:
189
210
  elif self.exchange == 'huobi':
190
211
  self._response_pool[res.get('cid') or self.ws_id] = res.get('data')
191
212
  self.in_event.set()
192
- # logger.info(f"_ws_listener: _response_pool: {self._response_pool}")
193
213
  await asyncio.sleep(0)
194
214
  else:
195
- logger.warning(f"UserWSS: {self.ws_id}: {msg}")
196
- await self.stop()
215
+ self.logger.warning(f"UserWSS: {self.ws_id}: {msg}")
216
+ break
217
+ self.logger.info(f"UserWSS listener loop for {self.ws_id} stopped")
218
+
219
+ async def stop(self):
220
+ """
221
+ Stop data stream
222
+ """
223
+ if not self.stopped:
224
+ self.stopped = True
225
+ self.operational_status = None # Not restart and break all loops
226
+ self.order_handling = False
227
+ if self._ws:
228
+ await self._ws.close(code=4000)
229
+ self._ws = None
230
+ await self.cycle_init()
231
+ self.logger.info(f"User WSS for {self.ws_id} stopped")
232
+
233
+ async def cycle_init(self):
234
+ await tasks_cancel(self.tasks, _logger=self.logger)
235
+ self._response_pool.clear()
236
+ gc.collect()
237
+ malloc_trim()
197
238
 
198
239
  async def start_wss(self):
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
240
+ async for self._ws in connect(
241
+ self.endpoint,
242
+ logger=self.logger,
243
+ ping_interval=None if self.exchange in ('binance', 'huobi') else 20
244
+ ):
245
+ try:
246
+ await self._ws_listener()
247
+ except ConnectionClosed as ex:
248
+ self._ws = None
249
+ if ex.rcvd and ex.rcvd.code == 4000:
250
+ self.logger.info(f"WSS closed for {self.ws_id}")
251
+ break
252
+ else:
253
+ self.operational_status = False
254
+ await self.cycle_init()
255
+ self.logger.warning(f"Restart UserWSS for {self.ws_id}")
256
+ continue
257
+ except Exception as ex:
258
+ self.logger.error(f"UserWSS start other exception: {ex}")
259
+ break
260
+ break
261
+ self.logger.info(f"UserWSS start loop for {self.ws_id} stopped")
262
+ await self.stop()
222
263
 
223
264
  async def ws_login(self):
224
265
  res = await self.request(CONST_WS_START)
225
266
  if res is None:
226
- logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
267
+ self.logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
227
268
  await self.stop()
228
269
  else:
270
+ self.operational_status = True
271
+ self.order_handling = True
272
+
229
273
  if self.exchange == 'huobi':
230
274
  tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
231
275
 
232
- self.operational_status = True
233
- self.order_handling = True
234
276
  tasks_manage(self.tasks, self._keepalive(), f"keepalive-{self.ws_id}")
235
- logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
277
+ self.logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
236
278
 
237
279
  async def request(self, method, _params=None, _signed=False):
238
280
  """
@@ -241,13 +283,13 @@ class UserWSS:
241
283
  :return: result: {} or None if temporary Out-of-Service state
242
284
  """
243
285
  if self.request_limit_reached:
244
- logger.warning(f"UserWSS {self.ws_id}: request limit reached, try later")
286
+ self.logger.warning(f"UserWSS {self.ws_id}: request limit reached, try later")
245
287
  return None
246
288
  if method != CONST_WS_START and not self.operational_status:
247
- logger.warning("UserWSS temporary in Out-of-Service state")
289
+ self.logger.warning("UserWSS temporary in Out-of-Service state")
248
290
  return None
249
291
  if method in ('order.place', 'order.cancelReplace', 'order', 'create-order') and not self.order_handling:
250
- logger.warning("UserWSS: exceeded order placement limit, try later")
292
+ self.logger.warning("UserWSS: exceeded order placement limit, try later")
251
293
  return None
252
294
  params = _params.copy() if _params else None
253
295
  r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}" #NOSONAR
@@ -262,7 +304,7 @@ class UserWSS:
262
304
  try:
263
305
  res = await asyncio.wait_for(self._response_distributor(_id), timeout=TIMEOUT)
264
306
  except asyncio.exceptions.TimeoutError:
265
- logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
307
+ self.logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
266
308
  await self.stop()
267
309
  return None
268
310
  else:
@@ -343,37 +385,24 @@ class UserWSS:
343
385
  while self.operational_status is not None:
344
386
  if self.request_limit_reached and (int(time.time() * 1000) - self._retry_after >= 0):
345
387
  self.request_limit_reached = False
346
- logger.info(f"UserWSS: request limit reached restored for {self.ws_id}")
388
+ self.logger.info(f"UserWSS: request limit reached restored for {self.ws_id}")
347
389
  if not self.order_handling and (int(time.time() * 1000) - self._retry_after >= 0):
348
390
  self.order_handling = True
349
- logger.info(f"UserWSS order handling status restored for {self.ws_id}")
391
+ self.logger.info(f"UserWSS order handling status restored for {self.ws_id}")
350
392
  await asyncio.sleep(interval)
351
393
 
352
394
  async def htx_keepalive(self, interval=60):
353
395
  await asyncio.sleep(interval * 10)
354
- while True:
396
+ while self.operational_status is not None:
355
397
  await asyncio.sleep(interval)
356
398
  if self.ping:
399
+ self.logger.warning("From HTX server PING timeout exceeded")
357
400
  break
358
401
  else:
359
402
  self.ping = 1
360
- logger.warning("From HTX server PING timeout exceeded")
403
+ self.logger.info(f"UserWSS htx_keepalive loop for {self.ws_id} stopped")
361
404
  await self.stop()
362
405
 
363
- async def stop(self):
364
- """
365
- Stop data stream
366
- """
367
- self.operational_status = None # Not restart and break all loops
368
- self.order_handling = False
369
- self.init = True
370
- [task.cancel() for task in self.tasks if not task.done()]
371
- self.tasks.clear()
372
- if self._ws:
373
- await self._ws.close(code=4000)
374
- gc.collect()
375
- logger.info(f"User WSS for {self.ws_id} stopped")
376
-
377
406
  async def _handle_msg(self, msg):
378
407
  if self.exchange == 'binance':
379
408
  self._handle_rate_limits(msg.pop('rateLimits', []))
@@ -408,13 +437,13 @@ class UserWSS:
408
437
 
409
438
  def htx_error_handle(self, msg):
410
439
  if msg.get('code') == 500:
411
- logger.warning(f"An issue occurred on exchange's side: {msg}")
440
+ self.logger.warning(f"An issue occurred on exchange's side: {msg}")
412
441
  elif msg.get('code') in {429, 4000}:
413
442
  self._retry_after = int((time.time() + TIMEOUT) * 1000)
414
443
  self.request_limit_reached = True
415
- logger.warning(f"HTX WSS exceed limit: {msg}")
444
+ self.logger.warning(f"HTX WSS exceed limit: {msg}")
416
445
  else:
417
- logger.warning(f"Malformed request: status: {msg}")
446
+ self.logger.warning(f"Malformed request: status: {msg}")
418
447
 
419
448
  # region BitfinexErrorHandle
420
449
  async def bitfinex_error_handle(self, msg):
@@ -437,20 +466,20 @@ class UserWSS:
437
466
 
438
467
  async def _handle_info_event(self, msg):
439
468
  if not msg.get('platform', {}).get('status'):
440
- logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
469
+ self.logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
441
470
  await self.stop()
442
471
  elif msg.get('version') != 2:
443
- logger.critical('Bitfinex WSS platform: version change detected')
472
+ self.logger.critical('Bitfinex WSS platform: version change detected')
444
473
  return 'pass'
445
474
 
446
475
  def _handle_error_code(self, msg):
447
476
  code = msg.get('code')
448
477
  if code == 10305:
449
- logger.warning('UserWSS Bitfinex: Reached limit of open channels')
478
+ self.logger.warning('UserWSS Bitfinex: Reached limit of open channels')
450
479
  self._retry_after = int((time.time() + TIMEOUT) * 1000)
451
480
  self.request_limit_reached = True
452
481
  else:
453
- logger.warning(f"Malformed request for {self.ws_id}: {msg}")
482
+ self.logger.warning(f"Malformed request for {self.ws_id}: {msg}")
454
483
 
455
484
  @staticmethod
456
485
  def _transform_list_message(msg):
@@ -471,18 +500,18 @@ class UserWSS:
471
500
 
472
501
  def okx_error_handle(self, msg):
473
502
  if msg.get('code') == '1':
474
- logger.warning(f"OKX User WSS operation failed: {msg}")
503
+ self.logger.warning(f"OKX User WSS operation failed: {msg}")
475
504
  elif msg.get('code') == '63999':
476
- logger.warning(f"An issue occurred on exchange's side: {msg}")
505
+ self.logger.warning(f"An issue occurred on exchange's side: {msg}")
477
506
  elif msg.get('code') == '60014':
478
507
  self._retry_after = int((time.time() + TIMEOUT) * 1000)
479
508
  self.request_limit_reached = True
480
509
  else:
481
- logger.warning(f"Malformed request: status: {msg}")
510
+ self.logger.warning(f"Malformed request: status: {msg}")
482
511
 
483
512
  async def binance_error_handle(self, msg):
484
513
  error_msg = msg.get('error')
485
- logger.error(f"Malformed request: status: {error_msg}")
514
+ self.logger.error(f"Malformed request: status: {error_msg}")
486
515
  if msg.get('status') in (401, 403):
487
516
  await self.stop()
488
517
  if msg.get('status') in (418, 429):
@@ -511,6 +540,7 @@ class UserWSSession:
511
540
  "_passphrase",
512
541
  "user_wss",
513
542
  "tasks_wss",
543
+ "logger",
514
544
  )
515
545
 
516
546
  def __init__(self, exchange, endpoint, api_key, api_secret, passphrase=None):
@@ -524,8 +554,9 @@ class UserWSSession:
524
554
  self._passphrase = passphrase
525
555
  self.user_wss = {}
526
556
  self.tasks_wss = set()
557
+ self.logger = logger
527
558
 
528
- async def handle_request(
559
+ async def handle_request( # NOSONAR(S3776)
529
560
  self,
530
561
  trade_id: str,
531
562
  method: str,
@@ -533,15 +564,29 @@ class UserWSSession:
533
564
  _signed=False,
534
565
  ):
535
566
 
567
+ if self.exchange == DEBUG_LOG:
568
+ log_key = f"ws_{trade_id}"
569
+ if log_key in logging.root.manager.loggerDict:
570
+ self.logger = logging.root.manager.loggerDict[log_key]
571
+ else:
572
+ self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
573
+
536
574
  ws_id = f"{self.exchange}-{trade_id}"
537
575
  if self.exchange in ('binance', 'vertex'):
538
576
  ws_id = f"{ws_id}{'_ST' if _signed else '_SF'}"
539
577
  else:
540
578
  ws_id = f"{ws_id}-{method}"
541
579
 
580
+
581
+ init_session = True
542
582
  if ws_id in self.user_wss:
543
583
  user_wss = self.user_wss[ws_id]
544
- else:
584
+ if user_wss.stopped:
585
+ await self.stop(ws_id)
586
+ else:
587
+ init_session = False
588
+
589
+ if init_session:
545
590
  user_wss = UserWSS(
546
591
  ws_id,
547
592
  self.exchange,
@@ -553,6 +598,7 @@ class UserWSSession:
553
598
  )
554
599
  self.user_wss[ws_id] = user_wss
555
600
 
601
+ # noinspection PyUnboundLocalVariable
556
602
  if user_wss.init:
557
603
  user_wss.init = False
558
604
  user_wss.operational_status = False
@@ -562,7 +608,7 @@ class UserWSSession:
562
608
  while not (user_wss.operational_status and user_wss.order_handling):
563
609
  await asyncio.sleep(DELAY)
564
610
  if duration > TIMEOUT:
565
- logger.warning(f"{trade_id}: Register timeout for method '{method}'")
611
+ self.logger.warning(f"{trade_id}: Register timeout for method '{method}'")
566
612
  return None
567
613
  duration += DELAY
568
614
 
@@ -571,14 +617,24 @@ class UserWSSession:
571
617
  except KeyboardInterrupt:
572
618
  pass # Task cancellation should not be logged as an error
573
619
  except Exception as ex:
574
- logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
575
- logger.warning(f"{trade_id}: {method}: None response")
620
+ self.logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
621
+ self.logger.warning(f"{trade_id}: {method}: None response")
576
622
  return None
577
623
 
578
- async def stop(self):
579
- user_wss_copy = dict(self.user_wss)
580
- for ws in user_wss_copy.values():
581
- await ws.stop()
582
- self.user_wss.clear()
583
- [task.cancel() for task in self.tasks_wss if not task.done()]
584
- self.tasks_wss.clear()
624
+ async def stop(self, _trade_id):
625
+ user_wss_to_stop = []
626
+ for key in list(self.user_wss.keys()): # NOSONAR
627
+ if _trade_id in key:
628
+ user_wss_to_stop.append((key, self.user_wss.pop(key)))
629
+
630
+ # Stop each UserWSS instance and clean up
631
+ await asyncio.gather(*[ws.stop() for _, ws in user_wss_to_stop])
632
+
633
+ # Remove related tasks
634
+ to_cancel = set()
635
+ for task in self.tasks_wss:
636
+ if _trade_id in task.get_name():
637
+ to_cancel.add(task)
638
+
639
+ self.tasks_wss.difference_update(to_cancel)
640
+ 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.8",
21
+ "platformdirs==4.5.0",
22
22
  "toml~=0.10.2",
23
23
  "websockets==15.0.1",
24
- "ujson~=5.10.0",
25
- "cryptography~=45.0.6"
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.8
3
- toml~=0.10.2
4
- websockets==15.0.1
5
- ujson~=5.10.0
6
- cryptography~=45.0.6
File without changes
File without changes