bitvavo-api-upgraded 4.2.1__py3-none-any.whl → 4.3.0__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.
@@ -167,7 +167,7 @@ class Bitvavo:
167
167
  )
168
168
  time_dict = bitvavo.time()
169
169
 
170
- # Multiple API keys with keyless preference
170
+ # Multiple API keys
171
171
  bitvavo = Bitvavo(
172
172
  {
173
173
  "APIKEYS": [
@@ -175,7 +175,6 @@ class Bitvavo:
175
175
  {"key": "$YOUR_API_KEY_2", "secret": "$YOUR_API_SECRET_2"},
176
176
  {"key": "$YOUR_API_KEY_3", "secret": "$YOUR_API_SECRET_3"},
177
177
  ],
178
- "PREFER_KEYLESS": True, # Use keyless requests first, then API keys
179
178
  "RESTURL": "https://api.bitvavo.com/v2",
180
179
  "WSURL": "wss://ws.bitvavo.com/v2/",
181
180
  "ACCESSWINDOW": 10000,
@@ -187,7 +186,6 @@ class Bitvavo:
187
186
  # Keyless only (no API keys)
188
187
  bitvavo = Bitvavo(
189
188
  {
190
- "PREFER_KEYLESS": True,
191
189
  "RESTURL": "https://api.bitvavo.com/v2",
192
190
  "WSURL": "wss://ws.bitvavo.com/v2/",
193
191
  "ACCESSWINDOW": 10000,
@@ -235,11 +233,14 @@ class Bitvavo:
235
233
  else:
236
234
  self.api_keys = []
237
235
 
238
- # Current API key index and keyless preference - options take precedence
236
+ if not self.api_keys:
237
+ msg = "API keys are required"
238
+ raise ValueError(msg)
239
+
240
+ # Current API key index - options take precedence
239
241
  self.current_api_key_index: int = 0
240
- self.prefer_keyless: bool = bool(_options.get("PREFER_KEYLESS", bitvavo_upgraded_settings.PREFER_KEYLESS))
241
242
 
242
- # Rate limiting per API key (keyless has index -1)
243
+ # Rate limiting per API key
243
244
  self.rate_limits: dict[int, dict[str, int | ms]] = {}
244
245
  # Get default rate limit from options or settings
245
246
  default_rate_limit_option = _options.get("DEFAULT_RATE_LIMIT", bitvavo_upgraded_settings.DEFAULT_RATE_LIMIT)
@@ -249,7 +250,6 @@ class Bitvavo:
249
250
  else bitvavo_upgraded_settings.DEFAULT_RATE_LIMIT
250
251
  )
251
252
 
252
- self.rate_limits[-1] = {"remaining": default_rate_limit, "resetAt": ms(0)} # keyless
253
253
  for i in range(len(self.api_keys)):
254
254
  self.rate_limits[i] = {"remaining": default_rate_limit, "resetAt": ms(0)}
255
255
 
@@ -265,36 +265,21 @@ class Bitvavo:
265
265
  self.debugging: bool = bool(_options.get("DEBUGGING", bitvavo_settings.DEBUGGING))
266
266
 
267
267
  def get_best_api_key_config(self, rateLimitingWeight: int = 1) -> tuple[str, str, int]:
268
- """
269
- Get the best API key configuration to use for a request.
268
+ """Get the best API key configuration to use for a request."""
270
269
 
271
- Returns:
272
- tuple: (api_key, api_secret, key_index) where key_index is -1 for keyless
273
- """
274
- # If prefer keyless and keyless has enough rate limit, use keyless
275
- if self.prefer_keyless and self._has_rate_limit_available(-1, rateLimitingWeight):
276
- return "", "", -1
277
-
278
- # Try to find an API key with enough rate limit
279
270
  for i in range(len(self.api_keys)):
280
271
  if self._has_rate_limit_available(i, rateLimitingWeight):
281
272
  return self.api_keys[i]["key"], self.api_keys[i]["secret"], i
282
273
 
283
- # If keyless is available, use it as fallback
284
- if self._has_rate_limit_available(-1, rateLimitingWeight):
285
- return "", "", -1
286
-
287
- # No keys available, use current key and let rate limiting handle the wait
288
- if self.api_keys:
289
- return (
290
- self.api_keys[self.current_api_key_index]["key"],
291
- self.api_keys[self.current_api_key_index]["secret"],
292
- self.current_api_key_index,
293
- )
294
- return "", "", -1
274
+ # No keys have available budget, use current key and let rate limiting handle the wait
275
+ return (
276
+ self.api_keys[self.current_api_key_index]["key"],
277
+ self.api_keys[self.current_api_key_index]["secret"],
278
+ self.current_api_key_index,
279
+ )
295
280
 
296
281
  def _has_rate_limit_available(self, key_index: int, weight: int) -> bool:
297
- """Check if a specific API key (or keyless) has enough rate limit."""
282
+ """Check if a specific API key has enough rate limit."""
298
283
  if key_index not in self.rate_limits:
299
284
  return False
300
285
  remaining = self.rate_limits[key_index]["remaining"]
@@ -320,7 +305,7 @@ class Bitvavo:
320
305
  self.rate_limits[key_index]["resetAt"] = ms(time_ms() + 60000)
321
306
 
322
307
  timeToWait = time_to_wait(ms(self.rate_limits[key_index]["resetAt"]))
323
- key_name = f"API_KEY_{key_index}" if key_index >= 0 else "KEYLESS"
308
+ key_name = f"API_KEY_{key_index}"
324
309
  logger.warning(
325
310
  "api-key-banned",
326
311
  info={
@@ -530,7 +515,7 @@ class Bitvavo:
530
515
  list[list[str]]
531
516
  ```
532
517
  """
533
- # Get the best API key configuration (keyless preferred, then available keys)
518
+ # Get the best API key configuration
534
519
  api_key, api_secret, key_index = self.get_best_api_key_config(rateLimitingWeight)
535
520
 
536
521
  # Check if we need to wait for rate limit
@@ -538,38 +523,25 @@ class Bitvavo:
538
523
  self._sleep_for_key(key_index)
539
524
 
540
525
  # Update current API key for legacy compatibility
541
- if api_key:
542
- self._current_api_key = api_key
543
- self._current_api_secret = api_secret
544
- self.current_api_key_index = key_index
545
- else:
546
- # Using keyless
547
- self._current_api_key = ""
548
- self._current_api_secret = ""
526
+ self._current_api_key = api_key
527
+ self._current_api_secret = api_secret
528
+ self.current_api_key_index = key_index
549
529
 
550
530
  if self.debugging:
551
531
  logger.debug(
552
532
  "api-request",
553
- info={
554
- "url": url,
555
- "with_api_key": bool(api_key != ""),
556
- "public_or_private": "public",
557
- "key_index": key_index,
558
- },
533
+ info={"url": url, "key_index": key_index},
559
534
  )
560
535
 
561
- if api_key:
562
- now = time_ms() + bitvavo_upgraded_settings.LAG
563
- sig = create_signature(now, "GET", url.replace(self.base, ""), None, api_secret)
564
- headers = {
565
- "bitvavo-access-key": api_key,
566
- "bitvavo-access-signature": sig,
567
- "bitvavo-access-timestamp": str(now),
568
- "bitvavo-access-window": str(self.ACCESSWINDOW),
569
- }
570
- r = get(url, headers=headers, timeout=(self.ACCESSWINDOW / 1000))
571
- else:
572
- r = get(url, timeout=(self.ACCESSWINDOW / 1000))
536
+ now = time_ms() + bitvavo_upgraded_settings.LAG
537
+ sig = create_signature(now, "GET", url.replace(self.base, ""), None, api_secret)
538
+ headers = {
539
+ "bitvavo-access-key": api_key,
540
+ "bitvavo-access-signature": sig,
541
+ "bitvavo-access-timestamp": str(now),
542
+ "bitvavo-access-window": str(self.ACCESSWINDOW),
543
+ }
544
+ r = get(url, headers=headers, timeout=(self.ACCESSWINDOW / 1000))
573
545
 
574
546
  # Update rate limit for the specific key used
575
547
  if "error" in r.json():
@@ -2371,7 +2343,7 @@ class Bitvavo:
2371
2343
  # Update rate limit tracking indices (shift them down)
2372
2344
  new_rate_limits = {}
2373
2345
  for key_idx, limits in self.rate_limits.items():
2374
- if key_idx == -1 or key_idx < i: # keyless
2346
+ if key_idx < i:
2375
2347
  new_rate_limits[key_idx] = limits
2376
2348
  elif key_idx > i:
2377
2349
  new_rate_limits[key_idx - 1] = limits
@@ -2389,19 +2361,10 @@ class Bitvavo:
2389
2361
  """Get the current status of all API keys including rate limits.
2390
2362
 
2391
2363
  Returns:
2392
- dict: Status information for keyless and all API keys
2364
+ dict: Status information for all API keys
2393
2365
  """
2394
2366
  status = {}
2395
2367
 
2396
- # Keyless status
2397
- keyless_limits = self.rate_limits.get(-1, {"remaining": 0, "resetAt": ms(0)})
2398
- status["keyless"] = {
2399
- "remaining": int(keyless_limits["remaining"]),
2400
- "resetAt": int(keyless_limits["resetAt"]),
2401
- "available": self._has_rate_limit_available(-1, 1),
2402
- }
2403
-
2404
- # API key status
2405
2368
  for i, key_data in enumerate(self.api_keys):
2406
2369
  key_limits = self.rate_limits.get(i, {"remaining": 0, "resetAt": ms(0)})
2407
2370
  KEY_LENGTH = 12
@@ -2419,15 +2382,6 @@ class Bitvavo:
2419
2382
 
2420
2383
  return status
2421
2384
 
2422
- def set_keyless_preference(self, prefer_keyless: bool) -> None: # noqa: FBT001 (Boolean-typed positional argument in function definition)
2423
- """Set whether to prefer keyless requests.
2424
-
2425
- Args:
2426
- prefer_keyless: If True, use keyless requests first when available
2427
- """
2428
- self.prefer_keyless = prefer_keyless
2429
- logger.info("keyless-preference-changed", prefer_keyless=prefer_keyless)
2430
-
2431
2385
  def get_current_config(self) -> dict[str, str | bool | int]:
2432
2386
  """Get the current configuration.
2433
2387
 
@@ -2437,7 +2391,6 @@ class Bitvavo:
2437
2391
  KEY_LENGTH = 12
2438
2392
  return {
2439
2393
  "api_key_count": len(self.api_keys),
2440
- "prefer_keyless": self.prefer_keyless,
2441
2394
  "current_api_key_index": self.current_api_key_index,
2442
2395
  "current_api_key": self._current_api_key[:8] + "..." + self._current_api_key[-4:]
2443
2396
  if len(self._current_api_key) > KEY_LENGTH
@@ -28,7 +28,6 @@ class BitvavoApiUpgradedSettings(BaseSettings):
28
28
  RATE_LIMITING_BUFFER: int = Field(default=25)
29
29
 
30
30
  # Multi-API key settings
31
- PREFER_KEYLESS: bool = Field(default=True, description="Prefer keyless requests over API key requests")
32
31
  DEFAULT_RATE_LIMIT: int = Field(default=1000, description="Default rate limit for new API keys")
33
32
 
34
33
  SSL_CERT_FILE: str | None = Field(
@@ -101,9 +100,6 @@ class BitvavoSettings(BaseSettings):
101
100
  RESTURL: str = Field(default="https://api.bitvavo.com/v2")
102
101
  WSURL: str = Field(default="wss://ws.bitvavo.com/v2/")
103
102
 
104
- # Multi-key specific settings
105
- PREFER_KEYLESS: bool = Field(default=True)
106
-
107
103
  # Configuration for Pydantic Settings
108
104
  model_config = SettingsConfigDict(
109
105
  env_file=Path.cwd() / ".env",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bitvavo-api-upgraded
3
- Version: 4.2.1
3
+ Version: 4.3.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>
@@ -214,7 +214,6 @@ BITVAVO_API_SECRET=your-api-secret-here
214
214
  # BITVAVO_API_KEYS='[{"key": "key1", "secret": "secret1"}, {"key": "key2", "secret": "secret2"}]'
215
215
 
216
216
  # Client behavior
217
- BITVAVO_PREFER_KEYLESS=true # Use keyless for public endpoints
218
217
  BITVAVO_DEFAULT_RATE_LIMIT=1000 # Rate limit per key
219
218
  BITVAVO_RATE_LIMIT_BUFFER=50 # Buffer to avoid hitting limits
220
219
  BITVAVO_DEBUGGING=false # Enable debug logging
@@ -238,7 +237,6 @@ client = BitvavoClient()
238
237
  settings = BitvavoSettings(
239
238
  api_key="your-key",
240
239
  api_secret="your-secret",
241
- prefer_keyless=True,
242
240
  debugging=True
243
241
  )
244
242
  client = BitvavoClient(settings)
@@ -279,7 +277,7 @@ bitvavo = Bitvavo({
279
277
  })
280
278
 
281
279
  # Option 4: Keyless (public endpoints only)
282
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
280
+ bitvavo = Bitvavo({})
283
281
  ```
284
282
 
285
283
  ## Data Format Flexibility
@@ -400,7 +398,7 @@ trades = bitvavo.getTrades('BTC-EUR', {}) # Automatic failover if rate limit re
400
398
  from bitvavo_api_upgraded import Bitvavo
401
399
 
402
400
  # No API keys needed for public data
403
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
401
+ bitvavo = Bitvavo({})
404
402
 
405
403
  # These work without authentication and don't count against your rate limits
406
404
  markets = bitvavo.markets({})
@@ -416,13 +414,12 @@ candles = bitvavo.candles('BTC-EUR', '1h', {})
416
414
  ### Hybrid Configuration
417
415
 
418
416
  ```python
419
- # Combine keyless preference with API keys for optimal performance
417
+ # Combine keyless access with API keys for optimal performance
420
418
  bitvavo = Bitvavo({
421
419
  'APIKEYS': [
422
420
  {'key': 'key1', 'secret': 'secret1'},
423
421
  {'key': 'key2', 'secret': 'secret2'}
424
- ],
425
- 'PREFER_KEYLESS': True # Use keyless for public endpoints, API keys for private
422
+ ]
426
423
  })
427
424
 
428
425
  # Public calls use keyless (no rate limit impact)
@@ -470,7 +467,7 @@ candles_result = client.public.candles('BTC-EUR', '1h')
470
467
  ```python
471
468
  from bitvavo_api_upgraded import Bitvavo
472
469
 
473
- bitvavo = Bitvavo({'PREFER_KEYLESS': True}) # For public endpoints
470
+ bitvavo = Bitvavo({}) # For public endpoints
474
471
 
475
472
  # Get server time
476
473
  time_resp = bitvavo.time()
@@ -705,7 +702,7 @@ for i in range(2000): # Would exceed single key limit
705
702
  markets = bitvavo_multi.markets({}) # Automatically switches keys
706
703
 
707
704
  # Keyless calls don't count against authenticated rate limits
708
- bitvavo_keyless = Bitvavo({'PREFER_KEYLESS': True})
705
+ bitvavo_keyless = Bitvavo({})
709
706
  markets = bitvavo_keyless.markets({}) # Uses public rate limit pool
710
707
  ```
711
708
 
@@ -886,7 +883,7 @@ result = client.private.balance()
886
883
  bitvavo = Bitvavo({'APIKEYS': [{'key': 'k1', 'secret': 's1'}, {'key': 'k2', 'secret': 's2'}]})
887
884
 
888
885
  # Keyless for public endpoints
889
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
886
+ bitvavo = Bitvavo({})
890
887
 
891
888
  # DataFrame support
892
889
  markets_df = bitvavo.markets({}, output_format='pandas')
@@ -1,39 +1,39 @@
1
1
  bitvavo_api_upgraded/__init__.py,sha256=J_HdGBmZOfb1eOydaxsPmXfOIZ58hVa1qAfE6QErUHs,301
2
- bitvavo_api_upgraded/bitvavo.py,sha256=_3FRVVPg7_1HrALyGPjcuokCsHF5oz6itN_GKx7yTMo,166155
2
+ bitvavo_api_upgraded/bitvavo.py,sha256=30AQBXNDVEKbmcDdEKknavb9M4JgD8_2WryKS19H49Q,163867
3
3
  bitvavo_api_upgraded/dataframe_utils.py,sha256=nGNHVhaCtnmwRYcZXoy4d5LREHeTDYxPmrixB08NcF4,5811
4
4
  bitvavo_api_upgraded/helper_funcs.py,sha256=4oBdQ1xB-C2XkQTmN-refzIzWfO-IUowDSWhOSFdCRU,3212
5
5
  bitvavo_api_upgraded/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- bitvavo_api_upgraded/settings.py,sha256=I1fogU6_kb1hOe_0YDzOgDhzKfnnYFoIR2OXbwtyD4E,5291
6
+ bitvavo_api_upgraded/settings.py,sha256=JTGcRxg2q0jm-3E5ApHlGA_leNid2lOp_sSH1bo8tOk,5101
7
7
  bitvavo_api_upgraded/type_aliases.py,sha256=SbPBcuKWJZPZ8DSDK-Uycu5O-TUO6ejVaTt_7oyGyIU,1979
8
8
  bitvavo_client/__init__.py,sha256=YXTBdP6fBREV34VeTqS_gkjfzIoHv5uSYhbqSUEeAVU,207
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=EfZtt4nxzu8D3Ea2ElYadrAvEYEt_Wlm60P8u-zVMvs,4391
12
+ bitvavo_client/auth/rate_limit.py,sha256=wv1UI733CVo8y5tI89CaUvomNpXLjbjSUh5vr9vn0ls,4949
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=H721f_EpMFrvk77SQLC3IOHN-UJecSX6uGlgV527PdA,2237
19
+ bitvavo_client/core/settings.py,sha256=jUu_8FQERkJneFHEOW6aHNeYX8DRRcBn_PXIkIipVtQ,2146
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
23
23
  bitvavo_client/df/convert.py,sha256=bf46QYmyB8o4KFdwUOfaxLV3_Qp29gq0L6hk3Qj-0pI,2522
24
24
  bitvavo_client/endpoints/__init__.py,sha256=X1e_Hn6xN7FBBLgKOpjdfIbKiXSp8f4gYSn8wRcn4ro,43
25
- bitvavo_client/endpoints/base.py,sha256=EyhxXJ2YVuXJeaIwgvF7VAzY-qVZ9maA282Xj0lSyIc,11977
25
+ bitvavo_client/endpoints/base.py,sha256=gZwUHG1XapoVvE9F7yqU8NLt2-QETwUsGSwcHkwOJ80,12042
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=y08585HMAhYGRI51dUbBezsTFOoZFKgpMZ4GA39r1ew,4368
29
+ bitvavo_client/facade.py,sha256=KZ8_yw9Ygtb8hK4_VIIwlrXGXIp43OJ4c-XncoYoaxE,1969
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=H5N-ZQbKwtKsGNadi6YB1gTCViZSns2TMLCGY0LiKgQ,6144
35
+ bitvavo_client/transport/http.py,sha256=hGablbfalBKAaJW9oQvz69oXpietBk7oGtCDZp1sQdc,9113
36
36
  bitvavo_client/ws/__init__.py,sha256=Q4SVEq3EihXLVUKpguMdxrhfNAoU8cncpMFbU6kIX_0,44
37
- bitvavo_api_upgraded-4.2.1.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
38
- bitvavo_api_upgraded-4.2.1.dist-info/METADATA,sha256=qil3_u24JDZf1uKRE22_kX87viD56lXcSZw4R6o8EdU,35937
39
- bitvavo_api_upgraded-4.2.1.dist-info/RECORD,,
37
+ bitvavo_api_upgraded-4.3.0.dist-info/WHEEL,sha256=F3mArEuDT3LDFEqo9fCiUx6ISLN64aIhcGSiIwtu4r8,79
38
+ bitvavo_api_upgraded-4.3.0.dist-info/METADATA,sha256=wzxm4ozq23mZI_CMvOsJUVTl0MrD1lCBx4-b0m7jQ8Q,35640
39
+ bitvavo_api_upgraded-4.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.15
2
+ Generator: uv 0.8.16
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -58,6 +58,20 @@ class RateLimitManager:
58
58
  self.ensure_key(idx)
59
59
  return (self.state[idx]["remaining"] - weight) >= self.buffer
60
60
 
61
+ def record_call(self, idx: int, weight: int) -> None:
62
+ """Record a request by decreasing the remaining budget.
63
+
64
+ This should be called whenever an API request is made to ensure
65
+ the local rate limit state reflects all outgoing calls, even when
66
+ the response doesn't include rate limit headers.
67
+
68
+ Args:
69
+ idx: API key index (-1 for keyless)
70
+ weight: Weight of the request
71
+ """
72
+ self.ensure_key(idx)
73
+ self.state[idx]["remaining"] = max(0, self.state[idx]["remaining"] - weight)
74
+
61
75
  def update_from_headers(self, idx: int, headers: dict[str, str]) -> None:
62
76
  """Update rate limit state from response headers.
63
77
 
@@ -29,7 +29,6 @@ class BitvavoSettings(BaseSettings):
29
29
  access_window_ms: int = Field(default=10_000, description="API access window in milliseconds")
30
30
 
31
31
  # Client behavior
32
- prefer_keyless: bool = Field(default=True, description="Prefer keyless requests when possible")
33
32
  default_rate_limit: int = Field(default=1_000, description="Default rate limit for new API keys")
34
33
  rate_limit_buffer: int = Field(default=0, description="Rate limit buffer to avoid hitting limits")
35
34
  lag_ms: int = Field(default=0, description="Artificial lag to add to requests in milliseconds")
@@ -41,7 +40,8 @@ class BitvavoSettings(BaseSettings):
41
40
 
42
41
  # Multiple API keys support
43
42
  api_keys: list[dict[str, str]] = Field(
44
- default_factory=list, description="List of API key/secret pairs for multi-key support"
43
+ default_factory=list,
44
+ description="List of API key/secret pairs for multi-key support",
45
45
  )
46
46
 
47
47
  @model_validator(mode="after")
@@ -13,6 +13,8 @@ from bitvavo_client.core.model_preferences import ModelPreference
13
13
  if TYPE_CHECKING:
14
14
  from collections.abc import Mapping
15
15
 
16
+ from bitvavo_client.transport.http import HTTPClient
17
+
16
18
  T = TypeVar("T")
17
19
 
18
20
 
@@ -214,7 +216,7 @@ class BaseAPI:
214
216
 
215
217
  def __init__(
216
218
  self,
217
- http_client: Any,
219
+ http_client: HTTPClient,
218
220
  *,
219
221
  preferred_model: ModelPreference | str | None = None,
220
222
  default_schema: Mapping[str, object] | None = None,
bitvavo_client/facade.py CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import time
6
- from typing import TYPE_CHECKING, TypeVar
5
+ from typing import TYPE_CHECKING
7
6
 
8
7
  from bitvavo_client.auth.rate_limit import RateLimitManager
9
8
  from bitvavo_client.core.settings import BitvavoSettings
@@ -14,8 +13,6 @@ from bitvavo_client.transport.http import HTTPClient
14
13
  if TYPE_CHECKING: # pragma: no cover
15
14
  from bitvavo_client.core.model_preferences import ModelPreference
16
15
 
17
- T = TypeVar("T")
18
-
19
16
 
20
17
  class BitvavoClient:
21
18
  """
@@ -47,71 +44,13 @@ class BitvavoClient:
47
44
  self.http = HTTPClient(self.settings, self.rate_limiter)
48
45
 
49
46
  # Initialize API endpoint handlers with preferred model settings
50
- self.public = PublicAPI(self.http, preferred_model=preferred_model, default_schema=default_schema)
51
- self.private = PrivateAPI(self.http, preferred_model=preferred_model, default_schema=default_schema)
52
-
53
- # Configure API keys if available
54
- self._api_keys: list[tuple[str, str]] = []
55
- self._current_key: int = -1
56
- self._configure_api_keys()
57
-
58
- def _configure_api_keys(self) -> None:
59
- """Configure API keys for authentication."""
60
- # Collect keys from settings
61
- if self.settings.api_key and self.settings.api_secret:
62
- self._api_keys.append((self.settings.api_key, self.settings.api_secret))
63
- if self.settings.api_keys:
64
- self._api_keys.extend((item["key"], item["secret"]) for item in self.settings.api_keys)
65
-
66
- if not self._api_keys:
67
- return
68
-
69
- for idx, (_key, _secret) in enumerate(self._api_keys):
70
- self.rate_limiter.ensure_key(idx)
71
-
72
- if self._api_keys:
73
- self.http.set_key_rotation_callback(self.rotate_key)
74
-
75
- def rotate_key(self) -> bool:
76
- """Rotate to the next configured API key if available."""
77
- if not self._api_keys:
78
- return False
79
-
80
- now = int(time.time() * 1000)
81
-
82
- if self._current_key == -1:
83
- idx = 0
84
- if now < self.rate_limiter.get_reset_at(idx):
85
- self.rate_limiter.sleep_until_reset(idx)
86
- self.rate_limiter.reset_key(idx)
87
- self._current_key = idx
88
- key, secret = self._api_keys[idx]
89
- self.http.configure_key(key, secret, idx)
90
- return True
91
-
92
- if self._current_key < len(self._api_keys) - 1:
93
- idx = self._current_key + 1
94
- if now < self.rate_limiter.get_reset_at(idx):
95
- self.rate_limiter.sleep_until_reset(idx)
96
- self.rate_limiter.reset_key(idx)
97
- self._current_key = idx
98
- key, secret = self._api_keys[idx]
99
- self.http.configure_key(key, secret, idx)
100
- return True
101
-
102
- reset_at = self.rate_limiter.get_reset_at(-1)
103
- if now < reset_at:
104
- self.rate_limiter.sleep_until_reset(-1)
105
- self.rate_limiter.reset_key(-1)
106
- self._current_key = -1
107
- self.http.configure_key("", "", -1)
108
- return True
109
-
110
- def select_key(self, index: int) -> None:
111
- """Select a specific API key by index."""
112
- if not (0 <= index < len(self._api_keys)):
113
- msg = "API key index out of range"
114
- raise IndexError(msg)
115
- self._current_key = index
116
- key, secret = self._api_keys[index]
117
- self.http.configure_key(key, secret, index)
47
+ self.public = PublicAPI(
48
+ self.http,
49
+ preferred_model=preferred_model,
50
+ default_schema=default_schema,
51
+ )
52
+ self.private = PrivateAPI(
53
+ self.http,
54
+ preferred_model=preferred_model,
55
+ default_schema=default_schema,
56
+ )
@@ -15,8 +15,6 @@ from bitvavo_client.adapters.returns_adapter import (
15
15
  from bitvavo_client.auth.signing import create_signature
16
16
 
17
17
  if TYPE_CHECKING: # pragma: no cover
18
- from collections.abc import Callable
19
-
20
18
  from bitvavo_client.auth.rate_limit import RateLimitManager
21
19
  from bitvavo_client.core.settings import BitvavoSettings
22
20
  from bitvavo_client.core.types import AnyDict
@@ -34,10 +32,21 @@ class HTTPClient:
34
32
  """
35
33
  self.settings: BitvavoSettings = settings
36
34
  self.rate_limiter: RateLimitManager = rate_limiter
37
- self.key_index: int = -1
35
+ self._keys: list[tuple[str, str]] = [(item["key"], item["secret"]) for item in self.settings.api_keys]
36
+ if not self._keys:
37
+ msg = "API keys are required"
38
+ raise ValueError(msg)
39
+
40
+ for idx in range(len(self._keys)):
41
+ self.rate_limiter.ensure_key(idx)
42
+
43
+ self.key_index: int = 0
38
44
  self.api_key: str = ""
39
45
  self.api_secret: str = ""
40
- self.key_rotation_callback: Callable[[], bool] | None = None
46
+ self._rate_limit_initialized: bool = False
47
+
48
+ key, secret = self._keys[0]
49
+ self.configure_key(key, secret, 0)
41
50
 
42
51
  def configure_key(self, key: str, secret: str, index: int) -> None:
43
52
  """Configure API key for authenticated requests.
@@ -51,9 +60,31 @@ class HTTPClient:
51
60
  self.api_secret = secret
52
61
  self.key_index = index
53
62
 
54
- def set_key_rotation_callback(self, callback: Callable[[], bool]) -> None:
55
- """Set callback to rotate API keys when rate limit exceeded."""
56
- self.key_rotation_callback = callback
63
+ def select_key(self, index: int) -> None:
64
+ """Select a specific API key by index."""
65
+ if not (0 <= index < len(self._keys)):
66
+ msg = "API key index out of range"
67
+ raise IndexError(msg)
68
+ key, secret = self._keys[index]
69
+ self.configure_key(key, secret, index)
70
+
71
+ def _rotate_key(self) -> bool:
72
+ """Rotate to the next configured API key if available."""
73
+ if len(self._keys) <= 1:
74
+ return False
75
+
76
+ next_idx = (self.key_index + 1) % len(self._keys)
77
+ now = int(time.time() * 1000)
78
+ reset_at = self.rate_limiter.get_reset_at(next_idx)
79
+
80
+ if now < reset_at:
81
+ self.rate_limiter.sleep_until_reset(next_idx)
82
+ self.rate_limiter.reset_key(next_idx)
83
+ elif self.rate_limiter.get_remaining(next_idx) <= self.rate_limiter.buffer:
84
+ self.rate_limiter.reset_key(next_idx)
85
+
86
+ self.select_key(next_idx)
87
+ return True
57
88
 
58
89
  def request(
59
90
  self,
@@ -77,43 +108,92 @@ class HTTPClient:
77
108
  Raises:
78
109
  HTTPError: On transport-level failures
79
110
  """
80
- # Check rate limits
81
- if not self.rate_limiter.has_budget(self.key_index, weight):
82
- rotated = False
83
- if self.key_rotation_callback:
84
- rotated = self.key_rotation_callback()
85
- if not rotated or not self.rate_limiter.has_budget(self.key_index, weight):
86
- self.rate_limiter.handle_limit(self.key_index, weight)
111
+ idx = self.key_index
112
+ self._ensure_rate_limit_initialized()
113
+
114
+ if not self.rate_limiter.has_budget(idx, weight):
115
+ for _ in range(len(self._keys)):
116
+ if self.rate_limiter.has_budget(idx, weight):
117
+ break
118
+ rotated = self._rotate_key()
119
+ idx = self.key_index
120
+ if not rotated:
121
+ break
122
+ if not self.rate_limiter.has_budget(idx, weight):
123
+ self.rate_limiter.handle_limit(idx, weight)
87
124
 
88
125
  url = f"{self.settings.rest_url}{endpoint}"
89
126
  headers = self._create_auth_headers(method, endpoint, body)
90
127
 
128
+ # Update rate limit usage for this call
129
+ self.rate_limiter.record_call(idx, weight)
130
+
91
131
  try:
92
132
  response = self._make_http_request(method, url, headers, body)
93
133
  except httpx.HTTPError as exc:
94
134
  return Failure(exc)
95
135
 
96
- self._update_rate_limits(response)
136
+ self._update_rate_limits(response, idx)
97
137
  # Always return raw data - let the caller handle model conversion
98
138
  return decode_response_result(response, model=Any)
99
139
 
140
+ def _ensure_rate_limit_initialized(self) -> None:
141
+ """Ensure the initial rate limit state is fetched from the API."""
142
+ if self._rate_limit_initialized:
143
+ return
144
+ self._rate_limit_initialized = True
145
+ self._initialize_rate_limit()
146
+
147
+ def _initialize_rate_limit(self) -> None:
148
+ """Fetch initial rate limit and handle potential rate limit errors."""
149
+ endpoint = "/account"
150
+ url = f"{self.settings.rest_url}{endpoint}"
151
+
152
+ while True:
153
+ headers = self._create_auth_headers("GET", endpoint, None)
154
+ # Record the weight for this check (weight 1)
155
+ self.rate_limiter.record_call(self.key_index, 1)
156
+
157
+ try:
158
+ response = self._make_http_request("GET", url, headers, None)
159
+ except httpx.HTTPError:
160
+ return
161
+
162
+ self._update_rate_limits(response, self.key_index)
163
+
164
+ err_code = ""
165
+ if response.status_code == httpx.codes.TOO_MANY_REQUESTS:
166
+ try:
167
+ data = response.json()
168
+ except ValueError:
169
+ data = {}
170
+ if isinstance(data, dict):
171
+ err = data.get("error")
172
+ if isinstance(err, dict):
173
+ err_code = str(err.get("code", ""))
174
+ if response.status_code == httpx.codes.TOO_MANY_REQUESTS and err_code == "101":
175
+ self.rate_limiter.sleep_until_reset(self.key_index)
176
+ self.rate_limiter.reset_key(self.key_index)
177
+ continue
178
+
179
+ if self.rate_limiter.get_remaining(self.key_index) < self.rate_limiter.buffer:
180
+ self.rate_limiter.sleep_until_reset(self.key_index)
181
+ self.rate_limiter.reset_key(self.key_index)
182
+ continue
183
+
184
+ break
185
+
100
186
  def _create_auth_headers(self, method: str, endpoint: str, body: AnyDict | None) -> dict[str, str]:
101
187
  """Create authentication headers if API key is configured."""
102
- headers: dict[str, str] = {}
103
-
104
- if self.api_key:
105
- timestamp = int(time.time() * 1000) + self.settings.lag_ms
106
- signature = create_signature(timestamp, method, endpoint, body, self.api_secret)
107
-
108
- headers.update(
109
- {
110
- "bitvavo-access-key": self.api_key,
111
- "bitvavo-access-signature": signature,
112
- "bitvavo-access-timestamp": str(timestamp),
113
- "bitvavo-access-window": str(self.settings.access_window_ms),
114
- },
115
- )
116
- return headers
188
+ timestamp = int(time.time() * 1000) + self.settings.lag_ms
189
+ signature = create_signature(timestamp, method, endpoint, body, self.api_secret)
190
+
191
+ return {
192
+ "bitvavo-access-key": self.api_key,
193
+ "bitvavo-access-signature": signature,
194
+ "bitvavo-access-timestamp": str(timestamp),
195
+ "bitvavo-access-window": str(self.settings.access_window_ms),
196
+ }
117
197
 
118
198
  def _make_http_request(
119
199
  self,
@@ -138,7 +218,7 @@ class HTTPClient:
138
218
  msg = f"Unsupported HTTP method: {method}"
139
219
  raise ValueError(msg)
140
220
 
141
- def _update_rate_limits(self, response: httpx.Response) -> None:
221
+ def _update_rate_limits(self, response: httpx.Response, idx: int) -> None:
142
222
  """Update rate limits based on response."""
143
223
  try:
144
224
  json_data = response.json()
@@ -147,11 +227,11 @@ class HTTPClient:
147
227
 
148
228
  if isinstance(json_data, dict) and "error" in json_data:
149
229
  if self._is_rate_limit_error(response, json_data):
150
- self.rate_limiter.update_from_error(self.key_index, json_data)
230
+ self.rate_limiter.update_from_error(idx, json_data)
151
231
  else:
152
- self.rate_limiter.update_from_headers(self.key_index, dict(response.headers))
232
+ self.rate_limiter.update_from_headers(idx, dict(response.headers))
153
233
  else:
154
- self.rate_limiter.update_from_headers(self.key_index, dict(response.headers))
234
+ self.rate_limiter.update_from_headers(idx, dict(response.headers))
155
235
 
156
236
  def _is_rate_limit_error(self, response: httpx.Response, json_data: dict[str, Any]) -> bool:
157
237
  """Check if response indicates a rate limit error."""