crypto-ws-api 2.0.17__tar.gz → 2.0.19__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.0.17 → crypto_ws_api-2.0.19}/CHANGELOG.md +12 -0
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/PKG-INFO +2 -2
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/crypto_ws_api/__init__.py +2 -1
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/crypto_ws_api/ws_session.py +59 -25
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/pyproject.toml +1 -1
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/requirements.txt +1 -1
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/.deepsource.toml +0 -0
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/LICENSE.md +0 -0
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/README.md +0 -0
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/crypto_ws_api/demo.py +0 -0
- {crypto_ws_api-2.0.17 → crypto_ws_api-2.0.19}/crypto_ws_api/ws_api.toml.template +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 2.0.19 - 2025-03-28
|
|
2
|
+
### Fix
|
|
3
|
+
* `ws_session.ws_login`: Incorrect exit from authentication procedure in case of unsuccessful attempt
|
|
4
|
+
|
|
5
|
+
## 2.0.18 - 2025-03-10
|
|
6
|
+
### Fix
|
|
7
|
+
* [🔒] fix/ws_session.py: Proper restoration of WebSocket connection during recurring failures
|
|
8
|
+
|
|
9
|
+
### Update
|
|
10
|
+
* [💻] feat/ws_session.py: Add logging configuration and handlers for debug purpose
|
|
11
|
+
* [🔄] fix/ws_session.py: Update UserWSS class to accept a logger instance
|
|
12
|
+
|
|
1
13
|
## 2.0.17 - 2025-02-20
|
|
2
14
|
### Update
|
|
3
15
|
* Bump dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.19
|
|
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.8
|
|
@@ -14,7 +14,7 @@ Classifier: Operating System :: MacOS
|
|
|
14
14
|
Requires-Dist: shortuuid~=1.0.13
|
|
15
15
|
Requires-Dist: platformdirs==4.3.6
|
|
16
16
|
Requires-Dist: toml~=0.10.2
|
|
17
|
-
Requires-Dist: websockets==15.0
|
|
17
|
+
Requires-Dist: websockets==15.0.1
|
|
18
18
|
Requires-Dist: ujson~=5.10.0
|
|
19
19
|
Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
|
|
20
20
|
|
|
@@ -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.0.
|
|
16
|
+
__version__ = "2.0.19"
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
import shutil
|
|
@@ -22,6 +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
26
|
# Maximum str size for unique query ID
|
|
26
27
|
ID_LEN_LIMIT = {
|
|
27
28
|
"binance": 36,
|
|
@@ -4,7 +4,8 @@ import asyncio
|
|
|
4
4
|
import gc
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
|
-
import logging
|
|
7
|
+
import logging.handlers
|
|
8
|
+
from pathlib import Path
|
|
8
9
|
import ujson as json
|
|
9
10
|
from ujson import JSONDecodeError
|
|
10
11
|
import hmac
|
|
@@ -19,14 +20,34 @@ from websockets.asyncio.client import connect
|
|
|
19
20
|
from websockets import ConnectionClosed
|
|
20
21
|
|
|
21
22
|
from enum import Enum
|
|
22
|
-
from crypto_ws_api import TIMEOUT, ID_LEN_LIMIT, DELAY
|
|
23
|
+
from crypto_ws_api import TIMEOUT, ID_LEN_LIMIT, DELAY, DEBUG_LOG
|
|
24
|
+
|
|
25
|
+
from exchanges_wrapper import LOG_PATH
|
|
23
26
|
|
|
24
|
-
logger = logging.getLogger(__name__)
|
|
25
|
-
logger.level = logging.INFO
|
|
26
27
|
sys.tracebacklimit = 0
|
|
28
|
+
|
|
27
29
|
ALPHABET = string.ascii_letters + string.digits
|
|
28
30
|
CONST_WS_START = "userDataStream.start"
|
|
29
31
|
|
|
32
|
+
def set_logger(name, log_file, file_level=logging.INFO, propagate=False):
|
|
33
|
+
formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
|
|
34
|
+
#
|
|
35
|
+
fh = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10)
|
|
36
|
+
fh.setFormatter(formatter)
|
|
37
|
+
fh.setLevel(file_level)
|
|
38
|
+
#
|
|
39
|
+
sh = logging.StreamHandler()
|
|
40
|
+
sh.setFormatter(formatter)
|
|
41
|
+
sh.setLevel(logging.INFO)
|
|
42
|
+
#
|
|
43
|
+
_logger = logging.getLogger(name)
|
|
44
|
+
_logger.addHandler(fh)
|
|
45
|
+
_logger.addHandler(sh)
|
|
46
|
+
_logger.propagate = propagate
|
|
47
|
+
|
|
48
|
+
return _logger
|
|
49
|
+
|
|
50
|
+
logger = set_logger(__name__, Path(LOG_PATH, 'ws_session.log'))
|
|
30
51
|
|
|
31
52
|
def generate_signature(exchange: str, secret: str, data: str) -> str:
|
|
32
53
|
digest_func = hashlib.sha384 if exchange == 'bitfinex' else hashlib.sha256
|
|
@@ -75,7 +96,6 @@ class UserWSS:
|
|
|
75
96
|
"endpoint",
|
|
76
97
|
"_api_key",
|
|
77
98
|
"_api_secret",
|
|
78
|
-
"signed",
|
|
79
99
|
"_passphrase",
|
|
80
100
|
"_ws",
|
|
81
101
|
"_listen_key",
|
|
@@ -87,17 +107,17 @@ class UserWSS:
|
|
|
87
107
|
"in_event",
|
|
88
108
|
"_response_pool",
|
|
89
109
|
"tasks",
|
|
90
|
-
"ping"
|
|
110
|
+
"ping",
|
|
111
|
+
"logger"
|
|
91
112
|
)
|
|
92
113
|
|
|
93
|
-
def __init__(self, ws_id, exchange, endpoint, api_key, api_secret,
|
|
114
|
+
def __init__(self, ws_id, exchange, endpoint, api_key, api_secret, passphrase=None, trade_id=None):
|
|
94
115
|
self.init = True
|
|
95
116
|
self.exchange = exchange
|
|
96
117
|
self.endpoint = endpoint
|
|
97
118
|
#
|
|
98
119
|
self._api_key = api_key
|
|
99
120
|
self._api_secret = api_secret
|
|
100
|
-
self.signed = signed
|
|
101
121
|
self._passphrase = passphrase
|
|
102
122
|
self._ws = None
|
|
103
123
|
self._listen_key = None
|
|
@@ -111,6 +131,15 @@ class UserWSS:
|
|
|
111
131
|
self.tasks = set()
|
|
112
132
|
self.ping = 0
|
|
113
133
|
|
|
134
|
+
if self.exchange == DEBUG_LOG:
|
|
135
|
+
if trade_id in logging.root.manager.loggerDict:
|
|
136
|
+
logger.info(f"Logger {trade_id} already exists")
|
|
137
|
+
self.logger = logging.root.manager.loggerDict[trade_id]
|
|
138
|
+
else:
|
|
139
|
+
self.logger = set_logger(trade_id, Path(LOG_PATH, f"ws_{trade_id}.log"), logging.DEBUG)
|
|
140
|
+
else:
|
|
141
|
+
self.logger = logger
|
|
142
|
+
|
|
114
143
|
def tasks_manage(self, coro, name=None):
|
|
115
144
|
_t = asyncio.create_task(coro, name=name)
|
|
116
145
|
self.tasks.add(_t)
|
|
@@ -143,11 +172,13 @@ class UserWSS:
|
|
|
143
172
|
else:
|
|
144
173
|
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
145
174
|
await self.stop()
|
|
175
|
+
logger.warning(f"UserWSS: {self.ws_id}: _ws_listener stopped")
|
|
176
|
+
await self.stop()
|
|
146
177
|
|
|
147
178
|
async def start_wss(self):
|
|
148
179
|
async for self._ws in connect(
|
|
149
180
|
self.endpoint,
|
|
150
|
-
logger=logger,
|
|
181
|
+
logger=self.logger,
|
|
151
182
|
ping_interval=None if self.exchange == 'huobi' else 20
|
|
152
183
|
):
|
|
153
184
|
try:
|
|
@@ -160,16 +191,16 @@ class UserWSS:
|
|
|
160
191
|
self.operational_status = False
|
|
161
192
|
[task.cancel() for task in self.tasks if not task.done()]
|
|
162
193
|
self.tasks.clear()
|
|
163
|
-
logger.warning(f"Restart
|
|
194
|
+
logger.warning(f"Restart UserWSS for {self.ws_id}")
|
|
164
195
|
continue
|
|
165
196
|
except Exception as ex:
|
|
166
|
-
logger.error(f"
|
|
197
|
+
logger.error(f"UserWSS start other exception: {ex}")
|
|
167
198
|
|
|
168
199
|
async def ws_login(self):
|
|
169
200
|
res = await self.request(CONST_WS_START, send_api_key=True)
|
|
170
201
|
if res is None:
|
|
171
202
|
logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
|
|
172
|
-
|
|
203
|
+
await self.stop()
|
|
173
204
|
else:
|
|
174
205
|
if self.exchange == 'binance':
|
|
175
206
|
self._listen_key = res.get('listenKey')
|
|
@@ -215,7 +246,6 @@ class UserWSS:
|
|
|
215
246
|
except asyncio.CancelledError:
|
|
216
247
|
pass # Task cancellation should not be logged as an error
|
|
217
248
|
else:
|
|
218
|
-
# logger.info(f"request: {self.ws_id}: {res}")
|
|
219
249
|
return res
|
|
220
250
|
|
|
221
251
|
async def _response_distributor(self, _id):
|
|
@@ -494,20 +524,22 @@ class UserWSSession:
|
|
|
494
524
|
send_api_key=False,
|
|
495
525
|
_signed=False,
|
|
496
526
|
):
|
|
527
|
+
|
|
497
528
|
ws_id = f"{self.exchange}-{trade_id}-{method}"
|
|
498
529
|
|
|
499
|
-
|
|
500
|
-
ws_id
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
530
|
+
if ws_id in self.user_wss:
|
|
531
|
+
user_wss = self.user_wss[ws_id]
|
|
532
|
+
else:
|
|
533
|
+
user_wss = UserWSS(
|
|
534
|
+
ws_id,
|
|
535
|
+
self.exchange,
|
|
536
|
+
self.endpoint,
|
|
537
|
+
self._api_key,
|
|
538
|
+
self._api_secret,
|
|
539
|
+
self._passphrase,
|
|
540
|
+
trade_id
|
|
541
|
+
)
|
|
542
|
+
self.user_wss[ws_id] = user_wss
|
|
511
543
|
|
|
512
544
|
if user_wss.init:
|
|
513
545
|
user_wss.init = False
|
|
@@ -520,6 +552,7 @@ class UserWSSession:
|
|
|
520
552
|
while not (user_wss.operational_status and user_wss.order_handling):
|
|
521
553
|
await asyncio.sleep(DELAY)
|
|
522
554
|
if duration > TIMEOUT:
|
|
555
|
+
logger.warning(f"{trade_id}: Register timeout for method '{method}'")
|
|
523
556
|
return None
|
|
524
557
|
duration += DELAY
|
|
525
558
|
|
|
@@ -529,6 +562,7 @@ class UserWSSession:
|
|
|
529
562
|
pass # Task cancellation should not be logged as an error
|
|
530
563
|
except Exception as ex:
|
|
531
564
|
logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
|
|
565
|
+
logger.warning(f"{trade_id}: {method}: None response")
|
|
532
566
|
return None
|
|
533
567
|
|
|
534
568
|
async def stop(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|