crypto-ws-api 2.0.16__tar.gz → 2.0.18__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.16 → crypto_ws_api-2.0.18}/CHANGELOG.md +13 -1
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/PKG-INFO +3 -3
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/crypto_ws_api/__init__.py +2 -1
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/crypto_ws_api/ws_session.py +58 -25
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/pyproject.toml +2 -2
- crypto_ws_api-2.0.18/requirements.txt +5 -0
- crypto_ws_api-2.0.16/requirements.txt +0 -5
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/.deepsource.toml +0 -0
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/LICENSE.md +0 -0
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/README.md +0 -0
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/crypto_ws_api/demo.py +0 -0
- {crypto_ws_api-2.0.16 → crypto_ws_api-2.0.18}/crypto_ws_api/ws_api.toml.template +0 -0
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
## 2.0.
|
|
1
|
+
## 2.0.18 - 2025-03-10
|
|
2
|
+
### Fix
|
|
3
|
+
* [🔒] fix/ws_session.py: Proper restoration of WebSocket connection during recurring failures
|
|
4
|
+
|
|
5
|
+
### Update
|
|
6
|
+
* [💻] feat/ws_session.py: Add logging configuration and handlers for debug purpose
|
|
7
|
+
* [🔄] fix/ws_session.py: Update UserWSS class to accept a logger instance
|
|
8
|
+
|
|
9
|
+
## 2.0.17 - 2025-02-20
|
|
10
|
+
### Update
|
|
11
|
+
* Bump dependencies
|
|
12
|
+
|
|
13
|
+
## 2.0.16 - 2024-12-27
|
|
2
14
|
* `websockets`: bump to v14.2
|
|
3
15
|
|
|
4
16
|
## 2.0.15 - 2024-12-16
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.18
|
|
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
|
|
@@ -12,9 +12,9 @@ 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==3.
|
|
15
|
+
Requires-Dist: platformdirs==4.3.6
|
|
16
16
|
Requires-Dist: toml~=0.10.2
|
|
17
|
-
Requires-Dist: websockets==
|
|
17
|
+
Requires-Dist: websockets==15.0
|
|
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.18"
|
|
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,17 @@ 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
|
+
if self._ws:
|
|
204
|
+
await self._ws.close(code=4000)
|
|
173
205
|
else:
|
|
174
206
|
if self.exchange == 'binance':
|
|
175
207
|
self._listen_key = res.get('listenKey')
|
|
@@ -215,7 +247,6 @@ class UserWSS:
|
|
|
215
247
|
except asyncio.CancelledError:
|
|
216
248
|
pass # Task cancellation should not be logged as an error
|
|
217
249
|
else:
|
|
218
|
-
# logger.info(f"request: {self.ws_id}: {res}")
|
|
219
250
|
return res
|
|
220
251
|
|
|
221
252
|
async def _response_distributor(self, _id):
|
|
@@ -494,20 +525,22 @@ class UserWSSession:
|
|
|
494
525
|
send_api_key=False,
|
|
495
526
|
_signed=False,
|
|
496
527
|
):
|
|
528
|
+
|
|
497
529
|
ws_id = f"{self.exchange}-{trade_id}-{method}"
|
|
498
530
|
|
|
499
|
-
|
|
500
|
-
ws_id
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
531
|
+
if ws_id in self.user_wss:
|
|
532
|
+
user_wss = self.user_wss[ws_id]
|
|
533
|
+
else:
|
|
534
|
+
user_wss = UserWSS(
|
|
535
|
+
ws_id,
|
|
536
|
+
self.exchange,
|
|
537
|
+
self.endpoint,
|
|
538
|
+
self._api_key,
|
|
539
|
+
self._api_secret,
|
|
540
|
+
self._passphrase,
|
|
541
|
+
trade_id
|
|
542
|
+
)
|
|
543
|
+
self.user_wss[ws_id] = user_wss
|
|
511
544
|
|
|
512
545
|
if user_wss.init:
|
|
513
546
|
user_wss.init = False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|