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.
@@ -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.17
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.17"
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, 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,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 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
+ 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
- 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
- )
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):
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "shortuuid~=1.0.13",
21
21
  "platformdirs==4.3.6",
22
22
  "toml~=0.10.2",
23
- "websockets==15.0",
23
+ "websockets==15.0.1",
24
24
  "ujson~=5.10.0"
25
25
  ]
26
26
 
@@ -1,5 +1,5 @@
1
1
  shortuuid~=1.0.13
2
2
  platformdirs==4.3.6
3
3
  toml~=0.10.2
4
- websockets==15.0
4
+ websockets==15.0.1
5
5
  ujson~=5.10.0
File without changes