bitvavo-api-upgraded 4.2.0__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.
- bitvavo_api_upgraded/bitvavo.py +32 -79
- bitvavo_api_upgraded/settings.py +0 -4
- {bitvavo_api_upgraded-4.2.0.dist-info → bitvavo_api_upgraded-4.3.0.dist-info}/METADATA +8 -11
- {bitvavo_api_upgraded-4.2.0.dist-info → bitvavo_api_upgraded-4.3.0.dist-info}/RECORD +10 -10
- {bitvavo_api_upgraded-4.2.0.dist-info → bitvavo_api_upgraded-4.3.0.dist-info}/WHEEL +1 -1
- bitvavo_client/auth/rate_limit.py +21 -0
- bitvavo_client/core/settings.py +2 -2
- bitvavo_client/endpoints/base.py +3 -1
- bitvavo_client/facade.py +11 -47
- bitvavo_client/transport/http.py +114 -34
bitvavo_api_upgraded/bitvavo.py
CHANGED
@@ -167,7 +167,7 @@ class Bitvavo:
|
|
167
167
|
)
|
168
168
|
time_dict = bitvavo.time()
|
169
169
|
|
170
|
-
# Multiple API keys
|
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
|
-
|
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
|
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
|
-
#
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
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}"
|
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
|
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
|
-
|
542
|
-
|
543
|
-
|
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
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
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
|
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
|
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
|
bitvavo_api_upgraded/settings.py
CHANGED
@@ -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.
|
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({
|
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({
|
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
|
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({
|
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({
|
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({
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
38
|
-
bitvavo_api_upgraded-4.
|
39
|
-
bitvavo_api_upgraded-4.
|
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,,
|
@@ -34,6 +34,7 @@ class RateLimitManager:
|
|
34
34
|
buffer: Buffer to keep before hitting limit
|
35
35
|
strategy: Optional strategy callback when rate limit exceeded
|
36
36
|
"""
|
37
|
+
self.default_remaining: int = default_remaining
|
37
38
|
self.state: dict[int, dict[str, int]] = {-1: {"remaining": default_remaining, "resetAt": 0}}
|
38
39
|
self.buffer: int = buffer
|
39
40
|
|
@@ -57,6 +58,20 @@ class RateLimitManager:
|
|
57
58
|
self.ensure_key(idx)
|
58
59
|
return (self.state[idx]["remaining"] - weight) >= self.buffer
|
59
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
|
+
|
60
75
|
def update_from_headers(self, idx: int, headers: dict[str, str]) -> None:
|
61
76
|
"""Update rate limit state from response headers.
|
62
77
|
|
@@ -100,6 +115,12 @@ class RateLimitManager:
|
|
100
115
|
"""Invoke the configured strategy when rate limit is exceeded."""
|
101
116
|
self._strategy(self, idx, weight)
|
102
117
|
|
118
|
+
def reset_key(self, idx: int) -> None:
|
119
|
+
"""Reset the remaining budget and reset time for a key index."""
|
120
|
+
self.ensure_key(idx)
|
121
|
+
self.state[idx]["remaining"] = self.default_remaining
|
122
|
+
self.state[idx]["resetAt"] = 0
|
123
|
+
|
103
124
|
def get_remaining(self, idx: int) -> int:
|
104
125
|
"""Get remaining rate limit for key index.
|
105
126
|
|
bitvavo_client/core/settings.py
CHANGED
@@ -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,
|
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")
|
bitvavo_client/endpoints/base.py
CHANGED
@@ -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:
|
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,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import TYPE_CHECKING
|
5
|
+
from typing import TYPE_CHECKING
|
6
6
|
|
7
7
|
from bitvavo_client.auth.rate_limit import RateLimitManager
|
8
8
|
from bitvavo_client.core.settings import BitvavoSettings
|
@@ -13,8 +13,6 @@ from bitvavo_client.transport.http import HTTPClient
|
|
13
13
|
if TYPE_CHECKING: # pragma: no cover
|
14
14
|
from bitvavo_client.core.model_preferences import ModelPreference
|
15
15
|
|
16
|
-
T = TypeVar("T")
|
17
|
-
|
18
16
|
|
19
17
|
class BitvavoClient:
|
20
18
|
"""
|
@@ -46,47 +44,13 @@ class BitvavoClient:
|
|
46
44
|
self.http = HTTPClient(self.settings, self.rate_limiter)
|
47
45
|
|
48
46
|
# Initialize API endpoint handlers with preferred model settings
|
49
|
-
self.public = PublicAPI(
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
self.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# Collect keys from settings
|
60
|
-
if self.settings.api_key and self.settings.api_secret:
|
61
|
-
self._api_keys.append((self.settings.api_key, self.settings.api_secret))
|
62
|
-
if self.settings.api_keys:
|
63
|
-
self._api_keys.extend((item["key"], item["secret"]) for item in self.settings.api_keys)
|
64
|
-
|
65
|
-
if not self._api_keys:
|
66
|
-
return
|
67
|
-
|
68
|
-
for idx, (_key, _secret) in enumerate(self._api_keys):
|
69
|
-
self.rate_limiter.ensure_key(idx)
|
70
|
-
|
71
|
-
first_key = self._api_keys[0]
|
72
|
-
self.http.configure_key(first_key[0], first_key[1], 0)
|
73
|
-
if len(self._api_keys) > 1:
|
74
|
-
self.http.set_key_rotation_callback(self.rotate_key)
|
75
|
-
|
76
|
-
def rotate_key(self) -> bool:
|
77
|
-
"""Rotate to the next configured API key if available."""
|
78
|
-
if len(self._api_keys) <= 1:
|
79
|
-
return False
|
80
|
-
self._current_key = (self._current_key + 1) % len(self._api_keys)
|
81
|
-
key, secret = self._api_keys[self._current_key]
|
82
|
-
self.http.configure_key(key, secret, self._current_key)
|
83
|
-
return True
|
84
|
-
|
85
|
-
def select_key(self, index: int) -> None:
|
86
|
-
"""Select a specific API key by index."""
|
87
|
-
if not (0 <= index < len(self._api_keys)):
|
88
|
-
msg = "API key index out of range"
|
89
|
-
raise IndexError(msg)
|
90
|
-
self._current_key = index
|
91
|
-
key, secret = self._api_keys[index]
|
92
|
-
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
|
+
)
|
bitvavo_client/transport/http.py
CHANGED
@@ -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.
|
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.
|
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
|
55
|
-
"""
|
56
|
-
self.
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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(
|
230
|
+
self.rate_limiter.update_from_error(idx, json_data)
|
151
231
|
else:
|
152
|
-
self.rate_limiter.update_from_headers(
|
232
|
+
self.rate_limiter.update_from_headers(idx, dict(response.headers))
|
153
233
|
else:
|
154
|
-
self.rate_limiter.update_from_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."""
|