pybit 5.15.0__py2.py3-none-any.whl
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.
- pybit/__init__.py +1 -0
- pybit/_helpers.py +75 -0
- pybit/_http_manager.py +354 -0
- pybit/_v5_account.py +501 -0
- pybit/_v5_asset.py +826 -0
- pybit/_v5_broker.py +191 -0
- pybit/_v5_crypto_loan.py +700 -0
- pybit/_v5_earn.py +115 -0
- pybit/_v5_fiat.py +132 -0
- pybit/_v5_institutional_loan.py +117 -0
- pybit/_v5_market.py +404 -0
- pybit/_v5_misc.py +53 -0
- pybit/_v5_position.py +349 -0
- pybit/_v5_pre_upgrade.py +130 -0
- pybit/_v5_rate_limit.py +78 -0
- pybit/_v5_rfq.py +264 -0
- pybit/_v5_spot_leverage_token.py +95 -0
- pybit/_v5_spot_margin_trade.py +394 -0
- pybit/_v5_spread.py +190 -0
- pybit/_v5_trade.py +270 -0
- pybit/_v5_user.py +320 -0
- pybit/_websocket_stream.py +605 -0
- pybit/_websocket_trading.py +89 -0
- pybit/account.py +34 -0
- pybit/asset.py +62 -0
- pybit/broker.py +17 -0
- pybit/crypto_loan.py +53 -0
- pybit/earn.py +13 -0
- pybit/exceptions.py +58 -0
- pybit/fiat.py +14 -0
- pybit/helpers.py +48 -0
- pybit/institutional_loan.py +14 -0
- pybit/market.py +29 -0
- pybit/misc.py +10 -0
- pybit/position.py +22 -0
- pybit/pre_upgrade.py +13 -0
- pybit/rate_limit.py +11 -0
- pybit/rfq.py +22 -0
- pybit/spot_leverage_token.py +12 -0
- pybit/spot_margin_trade.py +33 -0
- pybit/spread.py +18 -0
- pybit/trade.py +19 -0
- pybit/unified_trading.py +522 -0
- pybit/user.py +25 -0
- pybit-5.15.0.dist-info/METADATA +142 -0
- pybit-5.15.0.dist-info/RECORD +49 -0
- pybit-5.15.0.dist-info/WHEEL +6 -0
- pybit-5.15.0.dist-info/licenses/LICENSE +24 -0
- pybit-5.15.0.dist-info/top_level.txt +1 -0
pybit/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "5.15.0"
|
pybit/_helpers.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import re
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_timestamp():
|
|
7
|
+
"""
|
|
8
|
+
Return a millisecond integer timestamp.
|
|
9
|
+
"""
|
|
10
|
+
return int(time.time() * 10**3)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def identify_ws_method(input_wss_url, wss_dictionary):
|
|
14
|
+
"""
|
|
15
|
+
This method matches the input_wss_url with a particular WSS method. This
|
|
16
|
+
helps ensure that, when subscribing to a custom topic, the topic
|
|
17
|
+
subscription message is sent down the correct WSS connection.
|
|
18
|
+
"""
|
|
19
|
+
path = re.compile(r"(wss://)?([^/\s]+)(.*)")
|
|
20
|
+
input_wss_url_path = path.match(input_wss_url).group(3)
|
|
21
|
+
for wss_url, function_call in wss_dictionary.items():
|
|
22
|
+
wss_url_path = path.match(wss_url).group(3)
|
|
23
|
+
if input_wss_url_path == wss_url_path:
|
|
24
|
+
return function_call
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_index(source, target, key):
|
|
28
|
+
"""
|
|
29
|
+
Find the index in source list of the targeted ID.
|
|
30
|
+
"""
|
|
31
|
+
return next(i for i, j in enumerate(source) if j[key] == target[key])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def make_private_args(args):
|
|
35
|
+
"""
|
|
36
|
+
Exists to pass on the user's arguments to a lower-level class without
|
|
37
|
+
giving the user access to that classes attributes (ie, passing on args
|
|
38
|
+
without inheriting the parent class).
|
|
39
|
+
"""
|
|
40
|
+
args.pop("self")
|
|
41
|
+
return args
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_public_kwargs(private_kwargs):
|
|
45
|
+
public_kwargs = copy.deepcopy(private_kwargs)
|
|
46
|
+
public_kwargs.pop("api_key", "")
|
|
47
|
+
public_kwargs.pop("api_secret", "")
|
|
48
|
+
return public_kwargs
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def are_connections_connected(active_connections):
|
|
52
|
+
for connection in active_connections:
|
|
53
|
+
if not connection.is_connected():
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_inverse_contract(symbol: str):
|
|
59
|
+
if re.search(r"(USD)([HMUZ]\d\d|$)", symbol):
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_usdt_perpetual(symbol: str):
|
|
64
|
+
if symbol.endswith("USDT"):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_usdc_perpetual(symbol: str):
|
|
69
|
+
if symbol.endswith("USDC"):
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_usdc_option(symbol: str):
|
|
74
|
+
if re.search(r"[A-Z]{3}-.*-[PC]$", symbol):
|
|
75
|
+
return True
|
pybit/_http_manager.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
import time
|
|
4
|
+
import hmac
|
|
5
|
+
import hashlib
|
|
6
|
+
from Crypto.Hash import SHA256
|
|
7
|
+
from Crypto.PublicKey import RSA
|
|
8
|
+
from Crypto.Signature import PKCS1_v1_5
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from datetime import datetime as dt, timezone
|
|
15
|
+
|
|
16
|
+
from .exceptions import FailedRequestError, InvalidRequestError
|
|
17
|
+
from . import _helpers
|
|
18
|
+
|
|
19
|
+
# Requests will use simplejson if available.
|
|
20
|
+
try:
|
|
21
|
+
from simplejson.errors import JSONDecodeError
|
|
22
|
+
except ImportError:
|
|
23
|
+
from json.decoder import JSONDecodeError
|
|
24
|
+
|
|
25
|
+
HTTP_URL = "https://{SUBDOMAIN}.{DOMAIN}.{TLD}"
|
|
26
|
+
SUBDOMAIN_TESTNET = "api-testnet"
|
|
27
|
+
SUBDOMAIN_MAINNET = "api"
|
|
28
|
+
DEMO_SUBDOMAIN_TESTNET = "api-demo-testnet"
|
|
29
|
+
DEMO_SUBDOMAIN_MAINNET = "api-demo"
|
|
30
|
+
DOMAIN_MAIN = "bybit"
|
|
31
|
+
DOMAIN_ALT = "bytick"
|
|
32
|
+
DOMAIN_TK = "bybit-tr" # Turkey
|
|
33
|
+
TLD_MAIN = "com" # Global
|
|
34
|
+
TLD_NL = "nl" # The Netherlands
|
|
35
|
+
TLD_HK = "com.hk" # Hong Kong
|
|
36
|
+
TLD_KZ = "kz" # Kazakhstan
|
|
37
|
+
TLD_EU = "eu" # European Economic Area. ONLY AVAILABLE TO INSTITUTIONS
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _RetryableRequestError(Exception):
|
|
41
|
+
def __init__(self, recv_window):
|
|
42
|
+
self.recv_window = recv_window
|
|
43
|
+
super().__init__("Retryable error occurred, retrying...")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_signature(use_rsa_authentication, secret, param_str):
|
|
47
|
+
def generate_hmac():
|
|
48
|
+
hash = hmac.new(
|
|
49
|
+
bytes(secret, "utf-8"),
|
|
50
|
+
param_str.encode("utf-8"),
|
|
51
|
+
hashlib.sha256,
|
|
52
|
+
)
|
|
53
|
+
return hash.hexdigest()
|
|
54
|
+
|
|
55
|
+
def generate_rsa():
|
|
56
|
+
hash = SHA256.new(param_str.encode("utf-8"))
|
|
57
|
+
encoded_signature = base64.b64encode(
|
|
58
|
+
PKCS1_v1_5.new(RSA.importKey(secret)).sign(
|
|
59
|
+
hash
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return encoded_signature.decode()
|
|
63
|
+
|
|
64
|
+
if not use_rsa_authentication:
|
|
65
|
+
return generate_hmac()
|
|
66
|
+
else:
|
|
67
|
+
return generate_rsa()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class _V5HTTPManager:
|
|
72
|
+
testnet: bool = field(default=False)
|
|
73
|
+
domain: str = field(default=DOMAIN_MAIN)
|
|
74
|
+
tld: str = field(default=TLD_MAIN)
|
|
75
|
+
demo: bool = field(default=False)
|
|
76
|
+
rsa_authentication: str = field(default=False)
|
|
77
|
+
api_key: str = field(default=None)
|
|
78
|
+
api_secret: str = field(default=None)
|
|
79
|
+
logging_level: logging = field(default=logging.INFO)
|
|
80
|
+
log_requests: bool = field(default=False)
|
|
81
|
+
timeout: int = field(default=10)
|
|
82
|
+
recv_window: bool = field(default=5000)
|
|
83
|
+
force_retry: bool = field(default=False)
|
|
84
|
+
retry_codes: defaultdict[dict] = field(default_factory=dict)
|
|
85
|
+
ignore_codes: set = field(default_factory=set)
|
|
86
|
+
max_retries: bool = field(default=3)
|
|
87
|
+
retry_delay: bool = field(default=3)
|
|
88
|
+
referral_id: str = field(default=None)
|
|
89
|
+
record_request_time: bool = field(default=False)
|
|
90
|
+
return_response_headers: bool = field(default=False)
|
|
91
|
+
|
|
92
|
+
def __post_init__(self):
|
|
93
|
+
subdomain = SUBDOMAIN_TESTNET if self.testnet else SUBDOMAIN_MAINNET
|
|
94
|
+
domain = DOMAIN_MAIN if not self.domain else self.domain
|
|
95
|
+
if self.demo:
|
|
96
|
+
if self.testnet:
|
|
97
|
+
subdomain = DEMO_SUBDOMAIN_TESTNET
|
|
98
|
+
else:
|
|
99
|
+
subdomain = DEMO_SUBDOMAIN_MAINNET
|
|
100
|
+
url = HTTP_URL.format(SUBDOMAIN=subdomain, DOMAIN=domain, TLD=self.tld)
|
|
101
|
+
self.endpoint = url
|
|
102
|
+
|
|
103
|
+
if not self.ignore_codes:
|
|
104
|
+
self.ignore_codes = set()
|
|
105
|
+
if not self.retry_codes:
|
|
106
|
+
self.retry_codes = {10002, 10006, 30034, 30035, 130035, 130150}
|
|
107
|
+
self.logger = logging.getLogger(__name__)
|
|
108
|
+
if len(logging.root.handlers) == 0:
|
|
109
|
+
# no handler on root logger set -> we add handler just for this logger to not mess with custom logic from
|
|
110
|
+
# outside
|
|
111
|
+
handler = logging.StreamHandler()
|
|
112
|
+
handler.setFormatter(
|
|
113
|
+
logging.Formatter(
|
|
114
|
+
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
115
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
handler.setLevel(self.logging_level)
|
|
119
|
+
self.logger.addHandler(handler)
|
|
120
|
+
|
|
121
|
+
self.logger.debug("Initializing HTTP session.")
|
|
122
|
+
|
|
123
|
+
self.client = requests.Session()
|
|
124
|
+
self.client.headers.update(
|
|
125
|
+
{
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
"Accept": "application/json",
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
if self.referral_id:
|
|
131
|
+
self.client.headers.update({"Referer": self.referral_id})
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def prepare_payload(method, parameters):
|
|
135
|
+
"""
|
|
136
|
+
Prepares the request payload and validates parameter value types.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def cast_values():
|
|
140
|
+
string_params = [
|
|
141
|
+
"qty",
|
|
142
|
+
"price",
|
|
143
|
+
"triggerPrice",
|
|
144
|
+
"takeProfit",
|
|
145
|
+
"stopLoss",
|
|
146
|
+
]
|
|
147
|
+
integer_params = ["positionIdx"]
|
|
148
|
+
for key, value in parameters.items():
|
|
149
|
+
if key in string_params:
|
|
150
|
+
if type(value) != str:
|
|
151
|
+
parameters[key] = str(value)
|
|
152
|
+
elif key in integer_params:
|
|
153
|
+
if type(value) != int:
|
|
154
|
+
parameters[key] = int(value)
|
|
155
|
+
|
|
156
|
+
if method == "GET":
|
|
157
|
+
payload = "&".join(
|
|
158
|
+
[
|
|
159
|
+
str(k) + "=" + str(v)
|
|
160
|
+
for k, v in sorted(parameters.items())
|
|
161
|
+
if v is not None
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
return payload
|
|
165
|
+
else:
|
|
166
|
+
cast_values()
|
|
167
|
+
return json.dumps(parameters)
|
|
168
|
+
|
|
169
|
+
def _auth(self, payload, recv_window, timestamp):
|
|
170
|
+
"""
|
|
171
|
+
Prepares authentication signature per Bybit API specifications.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
if self.api_key is None or self.api_secret is None:
|
|
175
|
+
raise PermissionError("Authenticated endpoints require keys.")
|
|
176
|
+
|
|
177
|
+
param_str = str(timestamp) + self.api_key + str(recv_window) + payload
|
|
178
|
+
|
|
179
|
+
return generate_signature(
|
|
180
|
+
self.rsa_authentication, self.api_secret, param_str
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _submit_request(self, method=None, path=None, query=None, auth=False):
|
|
184
|
+
"""
|
|
185
|
+
Submits the request to the API.
|
|
186
|
+
"""
|
|
187
|
+
query = self._clean_query(query)
|
|
188
|
+
recv_window = self.recv_window
|
|
189
|
+
retries_attempted = self.max_retries
|
|
190
|
+
|
|
191
|
+
while retries_attempted > 0:
|
|
192
|
+
retries_attempted -= 1
|
|
193
|
+
try:
|
|
194
|
+
req_params = self.prepare_payload(method, query)
|
|
195
|
+
headers = self._prepare_headers(req_params, recv_window) if auth else {}
|
|
196
|
+
|
|
197
|
+
request = self._prepare_request(method, path, req_params, headers)
|
|
198
|
+
self._log_request(method, path, req_params, request.headers)
|
|
199
|
+
|
|
200
|
+
response = self.client.send(request, timeout=self.timeout)
|
|
201
|
+
self._check_status_code(response, method, path, req_params)
|
|
202
|
+
|
|
203
|
+
return self._handle_response(response, method, path, req_params, recv_window, retries_attempted)
|
|
204
|
+
|
|
205
|
+
except _RetryableRequestError as e:
|
|
206
|
+
recv_window = e.recv_window
|
|
207
|
+
continue
|
|
208
|
+
except (requests.exceptions.ReadTimeout, requests.exceptions.SSLError,
|
|
209
|
+
requests.exceptions.ConnectionError) as e:
|
|
210
|
+
self._handle_network_error(e, retries_attempted)
|
|
211
|
+
except JSONDecodeError as e:
|
|
212
|
+
self._handle_json_error(e, retries_attempted)
|
|
213
|
+
|
|
214
|
+
raise FailedRequestError(
|
|
215
|
+
request=f"{method} {path}: {req_params}",
|
|
216
|
+
message="Bad Request. Retries exceeded maximum.",
|
|
217
|
+
status_code=400,
|
|
218
|
+
time=dt.now(timezone.utc).strftime("%H:%M:%S"),
|
|
219
|
+
resp_headers=None,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _clean_query(self, query):
|
|
223
|
+
"""Remove None values and fix floats."""
|
|
224
|
+
if query is None:
|
|
225
|
+
return {}
|
|
226
|
+
for key in list(query.keys()):
|
|
227
|
+
if isinstance(query[key], float) and query[key] == int(query[key]):
|
|
228
|
+
query[key] = int(query[key])
|
|
229
|
+
return {k: v for k, v in query.items() if v is not None}
|
|
230
|
+
|
|
231
|
+
def _prepare_headers(self, payload, recv_window):
|
|
232
|
+
"""Prepare headers for authenticated request."""
|
|
233
|
+
timestamp = _helpers.generate_timestamp()
|
|
234
|
+
signature = self._auth(payload=payload, recv_window=recv_window, timestamp=timestamp)
|
|
235
|
+
return {
|
|
236
|
+
"Content-Type": "application/json",
|
|
237
|
+
"X-BAPI-API-KEY": self.api_key,
|
|
238
|
+
"X-BAPI-SIGN": signature,
|
|
239
|
+
"X-BAPI-SIGN-TYPE": "2",
|
|
240
|
+
"X-BAPI-TIMESTAMP": str(timestamp),
|
|
241
|
+
"X-BAPI-RECV-WINDOW": str(recv_window),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def _prepare_request(self, method, path, params, headers):
|
|
245
|
+
"""Prepare request object."""
|
|
246
|
+
if method == "GET" and params:
|
|
247
|
+
return self.client.prepare_request(requests.Request(method, f"{path}?{params}", headers=headers))
|
|
248
|
+
return self.client.prepare_request(requests.Request(method, path, data=params, headers=headers))
|
|
249
|
+
|
|
250
|
+
def _log_request(self, method, path, params, headers):
|
|
251
|
+
"""Log request."""
|
|
252
|
+
if self.log_requests:
|
|
253
|
+
if params:
|
|
254
|
+
self.logger.debug(f"Request -> {method} {path}. Body: {params}. Headers: {headers}")
|
|
255
|
+
else:
|
|
256
|
+
self.logger.debug(f"Request -> {method} {path}. Headers: {headers}")
|
|
257
|
+
|
|
258
|
+
def _check_status_code(self, response, method, path, params):
|
|
259
|
+
"""Check HTTP status code."""
|
|
260
|
+
if response.status_code != 200:
|
|
261
|
+
error_msg = "You have breached the IP rate limit or your IP is from the USA."\
|
|
262
|
+
if response.status_code == 403 else "HTTP status code is not 200."
|
|
263
|
+
self.logger.debug(f"Response text: {response.text}")
|
|
264
|
+
raise FailedRequestError(
|
|
265
|
+
request=f"{method} {path}: {params}",
|
|
266
|
+
message=error_msg,
|
|
267
|
+
status_code=response.status_code,
|
|
268
|
+
time=dt.now(timezone.utc).strftime("%H:%M:%S"),
|
|
269
|
+
resp_headers=response.headers,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _handle_response(self, response, method, path, params, recv_window, retries_attempted):
|
|
273
|
+
"""Handle JSON response and Bybit error codes."""
|
|
274
|
+
try:
|
|
275
|
+
s_json = response.json()
|
|
276
|
+
except JSONDecodeError as e:
|
|
277
|
+
raise e # Will be caught by main loop to retry.
|
|
278
|
+
|
|
279
|
+
ret_code = "retCode"
|
|
280
|
+
ret_msg = "retMsg"
|
|
281
|
+
|
|
282
|
+
if s_json.get(ret_code):
|
|
283
|
+
error_code = s_json[ret_code]
|
|
284
|
+
error_msg = f"{s_json[ret_msg]} (ErrCode: {error_code})"
|
|
285
|
+
|
|
286
|
+
if error_code in self.retry_codes:
|
|
287
|
+
recv_window = self._handle_retryable_error(
|
|
288
|
+
response, error_code, error_msg, recv_window
|
|
289
|
+
)
|
|
290
|
+
raise _RetryableRequestError(recv_window)
|
|
291
|
+
|
|
292
|
+
if error_code not in self.ignore_codes:
|
|
293
|
+
raise InvalidRequestError(
|
|
294
|
+
request=f"{method} {path}: {params}",
|
|
295
|
+
message=s_json[ret_msg],
|
|
296
|
+
status_code=error_code,
|
|
297
|
+
time=dt.now(timezone.utc).strftime("%H:%M:%S"),
|
|
298
|
+
resp_headers=response.headers,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if self.log_requests:
|
|
302
|
+
self.logger.debug(f"Response headers: {response.headers}")
|
|
303
|
+
|
|
304
|
+
if self.return_response_headers:
|
|
305
|
+
return s_json, response.elapsed, response.headers
|
|
306
|
+
elif self.record_request_time:
|
|
307
|
+
return s_json, response.elapsed
|
|
308
|
+
else:
|
|
309
|
+
return s_json
|
|
310
|
+
|
|
311
|
+
def _handle_retryable_error(self, response, error_code, error_msg, recv_window):
|
|
312
|
+
"""Handle specific retryable Bybit errors."""
|
|
313
|
+
delay_time = self.retry_delay
|
|
314
|
+
|
|
315
|
+
if error_code == 10002: # recv_window error
|
|
316
|
+
error_msg += ". Added 2.5 seconds to recv_window"
|
|
317
|
+
recv_window += 2500
|
|
318
|
+
elif error_code == 10006: # rate limit error
|
|
319
|
+
self.logger.error(f"{error_msg}. Hit the API rate limit on {response.url}. Sleeping then trying again.")
|
|
320
|
+
limit_reset_time = int(
|
|
321
|
+
response.headers.get(
|
|
322
|
+
"X-Bapi-Limit-Reset-Timestamp",
|
|
323
|
+
_helpers.generate_timestamp() + 2000
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
limit_reset_str = dt.fromtimestamp(limit_reset_time / 10 ** 3).strftime("%H:%M:%S.%f")[:-3]
|
|
327
|
+
delay_time = (limit_reset_time - _helpers.generate_timestamp()) / 10 ** 3
|
|
328
|
+
error_msg = f"API rate limit will reset at {limit_reset_str}. Sleeping for {int(delay_time * 10 ** 3)} ms"
|
|
329
|
+
|
|
330
|
+
self.logger.error(f"{error_msg}. Retrying...")
|
|
331
|
+
time.sleep(delay_time)
|
|
332
|
+
return recv_window
|
|
333
|
+
|
|
334
|
+
def _handle_network_error(self, error, retries_attempted):
|
|
335
|
+
"""Handle network-related exceptions."""
|
|
336
|
+
if self.force_retry and retries_attempted > 0:
|
|
337
|
+
self.logger.error(f"{error}. Retrying...")
|
|
338
|
+
time.sleep(self.retry_delay)
|
|
339
|
+
else:
|
|
340
|
+
raise error
|
|
341
|
+
|
|
342
|
+
def _handle_json_error(self, error, retries_attempted):
|
|
343
|
+
"""Handle JSON decoding errors."""
|
|
344
|
+
if self.force_retry and retries_attempted > 0:
|
|
345
|
+
self.logger.error(f"{error}. Retrying JSON decode...")
|
|
346
|
+
time.sleep(self.retry_delay)
|
|
347
|
+
else:
|
|
348
|
+
raise FailedRequestError(
|
|
349
|
+
request="JSON decoding",
|
|
350
|
+
message="Conflict. Could not decode JSON.",
|
|
351
|
+
status_code=409,
|
|
352
|
+
time=dt.now(timezone.utc).strftime("%H:%M:%S"),
|
|
353
|
+
resp_headers=None,
|
|
354
|
+
)
|