bitvavo-api-upgraded 4.4.0__py3-none-any.whl → 4.4.1__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.
- {bitvavo_api_upgraded-4.4.0.dist-info → bitvavo_api_upgraded-4.4.1.dist-info}/METADATA +1 -1
- {bitvavo_api_upgraded-4.4.0.dist-info → bitvavo_api_upgraded-4.4.1.dist-info}/RECORD +7 -7
- bitvavo_client/auth/rate_limit.py +126 -6
- bitvavo_client/core/settings.py +1 -1
- bitvavo_client/facade.py +2 -2
- bitvavo_client/transport/http.py +44 -2
- {bitvavo_api_upgraded-4.4.0.dist-info → bitvavo_api_upgraded-4.4.1.dist-info}/WHEEL +0 -0
@@ -9,14 +9,14 @@ bitvavo_client/__init__.py,sha256=YXTBdP6fBREV34VeTqS_gkjfzIoHv5uSYhbqSUEeAVU,20
|
|
9
9
|
bitvavo_client/adapters/__init__.py,sha256=9YVjMhNiAN6K1x7N0UvAXMQwuhOFy4W6Edrba1hW8KI,64
|
10
10
|
bitvavo_client/adapters/returns_adapter.py,sha256=3HSAPw6HB9GCS8AbKmeidURpZXnvMZqkvalOu6JhBv0,14195
|
11
11
|
bitvavo_client/auth/__init__.py,sha256=bjWu5WCKNNnNoLcVU290tKBml9M5afmcxaU_KrkisSQ,39
|
12
|
-
bitvavo_client/auth/rate_limit.py,sha256=
|
12
|
+
bitvavo_client/auth/rate_limit.py,sha256=iEfrzEvYvLQfhMSCy6epJQjDXyLkhcNFvH_zidW9omc,10559
|
13
13
|
bitvavo_client/auth/signing.py,sha256=DJrI1R1SLKjl276opj9hN4RrKIgsMhxsSEDA8b7T04I,1037
|
14
14
|
bitvavo_client/core/__init__.py,sha256=WqjaU9Ut5JdZwn4tsR1vDdrSfMjEJred3im6fvWpalc,39
|
15
15
|
bitvavo_client/core/errors.py,sha256=jWHHQKqkkhpHS9TeKlccl7wuyuRrq0H_PGZ0bl6sbW4,460
|
16
16
|
bitvavo_client/core/model_preferences.py,sha256=uPXjAD3B4UaBwzmhSN7k59oG71RGmV05y6-FGDKM184,1134
|
17
17
|
bitvavo_client/core/private_models.py,sha256=lttKQJQ6sVVwgFJ7WsnXim185KZUfjjuuQzsciPuEL8,33232
|
18
18
|
bitvavo_client/core/public_models.py,sha256=st1m1yOxutPhVQteJoRS72mzlQNeL7ynYjG1hqYg27w,37492
|
19
|
-
bitvavo_client/core/settings.py,sha256=
|
19
|
+
bitvavo_client/core/settings.py,sha256=EVvjxiecb7ju57JqOk0LNG_wjMmndrudvvPYoWFErxo,2147
|
20
20
|
bitvavo_client/core/types.py,sha256=wxqGlbBf6UFMMnRak4H8b7rNymQQuFqKFR8mkSsRig8,429
|
21
21
|
bitvavo_client/core/validation_helpers.py,sha256=2KeviuRXFiq4pgttHjC9q8gcrYmtLosSUrlvgnygMQY,3346
|
22
22
|
bitvavo_client/df/__init__.py,sha256=1ui3dsRhDvy0NOoOi4zj5gR48u828Au9K_qtH9S1hIo,44
|
@@ -26,14 +26,14 @@ bitvavo_client/endpoints/base.py,sha256=gZwUHG1XapoVvE9F7yqU8NLt2-QETwUsGSwcHkwO
|
|
26
26
|
bitvavo_client/endpoints/common.py,sha256=fc4gNNZ2zGMJJwkbHexNz6qMDLjl6dalQFGDXWmBo2E,2413
|
27
27
|
bitvavo_client/endpoints/private.py,sha256=yChMe-HL2wFm6GBYoghURjdLzSxCPTYlEIOjysCrn3E,34589
|
28
28
|
bitvavo_client/endpoints/public.py,sha256=EY7y3vuuo3li1BiPCM2KfSNnoa91X426DPtp8BvHiS8,21944
|
29
|
-
bitvavo_client/facade.py,sha256=
|
29
|
+
bitvavo_client/facade.py,sha256=nVAhmAtO53FfE_lODIjV3IjnMvkU3xGHuxUH5t-CLJk,1994
|
30
30
|
bitvavo_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
31
|
bitvavo_client/schemas/__init__.py,sha256=udqMyAFElrcBbNJIhoarQuTI-CF425JvPFqgLVjILU8,1126
|
32
32
|
bitvavo_client/schemas/private_schemas.py,sha256=cG-cV5HKO8ZvWp3hjUPBFMT6Th0UBumMViI8gAZTyik,6143
|
33
33
|
bitvavo_client/schemas/public_schemas.py,sha256=zfV6C_PQvNLLYEWS72ZD77Nm3XtRrEghKRhaFpgWHnI,4746
|
34
34
|
bitvavo_client/transport/__init__.py,sha256=H7txnyuz6v84_GzdBiqpsehVQitEymgUTA5AJPeUEvg,44
|
35
|
-
bitvavo_client/transport/http.py,sha256=
|
35
|
+
bitvavo_client/transport/http.py,sha256=4ch-_fC6UATzQAUfh4VAQJwjcnuNnPmFGaV7swotkkc,13741
|
36
36
|
bitvavo_client/ws/__init__.py,sha256=Q4SVEq3EihXLVUKpguMdxrhfNAoU8cncpMFbU6kIX_0,44
|
37
|
-
bitvavo_api_upgraded-4.4.
|
38
|
-
bitvavo_api_upgraded-4.4.
|
39
|
-
bitvavo_api_upgraded-4.4.
|
37
|
+
bitvavo_api_upgraded-4.4.1.dist-info/WHEEL,sha256=Pi5uDq5Fdo_Rr-HD5h9BiPn9Et29Y9Sh8NhcJNnFU1c,79
|
38
|
+
bitvavo_api_upgraded-4.4.1.dist-info/METADATA,sha256=GStFfK9ebWo5CEuTrc4N139mM8iWnfKfwo-wGGOsLFE,35640
|
39
|
+
bitvavo_api_upgraded-4.4.1.dist-info/RECORD,,
|
@@ -5,6 +5,10 @@ from __future__ import annotations
|
|
5
5
|
import time
|
6
6
|
from typing import Protocol
|
7
7
|
|
8
|
+
from structlog.stdlib import get_logger
|
9
|
+
|
10
|
+
logger = get_logger()
|
11
|
+
|
8
12
|
|
9
13
|
class RateLimitStrategy(Protocol):
|
10
14
|
"""Protocol for custom rate limit handling strategies."""
|
@@ -39,10 +43,22 @@ class RateLimitManager:
|
|
39
43
|
|
40
44
|
self._strategy: RateLimitStrategy = strategy or DefaultRateLimitStrategy()
|
41
45
|
|
46
|
+
logger.info(
|
47
|
+
"rate-limit-manager-initialized",
|
48
|
+
default_remaining=default_remaining,
|
49
|
+
buffer=buffer,
|
50
|
+
strategy=type(self._strategy).__name__,
|
51
|
+
)
|
52
|
+
|
42
53
|
def ensure_key(self, idx: int) -> None:
|
43
54
|
"""Ensure a key index exists in the state."""
|
44
55
|
if idx not in self.state:
|
45
56
|
self.state[idx] = {"remaining": self.default_remaining, "resetAt": 0}
|
57
|
+
logger.debug(
|
58
|
+
"rate-limit-key-initialized",
|
59
|
+
key_idx=idx,
|
60
|
+
default_remaining=self.default_remaining,
|
61
|
+
)
|
46
62
|
|
47
63
|
def has_budget(self, idx: int, weight: int) -> bool:
|
48
64
|
"""Check if there's enough rate limit budget for a request.
|
@@ -55,7 +71,18 @@ class RateLimitManager:
|
|
55
71
|
True if request can be made within rate limits
|
56
72
|
"""
|
57
73
|
self.ensure_key(idx)
|
58
|
-
|
74
|
+
has_budget = (self.state[idx]["remaining"] - weight) >= self.buffer
|
75
|
+
|
76
|
+
logger.debug(
|
77
|
+
"rate-limit-budget-check",
|
78
|
+
key_idx=idx,
|
79
|
+
weight=weight,
|
80
|
+
remaining=self.state[idx]["remaining"],
|
81
|
+
buffer=self.buffer,
|
82
|
+
has_budget=has_budget,
|
83
|
+
)
|
84
|
+
|
85
|
+
return has_budget
|
59
86
|
|
60
87
|
def record_call(self, idx: int, weight: int) -> None:
|
61
88
|
"""Record a request by decreasing the remaining budget.
|
@@ -69,8 +96,17 @@ class RateLimitManager:
|
|
69
96
|
weight: Weight of the request
|
70
97
|
"""
|
71
98
|
self.ensure_key(idx)
|
99
|
+
old_remaining = self.state[idx]["remaining"]
|
72
100
|
self.state[idx]["remaining"] = max(0, self.state[idx]["remaining"] - weight)
|
73
101
|
|
102
|
+
logger.debug(
|
103
|
+
"rate-limit-call-recorded",
|
104
|
+
key_idx=idx,
|
105
|
+
weight=weight,
|
106
|
+
old_remaining=old_remaining,
|
107
|
+
new_remaining=self.state[idx]["remaining"],
|
108
|
+
)
|
109
|
+
|
74
110
|
def update_from_headers(self, idx: int, headers: dict[str, str]) -> None:
|
75
111
|
"""Update rate limit state from response headers.
|
76
112
|
|
@@ -83,11 +119,24 @@ class RateLimitManager:
|
|
83
119
|
remaining = headers.get("bitvavo-ratelimit-remaining")
|
84
120
|
reset_at = headers.get("bitvavo-ratelimit-resetat")
|
85
121
|
|
122
|
+
old_state = dict(self.state[idx])
|
123
|
+
|
86
124
|
if remaining is not None:
|
87
125
|
self.state[idx]["remaining"] = int(remaining)
|
88
126
|
if reset_at is not None:
|
89
127
|
self.state[idx]["resetAt"] = int(reset_at)
|
90
128
|
|
129
|
+
logger.debug(
|
130
|
+
"rate-limit-updated-from-headers",
|
131
|
+
key_idx=idx,
|
132
|
+
old_remaining=old_state["remaining"],
|
133
|
+
new_remaining=self.state[idx]["remaining"],
|
134
|
+
old_reset_at=old_state["resetAt"],
|
135
|
+
new_reset_at=self.state[idx]["resetAt"],
|
136
|
+
has_remaining_header=remaining is not None,
|
137
|
+
has_reset_header=reset_at is not None,
|
138
|
+
)
|
139
|
+
|
91
140
|
def update_from_error(self, idx: int, _err: dict[str, object]) -> None:
|
92
141
|
"""Update rate limit state from API error response.
|
93
142
|
|
@@ -96,30 +145,73 @@ class RateLimitManager:
|
|
96
145
|
_err: Error response from API (unused but kept for interface compatibility)
|
97
146
|
"""
|
98
147
|
self.ensure_key(idx)
|
148
|
+
old_remaining = self.state[idx]["remaining"]
|
149
|
+
old_reset_at = self.state[idx]["resetAt"]
|
150
|
+
|
99
151
|
self.state[idx]["remaining"] = 0
|
100
152
|
self.state[idx]["resetAt"] = int(time.time() * 1000) + 60_000
|
101
153
|
|
154
|
+
logger.warning(
|
155
|
+
"rate-limit-updated-from-error",
|
156
|
+
key_idx=idx,
|
157
|
+
old_remaining=old_remaining,
|
158
|
+
old_reset_at=old_reset_at,
|
159
|
+
new_reset_at=self.state[idx]["resetAt"],
|
160
|
+
)
|
161
|
+
|
102
162
|
def sleep_until_reset(self, idx: int) -> None:
|
103
163
|
"""Sleep until rate limit resets for given key index.
|
104
164
|
|
105
165
|
Args:
|
106
166
|
idx: API key index
|
107
167
|
"""
|
168
|
+
|
108
169
|
self.ensure_key(idx)
|
109
170
|
now = int(time.time() * 1000)
|
110
171
|
ms_left = max(0, self.state[idx]["resetAt"] - now)
|
111
|
-
|
172
|
+
sleep_seconds = ms_left / 1000 + 1
|
173
|
+
|
174
|
+
logger.info(
|
175
|
+
"rate-limit-exceeded",
|
176
|
+
key_idx=idx,
|
177
|
+
sleep_seconds=sleep_seconds,
|
178
|
+
reset_at=self.state[idx]["resetAt"],
|
179
|
+
)
|
180
|
+
time.sleep(sleep_seconds)
|
181
|
+
|
182
|
+
logger.info(
|
183
|
+
"rate-limit-sleep-completed",
|
184
|
+
key_idx=idx,
|
185
|
+
slept_seconds=sleep_seconds,
|
186
|
+
)
|
112
187
|
|
113
188
|
def handle_limit(self, idx: int, weight: int) -> None:
|
114
189
|
"""Invoke the configured strategy when rate limit is exceeded."""
|
190
|
+
logger.info(
|
191
|
+
"rate-limit-handling-strategy",
|
192
|
+
key_idx=idx,
|
193
|
+
weight=weight,
|
194
|
+
strategy=type(self._strategy).__name__,
|
195
|
+
)
|
115
196
|
self._strategy(self, idx, weight)
|
116
197
|
|
117
198
|
def reset_key(self, idx: int) -> None:
|
118
199
|
"""Reset the remaining budget and reset time for a key index."""
|
119
200
|
self.ensure_key(idx)
|
201
|
+
old_remaining = self.state[idx]["remaining"]
|
202
|
+
old_reset_at = self.state[idx]["resetAt"]
|
203
|
+
|
120
204
|
self.state[idx]["remaining"] = self.default_remaining
|
121
205
|
self.state[idx]["resetAt"] = 0
|
122
206
|
|
207
|
+
logger.info(
|
208
|
+
"rate-limit-key-reset",
|
209
|
+
key_idx=idx,
|
210
|
+
old_remaining=old_remaining,
|
211
|
+
old_reset_at=old_reset_at,
|
212
|
+
new_remaining=self.default_remaining,
|
213
|
+
)
|
214
|
+
|
123
215
|
def get_remaining(self, idx: int) -> int:
|
124
216
|
"""Get remaining rate limit for key index.
|
125
217
|
|
@@ -160,6 +252,7 @@ class RateLimitManager:
|
|
160
252
|
Best key index or None if no keys are suitable
|
161
253
|
"""
|
162
254
|
if not available_keys:
|
255
|
+
logger.debug("rate-limit-no-keys-available", weight=weight)
|
163
256
|
return None
|
164
257
|
|
165
258
|
suitable_keys = []
|
@@ -174,12 +267,29 @@ class RateLimitManager:
|
|
174
267
|
|
175
268
|
# Return key with most remaining budget if any have sufficient budget
|
176
269
|
if suitable_keys:
|
177
|
-
|
270
|
+
best_key = max(suitable_keys, key=lambda x: x[1])[0]
|
271
|
+
logger.debug(
|
272
|
+
"rate-limit-best-key-found",
|
273
|
+
key_idx=best_key,
|
274
|
+
weight=weight,
|
275
|
+
remaining=self.state[best_key]["remaining"],
|
276
|
+
suitable_keys_count=len(suitable_keys),
|
277
|
+
)
|
278
|
+
return best_key
|
178
279
|
|
179
280
|
# If no keys have budget, return the one that resets earliest
|
180
281
|
if fallback_keys:
|
181
|
-
|
182
|
-
|
282
|
+
fallback_key = min(fallback_keys, key=lambda x: x[1])[0]
|
283
|
+
logger.warning(
|
284
|
+
"rate-limit-using-fallback-key",
|
285
|
+
key_idx=fallback_key,
|
286
|
+
weight=weight,
|
287
|
+
reset_at=self.state[fallback_key]["resetAt"],
|
288
|
+
fallback_keys_count=len(fallback_keys),
|
289
|
+
)
|
290
|
+
return fallback_key
|
291
|
+
|
292
|
+
logger.warning("rate-limit-no-suitable-keys", weight=weight, available_keys=available_keys)
|
183
293
|
return None
|
184
294
|
|
185
295
|
def get_earliest_reset_time(self, key_indices: list[int]) -> int:
|
@@ -192,6 +302,7 @@ class RateLimitManager:
|
|
192
302
|
Earliest reset timestamp in milliseconds, or 0 if no keys have reset times
|
193
303
|
"""
|
194
304
|
if not key_indices:
|
305
|
+
logger.debug("rate-limit-no-keys-for-reset-time")
|
195
306
|
return 0
|
196
307
|
|
197
308
|
reset_times = []
|
@@ -201,4 +312,13 @@ class RateLimitManager:
|
|
201
312
|
if reset_at > 0: # Only consider keys that actually have a reset time
|
202
313
|
reset_times.append(reset_at)
|
203
314
|
|
204
|
-
|
315
|
+
earliest_reset = min(reset_times) if reset_times else 0
|
316
|
+
|
317
|
+
logger.debug(
|
318
|
+
"rate-limit-earliest-reset-time",
|
319
|
+
key_indices=key_indices,
|
320
|
+
keys_with_reset=len(reset_times),
|
321
|
+
earliest_reset=earliest_reset,
|
322
|
+
)
|
323
|
+
|
324
|
+
return earliest_reset
|
bitvavo_client/core/settings.py
CHANGED
@@ -30,7 +30,7 @@ class BitvavoSettings(BaseSettings):
|
|
30
30
|
|
31
31
|
# Client behavior
|
32
32
|
default_rate_limit: int = Field(default=1_000, description="Default rate limit for new API keys")
|
33
|
-
rate_limit_buffer: int = Field(default=
|
33
|
+
rate_limit_buffer: int = Field(default=50, description="Rate limit buffer to avoid hitting limits")
|
34
34
|
lag_ms: int = Field(default=0, description="Artificial lag to add to requests in milliseconds")
|
35
35
|
debugging: bool = Field(default=False, description="Enable debug logging")
|
36
36
|
|
bitvavo_client/facade.py
CHANGED
@@ -38,8 +38,8 @@ class BitvavoClient:
|
|
38
38
|
"""
|
39
39
|
self.settings = settings or BitvavoSettings()
|
40
40
|
self.rate_limiter = RateLimitManager(
|
41
|
-
self.settings.default_rate_limit,
|
42
|
-
self.settings.rate_limit_buffer,
|
41
|
+
default_remaining=self.settings.default_rate_limit,
|
42
|
+
buffer=self.settings.rate_limit_buffer,
|
43
43
|
)
|
44
44
|
self.http = HTTPClient(self.settings, self.rate_limiter)
|
45
45
|
|
bitvavo_client/transport/http.py
CHANGED
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
8
8
|
import httpx
|
9
9
|
from returns.result import Failure, Result
|
10
|
+
from structlog.stdlib import get_logger
|
10
11
|
|
11
12
|
from bitvavo_client.adapters.returns_adapter import (
|
12
13
|
BitvavoError,
|
@@ -19,6 +20,8 @@ if TYPE_CHECKING: # pragma: no cover
|
|
19
20
|
from bitvavo_client.core.settings import BitvavoSettings
|
20
21
|
from bitvavo_client.core.types import AnyDict
|
21
22
|
|
23
|
+
logger = get_logger(__name__)
|
24
|
+
|
22
25
|
|
23
26
|
class HTTPClient:
|
24
27
|
"""HTTP client for Bitvavo REST API with rate limiting and authentication."""
|
@@ -30,11 +33,13 @@ class HTTPClient:
|
|
30
33
|
settings: Bitvavo settings configuration
|
31
34
|
rate_limiter: Rate limit manager instance
|
32
35
|
"""
|
36
|
+
|
33
37
|
self.settings: BitvavoSettings = settings
|
34
38
|
self.rate_limiter: RateLimitManager = rate_limiter
|
35
39
|
self._keys: list[tuple[str, str]] = [(item["key"], item["secret"]) for item in self.settings.api_keys]
|
36
40
|
if not self._keys:
|
37
41
|
msg = "API keys are required"
|
42
|
+
logger.error("api-keys-required")
|
38
43
|
raise ValueError(msg)
|
39
44
|
|
40
45
|
for idx in range(len(self._keys)):
|
@@ -47,6 +52,7 @@ class HTTPClient:
|
|
47
52
|
|
48
53
|
key, secret = self._keys[0]
|
49
54
|
self.configure_key(key, secret, 0)
|
55
|
+
logger.info("http-client-initialized", key_count=len(self._keys))
|
50
56
|
|
51
57
|
def configure_key(self, key: str, secret: str, index: int) -> None:
|
52
58
|
"""Configure API key for authenticated requests.
|
@@ -59,18 +65,23 @@ class HTTPClient:
|
|
59
65
|
self.api_key = key
|
60
66
|
self.api_secret = secret
|
61
67
|
self.key_index = index
|
68
|
+
key_suffix = key[-4:] if len(key) >= 4 else "short"
|
69
|
+
logger.debug("api-key-configured", index=index, key_suffix=key_suffix)
|
62
70
|
|
63
71
|
def select_key(self, index: int) -> None:
|
64
72
|
"""Select a specific API key by index."""
|
65
73
|
if not (0 <= index < len(self._keys)):
|
66
74
|
msg = "API key index out of range"
|
75
|
+
logger.error("api-key-index-out-of-range", index=index, available_keys=len(self._keys))
|
67
76
|
raise IndexError(msg)
|
68
77
|
key, secret = self._keys[index]
|
69
78
|
self.configure_key(key, secret, index)
|
79
|
+
logger.debug("api-key-selected", index=index)
|
70
80
|
|
71
81
|
def _rotate_key(self) -> bool:
|
72
82
|
"""Rotate to the next configured API key if available."""
|
73
83
|
if len(self._keys) <= 1:
|
84
|
+
logger.debug("key-rotation-skipped", reason="only-one-key")
|
74
85
|
return False
|
75
86
|
|
76
87
|
next_idx = (self.key_index + 1) % len(self._keys)
|
@@ -78,12 +89,15 @@ class HTTPClient:
|
|
78
89
|
reset_at = self.rate_limiter.get_reset_at(next_idx)
|
79
90
|
|
80
91
|
if now < reset_at:
|
92
|
+
logger.info("rotating-to-key-with-sleep", next_index=next_idx)
|
81
93
|
self.rate_limiter.sleep_until_reset(next_idx)
|
82
94
|
self.rate_limiter.reset_key(next_idx)
|
83
95
|
elif self.rate_limiter.get_remaining(next_idx) <= self.rate_limiter.buffer:
|
96
|
+
logger.debug("rotating-to-key-with-reset", next_index=next_idx, reason="low-remaining-budget")
|
84
97
|
self.rate_limiter.reset_key(next_idx)
|
85
98
|
|
86
99
|
self.select_key(next_idx)
|
100
|
+
logger.info("key-rotated", from_index=self.key_index, to_index=next_idx)
|
87
101
|
return True
|
88
102
|
|
89
103
|
def _find_available_key(self, weight: int) -> int | None:
|
@@ -96,7 +110,12 @@ class HTTPClient:
|
|
96
110
|
Best key index or None if all keys are rate limited
|
97
111
|
"""
|
98
112
|
available_keys = list(range(len(self._keys)))
|
99
|
-
|
113
|
+
best_key = self.rate_limiter.find_best_available_key(available_keys, weight)
|
114
|
+
if best_key is not None:
|
115
|
+
logger.debug("available-key-found", key_index=best_key, weight=weight)
|
116
|
+
else:
|
117
|
+
logger.warning("no-available-keys", weight=weight)
|
118
|
+
return best_key
|
100
119
|
|
101
120
|
def _handle_rate_limit_exhaustion(self, weight: int) -> None:
|
102
121
|
"""Handle situation when all API keys are rate limited.
|
@@ -106,6 +125,7 @@ class HTTPClient:
|
|
106
125
|
Args:
|
107
126
|
weight: Weight of the original request
|
108
127
|
"""
|
128
|
+
logger.warning("all-keys-rate-limited", weight=weight)
|
109
129
|
all_keys = list(range(len(self._keys)))
|
110
130
|
earliest_reset = self.rate_limiter.get_earliest_reset_time(all_keys)
|
111
131
|
|
@@ -116,12 +136,16 @@ class HTTPClient:
|
|
116
136
|
if self.rate_limiter.get_reset_at(idx) == earliest_reset:
|
117
137
|
if now < earliest_reset:
|
118
138
|
# Sleep until this key resets
|
139
|
+
sleep_ms = earliest_reset - now
|
140
|
+
logger.info("sleeping-until-key-reset", key_index=idx, sleep_ms=sleep_ms)
|
119
141
|
self.rate_limiter.sleep_until_reset(idx)
|
120
142
|
self.rate_limiter.reset_key(idx)
|
121
143
|
self.select_key(idx)
|
144
|
+
logger.info("key-reset-after-exhaustion", key_index=idx)
|
122
145
|
return
|
123
146
|
|
124
147
|
# Fallback to current key's rate limit strategy
|
148
|
+
logger.debug("fallback-to-current-key", key_index=self.key_index)
|
125
149
|
self.rate_limiter.handle_limit(self.key_index, weight)
|
126
150
|
|
127
151
|
def request(
|
@@ -146,17 +170,20 @@ class HTTPClient:
|
|
146
170
|
Raises:
|
147
171
|
HTTPError: On transport-level failures
|
148
172
|
"""
|
173
|
+
logger.debug("making-request", method=method, endpoint=endpoint, weight=weight)
|
149
174
|
idx = self.key_index
|
150
175
|
self._ensure_rate_limit_initialized()
|
151
176
|
|
152
177
|
# Try to find the best available key for this request
|
153
178
|
best_key = self._find_available_key(weight)
|
154
179
|
if best_key is not None and best_key != idx:
|
180
|
+
logger.debug("switching-to-best-key", from_index=idx, to_index=best_key)
|
155
181
|
self.select_key(best_key)
|
156
182
|
idx = best_key
|
157
183
|
|
158
184
|
# If current key doesn't have budget, try rotation
|
159
185
|
if not self.rate_limiter.has_budget(idx, weight):
|
186
|
+
logger.debug("key-lacks-budget", key_index=idx, weight=weight)
|
160
187
|
for _ in range(len(self._keys)):
|
161
188
|
if self.rate_limiter.has_budget(idx, weight):
|
162
189
|
break
|
@@ -167,6 +194,7 @@ class HTTPClient:
|
|
167
194
|
|
168
195
|
# If still no budget after trying all keys, handle exhaustion smartly
|
169
196
|
if not self.rate_limiter.has_budget(idx, weight):
|
197
|
+
logger.warning("no-budget-after-rotation", weight=weight)
|
170
198
|
self._handle_rate_limit_exhaustion(weight)
|
171
199
|
idx = self.key_index # Update idx after potential key change
|
172
200
|
|
@@ -175,10 +203,13 @@ class HTTPClient:
|
|
175
203
|
|
176
204
|
# Update rate limit usage for this call
|
177
205
|
self.rate_limiter.record_call(idx, weight)
|
206
|
+
logger.debug("weight-recorded", weight=weight, key_index=idx)
|
178
207
|
|
179
208
|
try:
|
180
209
|
response = self._make_http_request(method, url, headers, body)
|
210
|
+
logger.debug("request-completed", status_code=response.status_code)
|
181
211
|
except httpx.HTTPError as exc:
|
212
|
+
logger.error("http-request-failed", error=str(exc))
|
182
213
|
return Failure(exc)
|
183
214
|
|
184
215
|
self._update_rate_limits(response, idx)
|
@@ -189,6 +220,7 @@ class HTTPClient:
|
|
189
220
|
"""Ensure the initial rate limit state is fetched from the API."""
|
190
221
|
if self._rate_limit_initialized:
|
191
222
|
return
|
223
|
+
logger.info("initializing-rate-limit-state")
|
192
224
|
self._rate_limit_initialized = True
|
193
225
|
self._initialize_rate_limit()
|
194
226
|
|
@@ -196,6 +228,7 @@ class HTTPClient:
|
|
196
228
|
"""Fetch initial rate limit and handle potential rate limit errors."""
|
197
229
|
endpoint = "/account"
|
198
230
|
url = f"{self.settings.rest_url}{endpoint}"
|
231
|
+
logger.debug("fetching-initial-rate-limit")
|
199
232
|
|
200
233
|
while True:
|
201
234
|
headers = self._create_auth_headers("GET", endpoint, None)
|
@@ -204,7 +237,8 @@ class HTTPClient:
|
|
204
237
|
|
205
238
|
try:
|
206
239
|
response = self._make_http_request("GET", url, headers, None)
|
207
|
-
except httpx.HTTPError:
|
240
|
+
except httpx.HTTPError as exc:
|
241
|
+
logger.warning("rate-limit-init-failed", error=str(exc))
|
208
242
|
return
|
209
243
|
|
210
244
|
self._update_rate_limits(response, self.key_index)
|
@@ -220,15 +254,19 @@ class HTTPClient:
|
|
220
254
|
if isinstance(err, dict):
|
221
255
|
err_code = str(err.get("code", ""))
|
222
256
|
if response.status_code == httpx.codes.TOO_MANY_REQUESTS and err_code == "101":
|
257
|
+
logger.info("rate-limited-during-init", error_code=err_code)
|
223
258
|
self.rate_limiter.sleep_until_reset(self.key_index)
|
224
259
|
self.rate_limiter.reset_key(self.key_index)
|
225
260
|
continue
|
226
261
|
|
227
262
|
if self.rate_limiter.get_remaining(self.key_index) < self.rate_limiter.buffer:
|
263
|
+
remaining = self.rate_limiter.get_remaining(self.key_index)
|
264
|
+
logger.info("low-budget-during-init", remaining=remaining)
|
228
265
|
self.rate_limiter.sleep_until_reset(self.key_index)
|
229
266
|
self.rate_limiter.reset_key(self.key_index)
|
230
267
|
continue
|
231
268
|
|
269
|
+
logger.info("rate-limit-initialization-completed")
|
232
270
|
break
|
233
271
|
|
234
272
|
def _create_auth_headers(self, method: str, endpoint: str, body: AnyDict | None) -> dict[str, str]:
|
@@ -264,6 +302,7 @@ class HTTPClient:
|
|
264
302
|
return httpx.delete(url, headers=headers, timeout=timeout)
|
265
303
|
case _:
|
266
304
|
msg = f"Unsupported HTTP method: {method}"
|
305
|
+
logger.error("unsupported-http-method", method=method)
|
267
306
|
raise ValueError(msg)
|
268
307
|
|
269
308
|
def _update_rate_limits(self, response: httpx.Response, idx: int) -> None:
|
@@ -275,10 +314,13 @@ class HTTPClient:
|
|
275
314
|
|
276
315
|
if isinstance(json_data, dict) and "error" in json_data:
|
277
316
|
if self._is_rate_limit_error(response, json_data):
|
317
|
+
logger.warning("rate-limit-error-detected", key_index=idx)
|
278
318
|
self.rate_limiter.update_from_error(idx, json_data)
|
279
319
|
else:
|
320
|
+
logger.debug("non-rate-limit-error", key_index=idx)
|
280
321
|
self.rate_limiter.update_from_headers(idx, dict(response.headers))
|
281
322
|
else:
|
323
|
+
logger.debug("successful-response", key_index=idx)
|
282
324
|
self.rate_limiter.update_from_headers(idx, dict(response.headers))
|
283
325
|
|
284
326
|
def _is_rate_limit_error(self, response: httpx.Response, json_data: dict[str, Any]) -> bool:
|
File without changes
|