crypto-ws-api 2.0.17__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.
@@ -1,3 +1,11 @@
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
+
1
9
  ## 2.0.17 - 2025-02-20
2
10
  ### Update
3
11
  * Bump dependencies
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: crypto-ws-api
3
- Version: 2.0.17
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
@@ -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.17"
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, signed, passphrase=None):
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 WSS for {self.ws_id}")
194
+ logger.warning(f"Restart UserWSS for {self.ws_id}")
164
195
  continue
165
196
  except Exception as ex:
166
- logger.error(f"WSS start other exception: {ex}")
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
- raise ConnectionClosed(None, None)
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
- user_wss = self.user_wss.setdefault(
500
- ws_id,
501
- UserWSS(
502
- ws_id,
503
- self.exchange,
504
- self.endpoint,
505
- self._api_key,
506
- self._api_secret,
507
- _signed,
508
- self._passphrase
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