bitvavo-api-upgraded 4.4.0__tar.gz → 4.4.1__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.
Files changed (39) hide show
  1. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/PKG-INFO +1 -1
  2. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/pyproject.toml +2 -2
  3. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/auth/rate_limit.py +126 -6
  4. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/settings.py +1 -1
  5. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/facade.py +2 -2
  6. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/transport/http.py +44 -2
  7. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/README.md +0 -0
  8. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/__init__.py +0 -0
  9. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/bitvavo.py +0 -0
  10. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
  11. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
  12. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/py.typed +0 -0
  13. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/settings.py +0 -0
  14. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
  15. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/__init__.py +0 -0
  16. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/adapters/__init__.py +0 -0
  17. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/adapters/returns_adapter.py +0 -0
  18. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/auth/__init__.py +0 -0
  19. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/auth/signing.py +0 -0
  20. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/__init__.py +0 -0
  21. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/errors.py +0 -0
  22. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/model_preferences.py +0 -0
  23. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/private_models.py +0 -0
  24. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/public_models.py +0 -0
  25. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/types.py +0 -0
  26. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/core/validation_helpers.py +0 -0
  27. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/df/__init__.py +0 -0
  28. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/df/convert.py +0 -0
  29. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/endpoints/__init__.py +0 -0
  30. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/endpoints/base.py +0 -0
  31. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/endpoints/common.py +0 -0
  32. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/endpoints/private.py +0 -0
  33. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/endpoints/public.py +0 -0
  34. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/py.typed +0 -0
  35. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/schemas/__init__.py +0 -0
  36. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/schemas/private_schemas.py +0 -0
  37. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/schemas/public_schemas.py +0 -0
  38. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/transport/__init__.py +0 -0
  39. {bitvavo_api_upgraded-4.4.0 → bitvavo_api_upgraded-4.4.1}/src/bitvavo_client/ws/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bitvavo-api-upgraded
3
- Version: 4.4.0
3
+ Version: 4.4.1
4
4
  Summary: A unit-tested fork of the Bitvavo API
5
5
  Author: Bitvavo BV (original code), NostraDavid
6
6
  Author-email: NostraDavid <55331731+NostraDavid@users.noreply.github.com>
@@ -6,7 +6,7 @@ build-backend = "uv_build"
6
6
  # https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
7
7
  [project]
8
8
  name = "bitvavo-api-upgraded"
9
- version = "4.4.0"
9
+ version = "4.4.1"
10
10
  description = "A unit-tested fork of the Bitvavo API"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -108,7 +108,7 @@ dev-dependencies = [
108
108
  ]
109
109
 
110
110
  [tool.bumpversion]
111
- current_version = "4.4.0"
111
+ current_version = "4.4.1"
112
112
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
113
113
  serialize = ["{major}.{minor}.{patch}"]
114
114
  search = "{current_version}"
@@ -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
- return (self.state[idx]["remaining"] - weight) >= self.buffer
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
- time.sleep(ms_left / 1000 + 1)
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
- return max(suitable_keys, key=lambda x: x[1])[0]
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
- return min(fallback_keys, key=lambda x: x[1])[0]
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
- return min(reset_times) if reset_times else 0
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
@@ -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=0, description="Rate limit buffer to avoid hitting limits")
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
 
@@ -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
 
@@ -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
- return self.rate_limiter.find_best_available_key(available_keys, weight)
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: