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.
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/PKG-INFO +8 -11
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/README.md +7 -10
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/pyproject.toml +2 -2
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/bitvavo.py +32 -79
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/settings.py +0 -4
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/rate_limit.py +14 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/settings.py +2 -2
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/base.py +3 -1
- bitvavo_api_upgraded-4.3.0/src/bitvavo_client/facade.py +56 -0
- bitvavo_api_upgraded-4.3.0/src/bitvavo_client/transport/http.py +250 -0
- bitvavo_api_upgraded-4.2.1/src/bitvavo_client/facade.py +0 -117
- bitvavo_api_upgraded-4.2.1/src/bitvavo_client/transport/http.py +0 -170
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/py.typed +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/adapters/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/adapters/returns_adapter.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/signing.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/errors.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/model_preferences.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/private_models.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/public_models.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/types.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/validation_helpers.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/df/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/df/convert.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/common.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/private.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/public.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/py.typed +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/__init__.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/private_schemas.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/public_schemas.py +0 -0
- {bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/transport/__init__.py +0 -0
- {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.
|
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')
|
@@ -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({
|
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({
|
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
|
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({
|
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({
|
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({
|
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.
|
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.
|
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}"
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/bitvavo.py
RENAMED
@@ -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-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/settings.py
RENAMED
@@ -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",
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/rate_limit.py
RENAMED
@@ -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
|
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/settings.py
RENAMED
@@ -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_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/base.py
RENAMED
@@ -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,
|
@@ -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"))
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/__init__.py
RENAMED
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/helper_funcs.py
RENAMED
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_api_upgraded/type_aliases.py
RENAMED
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/adapters/__init__.py
RENAMED
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/__init__.py
RENAMED
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/auth/signing.py
RENAMED
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/private_models.py
RENAMED
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/core/public_models.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/__init__.py
RENAMED
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/common.py
RENAMED
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/private.py
RENAMED
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/endpoints/public.py
RENAMED
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/schemas/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{bitvavo_api_upgraded-4.2.1 → bitvavo_api_upgraded-4.3.0}/src/bitvavo_client/transport/__init__.py
RENAMED
File without changes
|
File without changes
|