bitvavo-api-upgraded 4.2.1__tar.gz → 4.3.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 (41) hide show
  1. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/PKG-INFO +8 -11
  2. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/README.md +7 -10
  3. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/pyproject.toml +2 -2
  4. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/bitvavo.py +32 -79
  5. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/settings.py +0 -4
  6. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/rate_limit.py +14 -0
  7. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/settings.py +2 -2
  8. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/base.py +3 -1
  9. bitvavo_api_upgraded-4.3.0/src/bitvavo_client/facade.py +56 -0
  10. bitvavo_api_upgraded-4.3.0/src/bitvavo_client/transport/http.py +250 -0
  11. bitvavo_api_upgraded-4.2.1/src/bitvavo_client/facade.py +0 -117
  12. bitvavo_api_upgraded-4.2.1/src/bitvavo_client/transport/http.py +0 -170
  13. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/__init__.py +0 -0
  14. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
  15. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
  16. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/py.typed +0 -0
  17. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
  18. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/__init__.py +0 -0
  19. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/adapters/__init__.py +0 -0
  20. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/adapters/returns_adapter.py +0 -0
  21. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/__init__.py +0 -0
  22. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/signing.py +0 -0
  23. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/__init__.py +0 -0
  24. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/errors.py +0 -0
  25. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/model_preferences.py +0 -0
  26. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/private_models.py +0 -0
  27. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/public_models.py +0 -0
  28. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/types.py +0 -0
  29. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/validation_helpers.py +0 -0
  30. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/df/__init__.py +0 -0
  31. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/df/convert.py +0 -0
  32. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/__init__.py +0 -0
  33. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/common.py +0 -0
  34. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/private.py +0 -0
  35. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/public.py +0 -0
  36. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/py.typed +0 -0
  37. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/__init__.py +0 -0
  38. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/private_schemas.py +0 -0
  39. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/public_schemas.py +0 -0
  40. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/transport/__init__.py +0 -0
  41. {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.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.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')
@@ -150,7 +150,6 @@ BITVAVO_API_SECRET=your-api-secret-here
150
150
  # BITVAVO_API_KEYS='[{"key": "key1", "secret": "secret1"}, {"key": "key2", "secret": "secret2"}]'
151
151
 
152
152
  # Client behavior
153
- BITVAVO_PREFER_KEYLESS=true # Use keyless for public endpoints
154
153
  BITVAVO_DEFAULT_RATE_LIMIT=1000 # Rate limit per key
155
154
  BITVAVO_RATE_LIMIT_BUFFER=50 # Buffer to avoid hitting limits
156
155
  BITVAVO_DEBUGGING=false # Enable debug logging
@@ -174,7 +173,6 @@ client = BitvavoClient()
174
173
  settings = BitvavoSettings(
175
174
  api_key="your-key",
176
175
  api_secret="your-secret",
177
- prefer_keyless=True,
178
176
  debugging=True
179
177
  )
180
178
  client = BitvavoClient(settings)
@@ -215,7 +213,7 @@ bitvavo = Bitvavo({
215
213
  })
216
214
 
217
215
  # Option 4: Keyless (public endpoints only)
218
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
216
+ bitvavo = Bitvavo({})
219
217
  ```
220
218
 
221
219
  ## Data Format Flexibility
@@ -336,7 +334,7 @@ trades = bitvavo.getTrades('BTC-EUR', {}) # Automatic failover if rate limit re
336
334
  from bitvavo_api_upgraded import Bitvavo
337
335
 
338
336
  # No API keys needed for public data
339
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
337
+ bitvavo = Bitvavo({})
340
338
 
341
339
  # These work without authentication and don't count against your rate limits
342
340
  markets = bitvavo.markets({})
@@ -352,13 +350,12 @@ candles = bitvavo.candles('BTC-EUR', '1h', {})
352
350
  ### Hybrid Configuration
353
351
 
354
352
  ```python
355
- # Combine keyless preference with API keys for optimal performance
353
+ # Combine keyless access with API keys for optimal performance
356
354
  bitvavo = Bitvavo({
357
355
  'APIKEYS': [
358
356
  {'key': 'key1', 'secret': 'secret1'},
359
357
  {'key': 'key2', 'secret': 'secret2'}
360
- ],
361
- 'PREFER_KEYLESS': True # Use keyless for public endpoints, API keys for private
358
+ ]
362
359
  })
