bitvavo-api-upgraded 4.3.0__tar.gz → 4.4.0__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.3.0 → bitvavo_api_upgraded-4.4.0}/PKG-INFO +1 -1
  2. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/pyproject.toml +2 -2
  3. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/auth/rate_limit.py +65 -7
  4. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/transport/http.py +49 -1
  5. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/README.md +0 -0
  6. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/__init__.py +0 -0
  7. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/bitvavo.py +0 -0
  8. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
  9. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
  10. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/py.typed +0 -0
  11. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/settings.py +0 -0
  12. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
  13. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/__init__.py +0 -0
  14. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/adapters/__init__.py +0 -0
  15. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/adapters/returns_adapter.py +0 -0
  16. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/auth/__init__.py +0 -0
  17. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/auth/signing.py +0 -0
  18. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/__init__.py +0 -0
  19. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/errors.py +0 -0
  20. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/model_preferences.py +0 -0
  21. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/private_models.py +0 -0
  22. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/public_models.py +0 -0
  23. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/settings.py +0 -0
  24. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/types.py +0 -0
  25. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/core/validation_helpers.py +0 -0
  26. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/df/__init__.py +0 -0
  27. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/df/convert.py +0 -0
  28. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/endpoints/__init__.py +0 -0
  29. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/endpoints/base.py +0 -0
  30. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/endpoints/common.py +0 -0
  31. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/endpoints/private.py +0 -0
  32. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/endpoints/public.py +0 -0
  33. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/facade.py +0 -0
  34. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/py.typed +0 -0
  35. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/schemas/__init__.py +0 -0
  36. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/schemas/private_schemas.py +0 -0
  37. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/schemas/public_schemas.py +0 -0
  38. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/src/bitvavo_client/transport/__init__.py +0 -0
  39. {bitvavo_api_upgraded-4.3.0 → bitvavo_api_upgraded-4.4.0}/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.3.0
3
+ Version: 4.4.0
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.3.0"
9
+ version = "4.4.0"
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.3.0"
111
+ current_version = "4.4.0"
112
112
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
113
113
  serialize = ["{major}.{minor}.{patch}"]
114
114
  search = "{current_version}"
@@ -20,10 +20,9 @@ class DefaultRateLimitStrategy(RateLimitStrategy):
20
20
 
21
21
 
22
22
  class RateLimitManager:
23
- """Manages rate limiting for multiple API keys and keyless requests.
23
+ """Manages rate limiting for multiple API keys.
24
24
 
25
- Each API key index has its own rate limit state. Index -1 is reserved
26
- for keyless requests.
25
+ Each API key index has its own rate limit state.
27
26
  """
28
27
 
29
28
  def __init__(self, default_remaining: int, buffer: int, strategy: RateLimitStrategy | None = None) -> None:
@@ -35,7 +34,7 @@ class RateLimitManager:
35
34
  strategy: Optional strategy callback when rate limit exceeded
36
35
  """
37
36
  self.default_remaining: int = default_remaining
38
- self.state: dict[int, dict[str, int]] = {-1: {"remaining": default_remaining, "resetAt": 0}}
37
+ self.state: dict[int, dict[str, int]] = {}
39
38
  self.buffer: int = buffer
40
39
 
41
40
  self._strategy: RateLimitStrategy = strategy or DefaultRateLimitStrategy()
@@ -43,13 +42,13 @@ class RateLimitManager:
43
42
  def ensure_key(self, idx: int) -> None:
44
43
  """Ensure a key index exists in the state."""
45
44
  if idx not in self.state:
46
- self.state[idx] = {"remaining": self.state[-1]["remaining"], "resetAt": 0}
45
+ self.state[idx] = {"remaining": self.default_remaining, "resetAt": 0}
47
46
 
48
47
  def has_budget(self, idx: int, weight: int) -> bool:
49
48
  """Check if there's enough rate limit budget for a request.
50
49
 
51
50
  Args:
52
- idx: API key index (-1 for keyless)
51
+ idx: API key index
53
52
  weight: Weight of the request
54
53
 
55
54
  Returns:
@@ -66,7 +65,7 @@ class RateLimitManager:
66
65
  the response doesn't include rate limit headers.
67
66
 
68
67
  Args:
69
- idx: API key index (-1 for keyless)
68
+ idx: API key index
70
69
  weight: Weight of the request
71
70
  """
72
71
  self.ensure_key(idx)
