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.
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/CHANGELOG.md +14 -0
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/PKG-INFO +5 -5
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/crypto_ws_api/__init__.py +2 -2
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/crypto_ws_api/demo.py +2 -2
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/crypto_ws_api/ws_session.py +144 -97
- {crypto_ws_api-2.1.0 → 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.0/requirements.txt +0 -6
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/.deepsource.toml +0 -0
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/LICENSE.md +0 -0
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/README.md +0 -0
- {crypto_ws_api-2.1.0 → crypto_ws_api-2.1.3}/crypto_ws_api/ws_api.toml.template +0 -0
|
@@ -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.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
19
|
-
Requires-Dist: cryptography~=
|
|
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.
|
|
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,
|
|
@@ -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
|
-
|
|
155
|
-
|
|
156
|
-
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]
|
|
157
179
|
else:
|
|
158
|
-
self.logger = set_logger(
|
|
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
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
579
|
-
for
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|