363
360
 
364
361
  # Public calls use keyless (no rate limit impact)
@@ -406,7 +403,7 @@ candles_result = client.public.candles('BTC-EUR', '1h')
406
403
  ```python
407
404
  from bitvavo_api_upgraded import Bitvavo
408
405
 
409
- bitvavo = Bitvavo({'PREFER_KEYLESS': True}) # For public endpoints
406
+ bitvavo = Bitvavo({}) # For public endpoints
410
407
 
411
408
  # Get server time
412
409
  time_resp = bitvavo.time()
@@ -641,7 +638,7 @@ for i in range(2000): # Would exceed single key limit
641
638
  markets = bitvavo_multi.markets({}) # Automatically switches keys
642
639
 
643
640
  # Keyless calls don't count against authenticated rate limits
644
- bitvavo_keyless = Bitvavo({'PREFER_KEYLESS': True})
641
+ bitvavo_keyless = Bitvavo({})
645
642
  markets = bitvavo_keyless.markets({}) # Uses public rate limit pool
646
643
  ```
647
644
 
@@ -822,7 +819,7 @@ result = client.private.balance()
822
819
  bitvavo = Bitvavo({'APIKEYS': [{'key': 'k1', 'secret': 's1'}, {'key': 'k2', 'secret': 's2'}]})
823
820
 
824
821
  # Keyless for public endpoints
825
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
822
+ bitvavo = Bitvavo({})
826
823
 
827
824
  # DataFrame support
828
825
  markets_df = bitvavo.markets({}, output_format='pandas')
@@ -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.2.1"
9
+ version = "4.3.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.2.1"
111
+ current_version = "4.3.0"
112
112
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
113
113
  serialize = ["{major}.{minor}.{patch}"]
114
114
  search = "{current_version}"