@@ -144,3 +143,62 @@ class RateLimitManager:
144
143
  """
145
144
  self.ensure_key(idx)
146
145
  return self.state[idx]["resetAt"]
146
+
147
+ def find_best_available_key(self, available_keys: list[int], weight: int) -> int | None:
148
+ """Find the best API key for a request with given weight.
149
+
150
+ Prioritizes keys by:
151
+ 1. Keys with sufficient budget (remaining - weight >= buffer)
152
+ 2. Keys with the most remaining budget
153
+ 3. Keys with the earliest reset time (if all are rate limited)
154
+
155
+ Args:
156
+ available_keys: List of available key indices
157
+ weight: Weight of the request
158
+
159
+ Returns:
160
+ Best key index or None if no keys are suitable
161
+ """
162
+ if not available_keys:
163
+ return None
164
+
165
+ suitable_keys = []
166
+ fallback_keys = []
167
+
168
+ for idx in available_keys:
169
+ self.ensure_key(idx)
170
+ if self.has_budget(idx, weight):
171
+ suitable_keys.append((idx, self.state[idx]["remaining"]))
172
+ else:
173
+ fallback_keys.append((idx, self.state[idx]["resetAt"]))
174
+
175
+ # Return key with most remaining budget if any have sufficient budget
176
+ if suitable_keys:
177
+ return max(suitable_keys, key=lambda x: x[1])[0]
178
+
179
+ # If no keys have budget, return the one that resets earliest
180
+ if fallback_keys:
181
+ return min(fallback_keys, key=lambda x: x[1])[0]
182
+
183
+ return None
184
+
185
+ def get_earliest_reset_time(self, key_indices: list[int]) -> int:
186
+ """Get the earliest reset time among the given keys.
187
+
188
+ Args:
189
+ key_indices: List of key indices to check
190
+
191
+ Returns:
192
+ Earliest reset timestamp in milliseconds, or 0 if no keys have reset times
193
+ """
194
+ if not key_indices:
195
+ return 0
196
+
197
+ reset_times = []
198
+ for idx in key_indices:
199
+ self.ensure_key(idx)
200
+ reset_at = self.state[idx]["resetAt"]
201
+ if reset_at > 0: # Only consider keys that actually have a reset time
202
+ reset_times.append(reset_at)
203
+
204
+ return min(reset_times) if reset_times else 0
@@ -86,6 +86,44 @@ class HTTPClient:
86
86
  self.select_key(next_idx)
87
87
  return True
88
88
 
89
+ def _find_available_key(self, weight: int) -> int | None:
90
+ """Find the best available API key for a request with given weight.
91
+
92
+ Args:
93
+ weight: Weight of the request
94
+
95
+ Returns:
96
+ Best key index or None if all keys are rate limited
97
+ """
98
+ available_keys = list(range(len(self._keys)))
99
+ return self.rate_limiter.find_best_available_key(available_keys, weight)
100
+
101
+ def _handle_rate_limit_exhaustion(self, weight: int) -> None:
102
+ """Handle situation when all API keys are rate limited.
103
+
104
+ Sleeps until the earliest key reset time and then resets that key.
105
+
106
+ Args:
107
+ weight: Weight of the original request
108
+ """
109
+ all_keys = list(range(len(self._keys)))
110
+ earliest_reset = self.rate_limiter.get_earliest_reset_time(all_keys)
111
+
112
+ if earliest_reset > 0:
113
+ # Find which key has the earliest reset time
114
+ now = int(time.time() * 1000)
115
+ for idx in all_keys:
116
+ if self.rate_limiter.get_reset_at(idx) == earliest_reset:
117
+ if now < earliest_reset:
118
+ # Sleep until this key resets
119
+ self.rate_limiter.sleep_until_reset(idx)
120
+ self.rate_limiter.reset_key(idx)
121
+ self.select_key(idx)
122
+ return
123
+
124
+ # Fallback to current key's rate limit strategy
125
+ self.rate_limiter.handle_limit(self.key_index, weight)
126
+
89
127
  def request(
90
128
  self,
91
129
  method: str,
@@ -111,6 +149,13 @@ class HTTPClient:
111
149
  idx = self.key_index
112
150
  self._ensure_rate_limit_initialized()
113
151
 
152
+ # Try to find the best available key for this request
153
+ best_key = self._find_available_key(weight)
154
+ if best_key is not None and best_key != idx:
155
+ self.select_key(best_key)
156
+ idx = best_key
157
+
158
+ # If current key doesn't have budget, try rotation
114
159
  if not self.rate_limiter.has_budget(idx, weight):
115
160
  for _ in range(len(self._keys)):
116
161
  if self.rate_limiter.has_budget(idx, weight):
@@ -119,8 +164,11 @@ class HTTPClient:
119
164
  idx = self.key_index
120
165
  if not rotated:
121
166
  break
167
+
168
+ # If still no budget after trying all keys, handle exhaustion smartly
122
169
  if not self.rate_limiter.has_budget(idx, weight):
123
- self.rate_limiter.handle_limit(idx, weight)
170
+ self._handle_rate_limit_exhaustion(weight)
171
+ idx = self.key_index # Update idx after potential key change
124
172
 
125
173
  url = f"{self.settings.rest_url}{endpoint}"
126
174
  headers = self._create_auth_headers(method, endpoint, body)