crypto-ws-api 2.1.2__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.
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/CHANGELOG.md +7 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/PKG-INFO +5 -6
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/crypto_ws_api/__init__.py +2 -2
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/crypto_ws_api/ws_session.py +131 -85
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/pyproject.toml +3 -3
- crypto_ws_api-2.1.3/requirements.txt +6 -0
- crypto_ws_api-2.1.2/requirements.txt +0 -6
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/.deepsource.toml +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/LICENSE.md +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/README.md +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/crypto_ws_api/demo.py +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.3}/crypto_ws_api/ws_api.toml.template +0 -0
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
## 2.1.2 - 2025-08-10
|
|
2
9
|
♻️ refactor(demo/demo_loop): Store demo_loop coroutines in variables for reference
|
|
3
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.1.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
20
|
-
Requires-Dist: cryptography~=
|
|
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.
|
|
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
|
|
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,
|
|
@@ -75,7 +75,7 @@ def generate_signature(exchange: str, secret: str, data: str) -> str:
|
|
|
75
75
|
return signature.hexdigest()
|
|
76
76
|
|
|
77
77
|
def compose_binance_ws_auth(_id: str, api_key: str, api_secret: str) -> dict:
|
|
78
|
-
req = {"id": _id, "method": "session.logon"}
|
|
78
|
+
req: dict[str, str | dict[str, str | int]] = {"id": _id, "method": "session.logon"}
|
|
79
79
|
params = {
|
|
80
80
|
"apiKey": api_key,
|
|
81
81
|
"timestamp": int(time.time() * 1000)
|
|
@@ -109,6 +109,18 @@ def tasks_manage(tasks_set: set, coro, name=None, add_done_callback=True):
|
|
|
109
109
|
if add_done_callback:
|
|
110
110
|
_t.add_done_callback(tasks_set.discard)
|
|
111
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()
|
|
112
124
|
|
|
113
125
|
# https://binance-docs.github.io/apidocs/websocket_api/en/#rate-limits
|
|
114
126
|
class RateLimitInterval(Enum):
|
|
@@ -136,7 +148,8 @@ class UserWSS:
|
|
|
136
148
|
"_response_pool",
|
|
137
149
|
"tasks",
|
|
138
150
|
"ping",
|
|
139
|
-
"logger"
|
|
151
|
+
"logger",
|
|
152
|
+
"stopped"
|
|
140
153
|
)
|
|
141
154
|
|
|
142
155
|
def __init__(self, ws_id, exchange, endpoint, api_key, api_secret, passphrase=None, trade_id=None):
|
|
@@ -157,28 +170,27 @@ class UserWSS:
|
|
|
157
170
|
self.in_event = asyncio.Event()
|
|
158
171
|
self.tasks = set()
|
|
159
172
|
self.ping = 0
|
|
173
|
+
self.stopped = False
|
|
160
174
|
|
|
161
175
|
if self.exchange == DEBUG_LOG:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self.logger = logging.root.manager.loggerDict[
|
|
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]
|
|
165
179
|
else:
|
|
166
|
-
self.logger = set_logger(
|
|
180
|
+
self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
|
|
167
181
|
else:
|
|
168
182
|
self.logger = logger
|
|
169
183
|
|
|
170
|
-
async def _ws_listener(self): #NOSONAR
|
|
184
|
+
async def _ws_listener(self): # NOSONAR(S3776)
|
|
171
185
|
tasks_manage(self.tasks, self.ws_login(), f"ws_login-{self.ws_id}")
|
|
172
186
|
async for msg in self._ws:
|
|
173
|
-
# logger.info(f"_ws_listener: ws_id: {self.ws_id} msg: {msg}")
|
|
174
187
|
if isinstance(msg, str):
|
|
175
188
|
try:
|
|
176
189
|
_msg = json.loads(msg)
|
|
177
190
|
except JSONDecodeError:
|
|
178
|
-
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
191
|
+
self.logger.warning(f"UserWSS JSONDecodeError: {self.ws_id}: {msg}")
|
|
179
192
|
else:
|
|
180
193
|
res = await self._handle_msg(_msg)
|
|
181
|
-
# logger.info(f"_ws_listener: res: {self.ws_id}: {res}")
|
|
182
194
|
if res != 'pass':
|
|
183
195
|
if res is None:
|
|
184
196
|
self._response_pool[f"NoneResponse{self.ws_id}"] = None
|
|
@@ -189,50 +201,70 @@ class UserWSS:
|
|
|
189
201
|
elif self.exchange == 'huobi':
|
|
190
202
|
self._response_pool[res.get('cid') or self.ws_id] = res.get('data')
|
|
191
203
|
self.in_event.set()
|
|
192
|
-
# logger.info(f"_ws_listener: _response_pool: {self._response_pool}")
|
|
193
204
|
await asyncio.sleep(0)
|
|
194
205
|
else:
|
|
195
|
-
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
196
|
-
|
|
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()
|
|
197
228
|
|
|
198
229
|
async def start_wss(self):
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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()
|
|
222
253
|
|
|
223
254
|
async def ws_login(self):
|
|
224
255
|
res = await self.request(CONST_WS_START)
|
|
225
256
|
if res is None:
|
|
226
|
-
logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
|
|
257
|
+
self.logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
|
|
227
258
|
await self.stop()
|
|
228
259
|
else:
|
|
260
|
+
self.operational_status = True
|
|
261
|
+
self.order_handling = True
|
|
262
|
+
|
|
229
263
|
if self.exchange == 'huobi':
|
|
230
264
|
tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
|
|
231
265
|
|
|
232
|
-
self.operational_status = True
|
|
233
|
-
self.order_handling = True
|
|
234
266
|
tasks_manage(self.tasks, self._keepalive(), f"keepalive-{self.ws_id}")
|
|
235
|
-
logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
|
|
267
|
+
self.logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
|
|
236
268
|
|
|
237
269
|
async def request(self, method, _params=None, _signed=False):
|
|
238
270
|
"""
|
|
@@ -241,13 +273,13 @@ class UserWSS:
|
|
|
241
273
|
:return: result: {} or None if temporary Out-of-Service state
|
|
242
274
|
"""
|
|
243
275
|
if self.request_limit_reached:
|
|
244
|
-
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")
|
|
245
277
|
return None
|
|
246
278
|
if method != CONST_WS_START and not self.operational_status:
|
|
247
|
-
logger.warning("UserWSS temporary in Out-of-Service state")
|
|
279
|
+
self.logger.warning("UserWSS temporary in Out-of-Service state")
|
|
248
280
|
return None
|
|
249
281
|
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")
|
|
282
|
+
self.logger.warning("UserWSS: exceeded order placement limit, try later")
|
|
251
283
|
return None
|
|
252
284
|
params = _params.copy() if _params else None
|
|
253
285
|
r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}" #NOSONAR
|
|
@@ -262,7 +294,7 @@ class UserWSS:
|
|
|
262
294
|
try:
|
|
263
295
|
res = await asyncio.wait_for(self._response_distributor(_id), timeout=TIMEOUT)
|
|
264
296
|
except asyncio.exceptions.TimeoutError:
|
|
265
|
-
logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
|
|
297
|
+
self.logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
|
|
266
298
|
await self.stop()
|
|
267
299
|
return None
|
|
268
300
|
else:
|
|
@@ -343,37 +375,24 @@ class UserWSS:
|
|
|
343
375
|
while self.operational_status is not None:
|
|
344
376
|
if self.request_limit_reached and (int(time.time() * 1000) - self._retry_after >= 0):
|
|
345
377
|
self.request_limit_reached = False
|
|
346
|
-
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}")
|
|
347
379
|
if not self.order_handling and (int(time.time() * 1000) - self._retry_after >= 0):
|
|
348
380
|
self.order_handling = True
|
|
349
|
-
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}")
|
|
350
382
|
await asyncio.sleep(interval)
|
|
351
383
|
|
|
352
384
|
async def htx_keepalive(self, interval=60):
|
|
353
385
|
await asyncio.sleep(interval * 10)
|
|
354
|
-
while
|
|
386
|
+
while self.operational_status is not None:
|
|
355
387
|
await asyncio.sleep(interval)
|
|
356
388
|
if self.ping:
|
|
389
|
+
self.logger.warning("From HTX server PING timeout exceeded")
|
|
357
390
|
break
|
|
358
391
|
else:
|
|
359
392
|
self.ping = 1
|
|
360
|
-
logger.
|
|
393
|
+
self.logger.info(f"UserWSS htx_keepalive loop for {self.ws_id} stopped")
|
|
361
394
|
await self.stop()
|
|
362
395
|
|
|
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
396
|
async def _handle_msg(self, msg):
|
|
378
397
|
if self.exchange == 'binance':
|
|
379
398
|
self._handle_rate_limits(msg.pop('rateLimits', []))
|
|
@@ -408,13 +427,13 @@ class UserWSS:
|
|
|
408
427
|
|
|
409
428
|
def htx_error_handle(self, msg):
|
|
410
429
|
if msg.get('code') == 500:
|
|
411
|
-
logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
430
|
+
self.logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
412
431
|
elif msg.get('code') in {429, 4000}:
|
|
413
432
|
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
414
433
|
self.request_limit_reached = True
|
|
415
|
-
logger.warning(f"HTX WSS exceed limit: {msg}")
|
|
434
|
+
self.logger.warning(f"HTX WSS exceed limit: {msg}")
|
|
416
435
|
else:
|
|
417
|
-
logger.warning(f"Malformed request: status: {msg}")
|
|
436
|
+
self.logger.warning(f"Malformed request: status: {msg}")
|
|
418
437
|
|
|
419
438
|
# region BitfinexErrorHandle
|
|
420
439
|
async def bitfinex_error_handle(self, msg):
|
|
@@ -437,20 +456,20 @@ class UserWSS:
|
|
|
437
456
|
|
|
438
457
|
async def _handle_info_event(self, msg):
|
|
439
458
|
if not msg.get('platform', {}).get('status'):
|
|
440
|
-
logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
|
|
459
|
+
self.logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
|
|
441
460
|
await self.stop()
|
|
442
461
|
elif msg.get('version') != 2:
|
|
443
|
-
logger.critical('Bitfinex WSS platform: version change detected')
|
|
462
|
+
self.logger.critical('Bitfinex WSS platform: version change detected')
|
|
444
463
|
return 'pass'
|
|
445
464
|
|
|
446
465
|
def _handle_error_code(self, msg):
|
|
447
466
|
code = msg.get('code')
|
|
448
467
|
if code == 10305:
|
|
449
|
-
logger.warning('UserWSS Bitfinex: Reached limit of open channels')
|
|
468
|
+
self.logger.warning('UserWSS Bitfinex: Reached limit of open channels')
|
|
450
469
|
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
451
470
|
self.request_limit_reached = True
|
|
452
471
|
else:
|
|
453
|
-
logger.warning(f"Malformed request for {self.ws_id}: {msg}")
|
|
472
|
+
self.logger.warning(f"Malformed request for {self.ws_id}: {msg}")
|
|
454
473
|
|
|
455
474
|
@staticmethod
|
|
456
475
|
def _transform_list_message(msg):
|
|
@@ -471,18 +490,18 @@ class UserWSS:
|
|
|
471
490
|
|
|
472
491
|
def okx_error_handle(self, msg):
|
|
473
492
|
if msg.get('code') == '1':
|
|
474
|
-
logger.warning(f"OKX User WSS operation failed: {msg}")
|
|
493
|
+
self.logger.warning(f"OKX User WSS operation failed: {msg}")
|
|
475
494
|
elif msg.get('code') == '63999':
|
|
476
|
-
logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
495
|
+
self.logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
477
496
|
elif msg.get('code') == '60014':
|
|
478
497
|
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
479
498
|
self.request_limit_reached = True
|
|
480
499
|
else:
|
|
481
|
-
logger.warning(f"Malformed request: status: {msg}")
|
|
500
|
+
self.logger.warning(f"Malformed request: status: {msg}")
|
|
482
501
|
|
|
483
502
|
async def binance_error_handle(self, msg):
|
|
484
503
|
error_msg = msg.get('error')
|
|
485
|
-
logger.error(f"Malformed request: status: {error_msg}")
|
|
504
|
+
self.logger.error(f"Malformed request: status: {error_msg}")
|
|
486
505
|
if msg.get('status') in (401, 403):
|
|
487
506
|
await self.stop()
|
|
488
507
|
if msg.get('status') in (418, 429):
|
|
@@ -511,6 +530,7 @@ class UserWSSession:
|
|
|
511
530
|
"_passphrase",
|
|
512
531
|
"user_wss",
|
|
513
532
|
"tasks_wss",
|
|
533
|
+
"logger",
|
|
514
534
|
)
|
|
515
535
|
|
|
516
536
|
def __init__(self, exchange, endpoint, api_key, api_secret, passphrase=None):
|
|
@@ -524,8 +544,9 @@ class UserWSSession:
|
|
|
524
544
|
self._passphrase = passphrase
|
|
525
545
|
self.user_wss = {}
|
|
526
546
|
self.tasks_wss = set()
|
|
547
|
+
self.logger = logger
|
|
527
548
|
|
|
528
|
-
async def handle_request(
|
|
549
|
+
async def handle_request( # NOSONAR(S3776)
|
|
529
550
|
self,
|
|
530
551
|
trade_id: str,
|
|
531
552
|
method: str,
|
|
@@ -533,15 +554,29 @@ class UserWSSession:
|
|
|
533
554
|
_signed=False,
|
|
534
555
|
):
|
|
535
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
|
+
|
|
536
564
|
ws_id = f"{self.exchange}-{trade_id}"
|
|
537
565
|
if self.exchange in ('binance', 'vertex'):
|
|
538
566
|
ws_id = f"{ws_id}{'_ST' if _signed else '_SF'}"
|
|
539
567
|
else:
|
|
540
568
|
ws_id = f"{ws_id}-{method}"
|
|
541
569
|
|
|
570
|
+
|
|
571
|
+
init_session = True
|
|
542
572
|
if ws_id in self.user_wss:
|
|
543
573
|
user_wss = self.user_wss[ws_id]
|
|
544
|
-
|
|
574
|
+
if user_wss.stopped:
|
|
575
|
+
await self.stop(ws_id)
|
|
576
|
+
else:
|
|
577
|
+
init_session = False
|
|
578
|
+
|
|
579
|
+
if init_session:
|
|
545
580
|
user_wss = UserWSS(
|
|
546
581
|
ws_id,
|
|
547
582
|
self.exchange,
|
|
@@ -553,6 +588,7 @@ class UserWSSession:
|
|
|
553
588
|
)
|
|
554
589
|
self.user_wss[ws_id] = user_wss
|
|
555
590
|
|
|
591
|
+
# noinspection PyUnboundLocalVariable
|
|
556
592
|
if user_wss.init:
|
|
557
593
|
user_wss.init = False
|
|
558
594
|
user_wss.operational_status = False
|
|
@@ -562,7 +598,7 @@ class UserWSSession:
|
|
|
562
598
|
while not (user_wss.operational_status and user_wss.order_handling):
|
|
563
599
|
await asyncio.sleep(DELAY)
|
|
564
600
|
if duration > TIMEOUT:
|
|
565
|
-
logger.warning(f"{trade_id}: Register timeout for method '{method}'")
|
|
601
|
+
self.logger.warning(f"{trade_id}: Register timeout for method '{method}'")
|
|
566
602
|
return None
|
|
567
603
|
duration += DELAY
|
|
568
604
|
|
|
@@ -571,14 +607,24 @@ class UserWSSession:
|
|
|
571
607
|
except KeyboardInterrupt:
|
|
572
608
|
pass # Task cancellation should not be logged as an error
|
|
573
609
|
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")
|
|
610
|
+
self.logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
|
|
611
|
+
self.logger.warning(f"{trade_id}: {method}: None response")
|
|
576
612
|
return None
|
|
577
613
|
|
|
578
|
-
async def stop(self):
|
|
579
|
-
|
|
580
|
-
for
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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.
|
|
21
|
+
"platformdirs==4.5.0",
|
|
22
22
|
"toml~=0.10.2",
|
|
23
23
|
"websockets==15.0.1",
|
|
24
|
-
"ujson~=5.
|
|
25
|
-
"cryptography~=
|
|
24
|
+
"ujson~=5.11.0",
|
|
25
|
+
"cryptography~=46.0.3"
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[tool.flit.module]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|