@@ -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",
@@ -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,
@@ -0,0 +1,56 @@
1
+ """Main facade for the Bitvavo client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from bitvavo_client.auth.rate_limit import RateLimitManager
8
+ from bitvavo_client.core.settings import BitvavoSettings
9
+ from bitvavo_client.endpoints.private import PrivateAPI
10
+ from bitvavo_client.endpoints.public import PublicAPI
11
+ from bitvavo_client.transport.http import HTTPClient
12
+
13
+ if TYPE_CHECKING: # pragma: no cover
14
+ from bitvavo_client.core.model_preferences import ModelPreference
15
+
16
+
17
+ class BitvavoClient:
18
+ """
19
+ Main Bitvavo API client facade providing backward-compatible interface.
20
+
21
+ TODO(NostraDavid): add mechanisms to get a ton of data efficiently, which then uses the public and private APIs.
22
+ Otherwise, users can just grab the data themselves via the public and private API endpoints.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ settings: BitvavoSettings | None = None,
28
+ *,
29
+ preferred_model: ModelPreference | str | None = None,
30
+ default_schema: dict | None = None,
31
+ ) -> None:
32
+ """Initialize Bitvavo client.
33
+
34
+ Args:
35
+ settings: Optional settings override. If None, uses defaults.
36
+ preferred_model: Preferred model format for responses
37
+ default_schema: Default schema for DataFrame conversion
38
+ """
39
+ self.settings = settings or BitvavoSettings()
40
+ self.rate_limiter = RateLimitManager(
41
+ self.settings.default_rate_limit,
42
+ self.settings.rate_limit_buffer,
43
+ )
44
+ self.http = HTTPClient(self.settings, self.rate_limiter)
45
+
46
+ # Initialize API endpoint handlers with preferred model settings
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
+ )
@@ -0,0 +1,250 @@
1
+ """HTTP client for Bitvavo API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import httpx
9
+ from returns.result import Failure, Result
10
+
11
+ from bitvavo_client.adapters.returns_adapter import (
12
+ BitvavoError,
13
+ decode_response_result,
14
+ )
15
+ from bitvavo_client.auth.signing import create_signature
16
+
17
+ if TYPE_CHECKING: # pragma: no cover
18
+ from bitvavo_client.auth.rate_limit import RateLimitManager
19
+ from bitvavo_client.core.settings import BitvavoSettings
20
+ from bitvavo_client.core.types import AnyDict
21
+
22
+
23
+ class HTTPClient:
24
+ """HTTP client for Bitvavo REST API with rate limiting and authentication."""
25
+
26
+ def __init__(self, settings: BitvavoSettings, rate_limiter: RateLimitManager) -> None:
27
+ """Initialize HTTP client.
28
+
29
+ Args:
30
+ settings: Bitvavo settings configuration
31
+ rate_limiter: Rate limit manager instance
32
+ """
33
+ self.settings: BitvavoSettings = settings
34
+ self.rate_limiter: RateLimitManager = rate_limiter
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
44
+ self.api_key: str = ""
45
+ self.api_secret: str = ""
46
+ self._rate_limit_initialized: bool = False
47
+
48
+ key, secret = self._keys[0]
49
+ self.configure_key(key, secret, 0)
50
+
51
+ def configure_key(self, key: str, secret: str, index: int) -> None:
52
+ """Configure API key for authenticated requests.
53
+
54
+ Args:
55
+ key: API key
56
+ secret: API secret
57
+ index: Key index for rate limiting
58
+ """
59
+ self.api_key = key
60
+ self.api_secret = secret
61
+ self.key_index = index
62
+
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
88
+
89
+ def request(
90
+ self,
91
+ method: str,
92
+ endpoint: str,
93
+ *,
94
+ body: AnyDict | None = None,
95
+ weight: int = 1,
96
+ ) -> Result[Any, BitvavoError | httpx.HTTPError]:
97
+ """Make HTTP request and return raw JSON data as a Result.
98
+
99
+ Args:
100
+ method: HTTP method (GET, POST, PUT, DELETE)
101
+ endpoint: API endpoint path
102
+ body: Request body for POST/PUT requests
103
+ weight: Rate limit weight of the request
104
+
105
+ Returns:
106
+ Result containing raw JSON response or error
107
+
108
+ Raises:
109
+ HTTPError: On transport-level failures
110
+ """
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)
124
+
125
+ url = f"{self.settings.rest_url}{endpoint}"
126
+ headers = self._create_auth_headers(method, endpoint, body)
127
+
128
+ # Update rate limit usage for this call
129
+ self.rate_limiter.record_call(idx, weight)
130
+
131
+ try:
132
+ response = self._make_http_request(method, url, headers, body)
133
+ except httpx.HTTPError as exc:
134
+ return Failure(exc)
135
+
136
+ self._update_rate_limits(response, idx)
137
+ # Always return raw data - let the caller handle model conversion
138
+ return decode_response_result(response, model=Any)
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
+
186
+ def _create_auth_headers(self, method: str, endpoint: str, body: AnyDict | None) -> dict[str, str]:
187
+ """Create authentication headers if API key is configured."""
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
+ }
197
+
198
+ def _make_http_request(
199
+ self,
200
+ method: str,
201
+ url: str,
202
+ headers: dict[str, str],
203
+ body: AnyDict | None,
204
+ ) -> httpx.Response:
205
+ """Make the actual HTTP request."""
206
+ timeout = self.settings.access_window_ms / 1000
207
+
208
+ match method:
209
+ case "GET":
210
+ return httpx.get(url, headers=headers, timeout=timeout)
211
+ case "POST":
212
+ return httpx.post(url, headers=headers, json=body, timeout=timeout)
213
+ case "PUT":
214
+ return httpx.put(url, headers=headers, json=body, timeout=timeout)
215
+ case "DELETE":
216
+ return httpx.delete(url, headers=headers, timeout=timeout)
217
+ case _:
218
+ msg = f"Unsupported HTTP method: {method}"
219
+ raise ValueError(msg)
220
+
221
+ def _update_rate_limits(self, response: httpx.Response, idx: int) -> None:
222
+ """Update rate limits based on response."""
223
+ try:
224
+ json_data = response.json()
225
+ except ValueError:
226
+ json_data = {}
227
+
228
+ if isinstance(json_data, dict) and "error" in json_data:
229
+ if self._is_rate_limit_error(response, json_data):
230
+ self.rate_limiter.update_from_error(idx, json_data)
231
+ else:
232
+ self.rate_limiter.update_from_headers(idx, dict(response.headers))
233
+ else:
234
+ self.rate_limiter.update_from_headers(idx, dict(response.headers))
235
+
236
+ def _is_rate_limit_error(self, response: httpx.Response, json_data: dict[str, Any]) -> bool:
237
+ """Check if response indicates a rate limit error."""
238
+ status = getattr(response, "status_code", None)
239
+ if status == httpx.codes.TOO_MANY_REQUESTS:
240
+ return True
241
+
242
+ err = json_data.get("error")
243
+ if isinstance(err, dict):
244
+ code = str(err.get("code", "")).lower()
245
+ message = str(err.get("message", "")).lower()
246
+ else:
247
+ code = ""
248
+ message = str(err).lower()
249
+
250
+ return any(k in code or k in message for k in ("rate", "limit", "too_many"))
@@ -1,117 +0,0 @@
1
- """Main facade for the Bitvavo client."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
- from typing import TYPE_CHECKING, TypeVar
7
-
8
- from bitvavo_client.auth.rate_limit import RateLimitManager
9
- from bitvavo_client.core.settings import BitvavoSettings
10
- from bitvavo_client.endpoints.private import PrivateAPI
11
- from bitvavo_client.endpoints.public import PublicAPI
12
- from bitvavo_client.transport.http import HTTPClient
13
-
14
- if TYPE_CHECKING: # pragma: no cover
15
- from bitvavo_client.core.model_preferences import ModelPreference
16
-
17
- T = TypeVar("T")
18
-
19
-
20
- class BitvavoClient:
21
- """
22
- Main Bitvavo API client facade providing backward-compatible interface.
23
-
24
- TODO(NostraDavid): add mechanisms to get a ton of data efficiently, which then uses the public and private APIs.
25
- Otherwise, users can just grab the data themselves via the public and private API endpoints.
26
- """
27
-
28
- def __init__(
29
- self,
30
- settings: BitvavoSettings | None = None,
31
- *,
32
- preferred_model: ModelPreference | str | None = None,
33
- default_schema: dict | None = None,
34
- ) -> None:
35
- """Initialize Bitvavo client.
36
-
37
- Args:
38
- settings: Optional settings override. If None, uses defaults.
39
- preferred_model: Preferred model format for responses
40
- default_schema: Default schema for DataFrame conversion
41
- """
42
- self.settings = settings or BitvavoSettings()
43
- self.rate_limiter = RateLimitManager(
44
- self.settings.default_rate_limit,
45
- self.settings.rate_limit_buffer,
46
- )
47
- self.http = HTTPClient(self.settings, self.rate_limiter)
48
-
49
- # 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)
@@ -1,170 +0,0 @@
1
- """HTTP client for Bitvavo API."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
- from typing import TYPE_CHECKING, Any
7
-
8
- import httpx
9
- from returns.result import Failure, Result
10
-
11
- from bitvavo_client.adapters.returns_adapter import (
12
- BitvavoError,
13
- decode_response_result,
14
- )
15
- from bitvavo_client.auth.signing import create_signature
16
-
17
- if TYPE_CHECKING: # pragma: no cover
18
- from collections.abc import Callable
19
-
20
- from bitvavo_client.auth.rate_limit import RateLimitManager
21
- from bitvavo_client.core.settings import BitvavoSettings
22
- from bitvavo_client.core.types import AnyDict
23
-
24
-
25
- class HTTPClient:
26
- """HTTP client for Bitvavo REST API with rate limiting and authentication."""
27
-
28
- def __init__(self, settings: BitvavoSettings, rate_limiter: RateLimitManager) -> None:
29
- """Initialize HTTP client.
30
-
31
- Args:
32
- settings: Bitvavo settings configuration
33
- rate_limiter: Rate limit manager instance
34
- """
35
- self.settings: BitvavoSettings = settings
36
- self.rate_limiter: RateLimitManager = rate_limiter
37
- self.key_index: int = -1
38
- self.api_key: str = ""
39
- self.api_secret: str = ""
40
- self.key_rotation_callback: Callable[[], bool] | None = None
41
-
42
- def configure_key(self, key: str, secret: str, index: int) -> None:
43
- """Configure API key for authenticated requests.
44
-
45
- Args:
46
- key: API key
47
- secret: API secret
48
- index: Key index for rate limiting
49
- """
50
- self.api_key = key
51
- self.api_secret = secret
52
- self.key_index = index
53
-
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
57
-
58
- def request(
59
- self,
60
- method: str,
61
- endpoint: str,
62
- *,
63
- body: AnyDict | None = None,
64
- weight: int = 1,
65
- ) -> Result[Any, BitvavoError | httpx.HTTPError]:
66
- """Make HTTP request and return raw JSON data as a Result.
67
-
68
- Args:
69
- method: HTTP method (GET, POST, PUT, DELETE)
70
- endpoint: API endpoint path
71
- body: Request body for POST/PUT requests
72
- weight: Rate limit weight of the request
73
-
74
- Returns:
75
- Result containing raw JSON response or error
76
-
77
- Raises:
78
- HTTPError: On transport-level failures
79
- """
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)
87
-
88
- url = f"{self.settings.rest_url}{endpoint}"
89
- headers = self._create_auth_headers(method, endpoint, body)
90
-
91
- try:
92
- response = self._make_http_request(method, url, headers, body)
93
- except httpx.HTTPError as exc:
94
- return Failure(exc)
95
-
96
- self._update_rate_limits(response)
97
- # Always return raw data - let the caller handle model conversion
98
- return decode_response_result(response, model=Any)
99
-
100
- def _create_auth_headers(self, method: str, endpoint: str, body: AnyDict | None) -> dict[str, str]:
101
- """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
117
-
118
- def _make_http_request(
119
- self,
120
- method: str,
121
- url: str,
122
- headers: dict[str, str],
123
- body: AnyDict | None,
124
- ) -> httpx.Response:
125
- """Make the actual HTTP request."""
126
- timeout = self.settings.access_window_ms / 1000
127
-
128
- match method:
129
- case "GET":
130
- return httpx.get(url, headers=headers, timeout=timeout)
131
- case "POST":
132
- return httpx.post(url, headers=headers, json=body, timeout=timeout)
133
- case "PUT":
134
- return httpx.put(url, headers=headers, json=body, timeout=timeout)
135
- case "DELETE":
136
- return httpx.delete(url, headers=headers, timeout=timeout)
137
- case _:
138
- msg = f"Unsupported HTTP method: {method}"
139
- raise ValueError(msg)
140
-
141
- def _update_rate_limits(self, response: httpx.Response) -> None:
142
- """Update rate limits based on response."""
143
- try:
144
- json_data = response.json()
145
- except ValueError:
146
- json_data = {}
147
-
148
- if isinstance(json_data, dict) and "error" in json_data:
149
- if self._is_rate_limit_error(response, json_data):
150
- self.rate_limiter.update_from_error(self.key_index, json_data)
151
- else:
152
- self.rate_limiter.update_from_headers(self.key_index, dict(response.headers))
153
- else:
154
- self.rate_limiter.update_from_headers(self.key_index, dict(response.headers))
155
-
156
- def _is_rate_limit_error(self, response: httpx.Response, json_data: dict[str, Any]) -> bool:
157
- """Check if response indicates a rate limit error."""
158
- status = getattr(response, "status_code", None)
159
- if status == httpx.codes.TOO_MANY_REQUESTS:
160
- return True
161
-
162
- err = json_data.get("error")
163
- if isinstance(err, dict):
164
- code = str(err.get("code", "")).lower()
165
- message = str(err.get("message", "")).lower()
166
- else:
167
- code = ""
168
- message = str(err).lower()
169
-
170
- return any(k in code or k in message for k in ("rate", "limit", "too_many"))