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.
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/CHANGELOG.md +13 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/PKG-INFO +5 -6
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/crypto_ws_api/__init__.py +2 -2
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/crypto_ws_api/ws_session.py +142 -86
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/pyproject.toml +3 -3
- crypto_ws_api-2.1.4/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.4}/.deepsource.toml +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/LICENSE.md +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/README.md +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/crypto_ws_api/demo.py +0 -0
- {crypto_ws_api-2.1.2 → crypto_ws_api-2.1.4}/crypto_ws_api/ws_api.toml.template +0 -0
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.1.
|
|
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.
|
|
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.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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
self.logger = logging.root.manager.loggerDict[
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
for
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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.
|
|
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
|