crypto-ws-api 2.0.6__tar.gz → 2.0.8__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.6 → crypto-ws-api-2.0.8}/CHANGELOG.md +8 -0
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/PKG-INFO +3 -5
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/README.md +2 -4
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/crypto_ws_api/__init__.py +2 -2
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/crypto_ws_api/ws_session.py +99 -73
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/.deepsource.toml +0 -0
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/LICENSE.md +0 -0
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/crypto_ws_api/demo.py +0 -0
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/crypto_ws_api/ws_api.toml.template +0 -0
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/pyproject.toml +0 -0
- {crypto-ws-api-2.0.6 → crypto-ws-api-2.0.8}/requirements.txt +0 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.0.8 - 2024-03-31
|
|
2
|
+
### Fix
|
|
3
|
+
* tons log records: `websockets socket.send() raised exception.`
|
|
4
|
+
|
|
5
|
+
## 2.0.7 - 2024-03-25
|
|
6
|
+
### Update
|
|
7
|
+
* Refactoring and some minor fixes
|
|
8
|
+
|
|
1
9
|
## 2.0.6 - 2024-01-05
|
|
2
10
|
### Update
|
|
3
11
|
* replacing json with ujson to improve performance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.8
|
|
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
|
|
@@ -37,7 +37,7 @@ For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex
|
|
|
37
37
|
|
|
38
38
|
## Features
|
|
39
39
|
Lightweight and efficient solution to utilize of all available methods** provided through the:
|
|
40
|
-
* [Binance Websocket API v3](https://
|
|
40
|
+
* [Binance Websocket API v3](https://binance-docs.github.io/apidocs/websocket_api/en)
|
|
41
41
|
* [OKX Websocket API v5](https://www.okx.com/docs-v5/en/#overview-websocket)
|
|
42
42
|
* [Bitfinex Websocket Inputs](https://docs.bitfinex.com/reference/ws-auth-input)
|
|
43
43
|
|
|
@@ -222,7 +222,5 @@ If you send a request in this state, the answer will be `None`
|
|
|
222
222
|
*In any case, you are protected from exceeding limits and blocking for this reason*
|
|
223
223
|
|
|
224
224
|
## Donate
|
|
225
|
-
*
|
|
226
|
-
|
|
227
|
-
*USDT* (TRC20) TP1Y43dpY7rrRyTSLaSKDZmFirqvRcpopC
|
|
225
|
+
*USDT* (TRC20) TU3kagV9kxbjuUmEi6bUym5MTXjeM7Tm8K
|
|
228
226
|
|
|
@@ -17,7 +17,7 @@ For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex
|
|
|
17
17
|
|
|
18
18
|
## Features
|
|
19
19
|
Lightweight and efficient solution to utilize of all available methods** provided through the:
|
|
20
|
-
* [Binance Websocket API v3](https://
|
|
20
|
+
* [Binance Websocket API v3](https://binance-docs.github.io/apidocs/websocket_api/en)
|
|
21
21
|
* [OKX Websocket API v5](https://www.okx.com/docs-v5/en/#overview-websocket)
|
|
22
22
|
* [Bitfinex Websocket Inputs](https://docs.bitfinex.com/reference/ws-auth-input)
|
|
23
23
|
|
|
@@ -202,6 +202,4 @@ If you send a request in this state, the answer will be `None`
|
|
|
202
202
|
*In any case, you are protected from exceeding limits and blocking for this reason*
|
|
203
203
|
|
|
204
204
|
## Donate
|
|
205
|
-
*
|
|
206
|
-
|
|
207
|
-
*USDT* (TRC20) TP1Y43dpY7rrRyTSLaSKDZmFirqvRcpopC
|
|
205
|
+
*USDT* (TRC20) TU3kagV9kxbjuUmEi6bUym5MTXjeM7Tm8K
|
|
@@ -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.8"
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
import shutil
|
|
@@ -21,7 +21,7 @@ from platformdirs import user_config_path
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
TIMEOUT = 5 # sec timeout for WSS initialization and get response
|
|
24
|
-
DELAY = 0.1 # sec delay in
|
|
24
|
+
DELAY = 0.1 # sec delay in keepalive loop
|
|
25
25
|
# Maximum str size for unique query ID
|
|
26
26
|
ID_LEN_LIMIT = {
|
|
27
27
|
"binance": 36,
|
|
@@ -104,7 +104,7 @@ class UserWSS:
|
|
|
104
104
|
elif self.exchange in ['okx', 'bitfinex']:
|
|
105
105
|
self._response_pool[res.get('id') or self.ws_id] = res.get('data') or res
|
|
106
106
|
self.in_event.set()
|
|
107
|
-
|
|
107
|
+
await asyncio.sleep(0)
|
|
108
108
|
else:
|
|
109
109
|
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
110
110
|
await self.stop()
|
|
@@ -171,12 +171,12 @@ class UserWSS:
|
|
|
171
171
|
await self._ws.send(
|
|
172
172
|
json.dumps(self.compose_request(_id, _api_key, method, params, _signed))
|
|
173
173
|
)
|
|
174
|
+
await asyncio.sleep(0)
|
|
174
175
|
try:
|
|
175
176
|
res = await asyncio.wait_for(self._response_distributor(_id), timeout=TIMEOUT)
|
|
176
|
-
except asyncio.TimeoutError:
|
|
177
|
+
except asyncio.exceptions.TimeoutError:
|
|
177
178
|
logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
|
|
178
179
|
await self.stop()
|
|
179
|
-
return None
|
|
180
180
|
except asyncio.CancelledError:
|
|
181
181
|
pass # Task cancellation should not be logged as an error
|
|
182
182
|
else:
|
|
@@ -193,53 +193,59 @@ class UserWSS:
|
|
|
193
193
|
return self._response_pool.pop(f"NoneResponse{self.ws_id}", None)
|
|
194
194
|
|
|
195
195
|
def compose_request(self, _id, api_key, method, params, signed):
|
|
196
|
-
req = None
|
|
197
196
|
if self.exchange == "binance":
|
|
198
|
-
|
|
199
|
-
if (api_key or signed) and not params:
|
|
200
|
-
params = {}
|
|
201
|
-
if api_key:
|
|
202
|
-
params["apiKey"] = self._api_key
|
|
203
|
-
if signed:
|
|
204
|
-
params["timestamp"] = int(time.time() * 1000)
|
|
205
|
-
payload = '&'.join(f"{key}={value}" for key, value in dict(sorted(params.items())).items())
|
|
206
|
-
params["signature"] = generate_signature('binance_ws', self._api_secret, payload)
|
|
207
|
-
if params:
|
|
208
|
-
req["params"] = params
|
|
197
|
+
return self._compose_binance_request(_id, api_key, method, params, signed)
|
|
209
198
|
elif self.exchange == "okx":
|
|
210
|
-
|
|
211
|
-
# https://www.okx.com/docs-v5/en/?python#overview-websocket-connect
|
|
212
|
-
ts = int(time.time())
|
|
213
|
-
signature_payload = f"{ts}GET/users/self/verify"
|
|
214
|
-
signature = generate_signature(self.exchange, self._api_secret, signature_payload)
|
|
215
|
-
# Login on account
|
|
216
|
-
req = {"op": 'login',
|
|
217
|
-
"args": [{"apiKey": self._api_key,
|
|
218
|
-
"passphrase": self._passphrase,
|
|
219
|
-
"timestamp": ts,
|
|
220
|
-
"sign": signature}
|
|
221
|
-
]
|
|
222
|
-
}
|
|
223
|
-
else:
|
|
224
|
-
req = {"id": _id, "op": method, "args": params if isinstance(params, list) else [params]}
|
|
199
|
+
return self._compose_okx_request(_id, method, params)
|
|
225
200
|
elif self.exchange == 'bitfinex':
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
201
|
+
return self._compose_bitfinex_request(_id, method, params)
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError(f"Unsupported exchange: {self.exchange}")
|
|
204
|
+
|
|
205
|
+
def _compose_binance_request(self, _id, api_key, method, params, signed):
|
|
206
|
+
req = {"id": _id, "method": method}
|
|
207
|
+
params = params or {}
|
|
208
|
+
if api_key:
|
|
209
|
+
params["apiKey"] = self._api_key
|
|
210
|
+
if signed:
|
|
211
|
+
params["timestamp"] = int(time.time() * 1000)
|
|
212
|
+
payload = '&'.join(f"{key}={value}" for key, value in sorted(params.items()))
|
|
213
|
+
params["signature"] = generate_signature('binance_ws', self._api_secret, payload)
|
|
214
|
+
if params:
|
|
215
|
+
req["params"] = params
|
|
241
216
|
return req
|
|
242
217
|
|
|
218
|
+
def _compose_okx_request(self, _id, method, params):
|
|
219
|
+
if method == CONST_3:
|
|
220
|
+
ts = int(time.time())
|
|
221
|
+
signature_payload = f"{ts}GET/users/self/verify"
|
|
222
|
+
signature = generate_signature(self.exchange, self._api_secret, signature_payload)
|
|
223
|
+
return {
|
|
224
|
+
"op": 'login',
|
|
225
|
+
"args": [
|
|
226
|
+
{"apiKey": self._api_key, "passphrase": self._passphrase, "timestamp": ts, "sign": signature}
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
else:
|
|
230
|
+
return {"id": _id, "op": method, "args": params if isinstance(params, list) else [params]}
|
|
231
|
+
|
|
232
|
+
def _compose_bitfinex_request(self, _id, method, params):
|
|
233
|
+
if method == CONST_3:
|
|
234
|
+
ts = int(time.time() * 1000)
|
|
235
|
+
data = f"AUTH{ts}"
|
|
236
|
+
return {
|
|
237
|
+
'event': "auth",
|
|
238
|
+
'apiKey': self._api_key,
|
|
239
|
+
'authSig': generate_signature(self.exchange, self._api_secret, data),
|
|
240
|
+
'authPayload': data,
|
|
241
|
+
'authNonce': ts,
|
|
242
|
+
'filter': ['trading']
|
|
243
|
+
}
|
|
244
|
+
else:
|
|
245
|
+
if method == 'on':
|
|
246
|
+
params.update({"meta": {"aff_code": "v_4az2nCP"}})
|
|
247
|
+
return [0, method, _id, params]
|
|
248
|
+
|
|
243
249
|
async def _keepalive(self, interval=10):
|
|
244
250
|
while self.operational_status is not None:
|
|
245
251
|
if self.request_limit_reached and (int(time.time() * 1000) - self._retry_after >= 0):
|
|
@@ -291,40 +297,60 @@ class UserWSS:
|
|
|
291
297
|
elif self.exchange == 'bitfinex':
|
|
292
298
|
return await self.bitfinex_error_handle(msg)
|
|
293
299
|
|
|
300
|
+
#region BitfinexErrorHandle
|
|
294
301
|
async def bitfinex_error_handle(self, msg):
|
|
295
302
|
if isinstance(msg, dict):
|
|
296
|
-
|
|
297
|
-
logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
|
|
298
|
-
await self.stop()
|
|
299
|
-
if msg.get('event') == 'auth' and msg.get('status') == "OK":
|
|
300
|
-
return msg
|
|
301
|
-
if msg.get('event') == 'info' and msg.get('version') != 2:
|
|
302
|
-
logger.critical('Bitfinex WSS platform: version change detected')
|
|
303
|
-
# sourcery skip: merge-nested-ifs
|
|
304
|
-
if msg.get('code'):
|
|
305
|
-
if msg.get('code') == 10305:
|
|
306
|
-
logger.warning('UserWSS Bitfinex: Reached limit of open channels')
|
|
307
|
-
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
308
|
-
self.request_limit_reached = True
|
|
309
|
-
logger.warning(f"Malformed request for {self.ws_id}: {msg}")
|
|
310
|
-
return None
|
|
311
|
-
return 'pass'
|
|
303
|
+
return await self._handle_dict_message(msg)
|
|
312
304
|
elif isinstance(msg, list) and msg[1] == 'n' and msg[2][1] in ('on-req', 'oc-req', 'oc_multi-req'):
|
|
313
|
-
|
|
314
|
-
"data": [msg[2][0],
|
|
315
|
-
msg[2][1],
|
|
316
|
-
None,
|
|
317
|
-
None,
|
|
318
|
-
[msg[2][4]] if msg[2][1] == 'on-req' else msg[2][4],
|
|
319
|
-
None,
|
|
320
|
-
msg[2][6],
|
|
321
|
-
msg[2][7]
|
|
322
|
-
]
|
|
323
|
-
}
|
|
324
|
-
return msg
|
|
305
|
+
return self._transform_list_message(msg)
|
|
325
306
|
else:
|
|
326
307
|
return 'pass'
|
|
327
308
|
|
|
309
|
+
async def _handle_dict_message(self, msg):
|
|
310
|
+
event = msg.get('event')
|
|
311
|
+
if event == 'info':
|
|
312
|
+
return await self._handle_info_event(msg)
|
|
313
|
+
elif event == 'auth':
|
|
314
|
+
return msg if msg.get('status') == "OK" else None
|
|
315
|
+
elif msg.get('code'):
|
|
316
|
+
return await self._handle_error_code(msg)
|
|
317
|
+
return 'pass'
|
|
318
|
+
|
|
319
|
+
async def _handle_info_event(self, msg):
|
|
320
|
+
if not msg.get('platform', {}).get('status'):
|
|
321
|
+
logger.warning(f"UserWSS Bitfinex platform in maintenance mode: {msg}")
|
|
322
|
+
await self.stop()
|
|
323
|
+
elif msg.get('version') != 2:
|
|
324
|
+
logger.critical('Bitfinex WSS platform: version change detected')
|
|
325
|
+
return 'pass'
|
|
326
|
+
|
|
327
|
+
async def _handle_error_code(self, msg):
|
|
328
|
+
code = msg.get('code')
|
|
329
|
+
if code == 10305:
|
|
330
|
+
logger.warning('UserWSS Bitfinex: Reached limit of open channels')
|
|
331
|
+
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
332
|
+
self.request_limit_reached = True
|
|
333
|
+
else:
|
|
334
|
+
logger.warning(f"Malformed request for {self.ws_id}: {msg}")
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _transform_list_message(msg):
|
|
339
|
+
return {
|
|
340
|
+
"id": msg[2][2],
|
|
341
|
+
"data": [
|
|
342
|
+
msg[2][0],
|
|
343
|
+
msg[2][1],
|
|
344
|
+
None,
|
|
345
|
+
None,
|
|
346
|
+
[msg[2][4]] if msg[2][1] == 'on-req' else msg[2][4],
|
|
347
|
+
None,
|
|
348
|
+
msg[2][6],
|
|
349
|
+
msg[2][7]
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
#endregion
|
|
353
|
+
|
|
328
354
|
async def okx_error_handle(self, msg):
|
|
329
355
|
if msg.get('code') == '1':
|
|
330
356
|
logger.warning(f"Operation failed: {msg}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|