bitvavo-api-upgraded 4.0.0__py3-none-any.whl → 4.1.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.
@@ -2,16 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import datetime as dt
5
- import hashlib
6
- import hmac
7
5
  import json
6
+ import statistics
8
7
  import time
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
9
  from pathlib import Path
10
10
  from threading import Thread
11
- from typing import Any, Callable
11
+ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  import websocket as ws_lib
14
- from requests import delete, get, post, put
14
+ from httpx import delete, get, post, put
15
15
  from structlog.stdlib import get_logger
16
16
  from websocket import WebSocketApp # missing stubs for WebSocketApp
17
17
 
@@ -19,88 +19,24 @@ from bitvavo_api_upgraded.dataframe_utils import convert_candles_to_dataframe, c
19
19
  from bitvavo_api_upgraded.helper_funcs import configure_loggers, time_ms, time_to_wait
20
20
  from bitvavo_api_upgraded.settings import bitvavo_settings, bitvavo_upgraded_settings
21
21
  from bitvavo_api_upgraded.type_aliases import OutputFormat, anydict, errordict, intdict, ms, s_f, strdict, strintdict
22
+ from bitvavo_client.auth.signing import create_signature
23
+ from bitvavo_client.endpoints.common import (
24
+ asks_compare,
25
+ bids_compare,
26
+ create_postfix,
27
+ default,
28
+ epoch_millis,
29
+ sort_and_insert,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from collections.abc import Callable
22
34
 
23
35
  configure_loggers()
24
36
 
25
37
  logger = get_logger(__name__)
26
38
 
27
39
 
28
- def create_signature(timestamp: ms, method: str, url: str, body: anydict | None, api_secret: str) -> str:
29
- string = f"{timestamp}{method}/v2{url}"
30
- if body is not None and len(body.keys()) > 0:
31
- string += json.dumps(body, separators=(",", ":"))
32
- signature = hmac.new(api_secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha256).hexdigest()
33
- return signature
34
-
35
-
36
- def create_postfix(options: anydict | None) -> str:
37
- """Generate a URL postfix, based on the `options` dict.
38
-
39
- ---
40
- Args:
41
- options (anydict): [description]
42
-
43
- ---
44
- Returns:
45
- str: [description]
46
- """
47
- options = _default(options, {})
48
- params = [f"{key}={options[key]}" for key in options]
49
- postfix = "&".join(params) # intersperse
50
- return f"?{postfix}" if len(options) > 0 else postfix
51
-
52
-
53
- def _default(value: anydict | None, fallback: anydict) -> anydict:
54
- """
55
- Note that is close, but not actually equal to:
56
-
57
- `return value or fallback`
58
-
59
- I checked this with a temporary hypothesis test.
60
-
61
- This note is all you will get out of me.
62
- """
63
- return value if value is not None else fallback
64
-
65
-
66
- def _epoch_millis(dt: dt.datetime) -> int:
67
- return int(dt.timestamp() * 1000)
68
-
69
-
70
- def asks_compare(a: float, b: float) -> bool:
71
- return a < b
72
-
73
-
74
- def bids_compare(a: float, b: float) -> bool:
75
- return a > b
76
-
77
-
78
- def sort_and_insert(
79
- asks_or_bids: list[list[str]],
80
- update: list[list[str]],
81
- compareFunc: Callable[[float, float], bool],
82
- ) -> list[list[str]] | errordict:
83
- for updateEntry in update:
84
- entrySet: bool = False
85
- for j in range(len(asks_or_bids)):
86
- bookItem = asks_or_bids[j]
87
- if compareFunc(float(updateEntry[0]), float(bookItem[0])):
88
- asks_or_bids.insert(j, updateEntry)
89
- entrySet = True
90
- break
91
- if float(updateEntry[0]) == float(bookItem[0]):
92
- if float(updateEntry[1]) > 0.0:
93
- asks_or_bids[j] = updateEntry
94
- entrySet = True
95
- break
96
- asks_or_bids.pop(j)
97
- entrySet = True
98
- break
99
- if not entrySet:
100
- asks_or_bids.append(updateEntry)
101
- return asks_or_bids
102
-
103
-
104
40
  def process_local_book(ws: Bitvavo.WebSocketAppFacade, message: anydict) -> None:
105
41
  market: str = ""
106
42
  if "action" in message:
@@ -412,7 +348,9 @@ class Bitvavo:
412
348
  key_name = f"API_KEY_{key_index}" if key_index >= 0 else "KEYLESS"
413
349
 
414
350
  logger.warning(
415
- "rate-limit-reached", key_name=key_name, rateLimitRemaining=self.rate_limits[key_index]["remaining"]
351
+ "rate-limit-reached",
352
+ key_name=key_name,
353
+ rateLimitRemaining=self.rate_limits[key_index]["remaining"],
416
354
  )
417
355
  logger.info(
418
356
  "napping-until-reset",
@@ -423,27 +361,108 @@ class Bitvavo:
423
361
  )
424
362
  time.sleep(napTime + 1) # +1 to add a tiny bit of buffer time
425
363
 
426
- def calc_lag(self) -> ms:
364
+ def calc_lag(self, samples: int = 5, timeout_seconds: float = 5.0) -> ms: # noqa: C901
427
365
  """
428
- Calculate the time difference between the client and server; use this value with BITVAVO_API_UPGRADED_LAG,
429
- when you make an api call, to precent 304 errors.
366
+ Calculate the time difference between the client and server using statistical analysis.
367
+
368
+ Uses multiple samples with outlier detection to get a more accurate lag measurement.
369
+
370
+ Args:
371
+ samples: Number of time samples to collect (default: 5)
372
+ timeout_seconds: Maximum time to spend collecting samples (default: 5.0)
373
+
374
+ Returns:
375
+ Average lag in milliseconds
430
376
 
431
- Raises KeyError if time() returns an error dict.
377
+ Raises:
378
+ ValueError: If unable to collect sufficient valid samples
379
+ RuntimeError: If all API calls fail
432
380
  """
433
- lag_list = [
434
- self.time()["time"] - time_ms(),
435
- self.time()["time"] - time_ms(),
436
- self.time()["time"] - time_ms(),
437
- self.time()["time"] - time_ms(),
438
- self.time()["time"] - time_ms(),
439
- self.time()["time"] - time_ms(),
440
- self.time()["time"] - time_ms(),
441
- self.time()["time"] - time_ms(),
442
- self.time()["time"] - time_ms(),
443
- self.time()["time"] - time_ms(),
444
- ]
381
+ ARBITRARY = 3
382
+ if samples < ARBITRARY:
383
+ msg = f"Need at least {ARBITRARY} samples for statistical analysis"
384
+ raise ValueError(msg)
385
+
386
+ def measure_single_lag() -> ms | None:
387
+ """Measure lag for a single request with error handling."""
388
+ try:
389
+ client_time_before = time_ms()
390
+ server_response = self.time()
391
+ client_time_after = time_ms()
392
+
393
+ if isinstance(server_response, dict) and "time" in server_response:
394
+ # Use midpoint of request duration for better accuracy
395
+ client_time_avg = (client_time_before + client_time_after) // 2
396
+ server_time = server_response["time"]
397
+ if isinstance(server_time, int):
398
+ return ms(server_time - client_time_avg)
399
+ return None
400
+ except (ValueError, TypeError, KeyError):
401
+ return None
402
+ else:
403
+ # If error or unexpected response
404
+ return None
405
+
406
+ lag_measurements: list[ms] = []
407
+
408
+ # Collect samples concurrently for better performance
409
+ with ThreadPoolExecutor(max_workers=min(samples, 5)) as executor:
410
+ try:
411
+ # Submit all measurement tasks
412
+ future_to_sample = {executor.submit(measure_single_lag): i for i in range(samples)}
413
+
414
+ # Collect results with timeout
415
+ for future in as_completed(future_to_sample, timeout=timeout_seconds):
416
+ lag = future.result()
417
+ if lag is not None:
418
+ lag_measurements.append(lag)
419
+
420
+ except TimeoutError:
421
+ if self.debugging:
422
+ logger.warning(
423
+ "lag-calculation-timeout",
424
+ collected_samples=len(lag_measurements),
425
+ requested_samples=samples,
426
+ )
427
+
428
+ if len(lag_measurements) < max(2, samples // 2):
429
+ msg = f"Insufficient valid samples: got {len(lag_measurements)}, need at least {max(2, samples // 2)}"
430
+ raise RuntimeError(msg)
431
+
432
+ # Remove outliers using interquartile range method
433
+ QUARTILES = 4
434
+ if len(lag_measurements) >= QUARTILES:
435
+ try:
436
+ q1 = statistics.quantiles(lag_measurements, n=QUARTILES)[0]
437
+ q3 = statistics.quantiles(lag_measurements, n=QUARTILES)[2]
438
+ iqr = q3 - q1
439
+ lower_bound = q1 - 1.5 * iqr
440
+ upper_bound = q3 + 1.5 * iqr
441
+
442
+ filtered_measurements = [lag for lag in lag_measurements if lower_bound <= lag <= upper_bound]
443
+
444
+ # Use filtered data if we still have enough samples
445
+ if len(filtered_measurements) >= 2: # noqa: PLR2004
446
+ lag_measurements = filtered_measurements
447
+
448
+ except statistics.StatisticsError:
449
+ # Fall back to original measurements if filtering fails
450
+ pass
451
+
452
+ # Calculate final lag using median for robustness
453
+ final_lag = ms(statistics.median(lag_measurements))
454
+
455
+ if self.debugging:
456
+ logger.debug(
457
+ "lag-calculated",
458
+ samples_collected=len(lag_measurements),
459
+ lag_ms=final_lag,
460
+ min_lag=min(lag_measurements),
461
+ max_lag=max(lag_measurements),
462
+ std_dev=statistics.stdev(lag_measurements) if len(lag_measurements) > 1 else 0,
463
+ )
445
464
 
446
- return ms(sum(lag_list) / len(lag_list))
465
+ return final_lag
447
466
 
448
467
  def get_remaining_limit(self) -> int:
449
468
  """Get the remaining rate limit
@@ -576,7 +595,6 @@ class Bitvavo:
576
595
 
577
596
  ---
578
597
  Args:
579
- # TODO(NostraDavid) fill these in
580
598
  ```python
581
599
  endpoint: str = "/order"
582
600
  postfix: str = "" # ?key=value&key2=another_value&...
@@ -1045,14 +1063,14 @@ class Bitvavo:
1045
1063
  # timestamp is converted to datetime, numeric columns to float
1046
1064
  ```
1047
1065
  """
1048
- options = _default(options, {})
1066
+ options = default(options, {})
1049
1067
  options["interval"] = interval
1050
1068
  if limit is not None:
1051
1069
  options["limit"] = limit
1052
1070
  if start is not None:
1053
- options["start"] = _epoch_millis(start)
1071
+ options["start"] = epoch_millis(start)
1054
1072
  if end is not None:
1055
- options["end"] = _epoch_millis(end)
1073
+ options["end"] = epoch_millis(end)
1056
1074
  postfix = create_postfix(options)
1057
1075
  result = self.public_request(f"{self.base}/{market}/candles{postfix}") # type: ignore[return-value]
1058
1076
  return convert_candles_to_dataframe(result, output_format)
@@ -1279,7 +1297,7 @@ class Bitvavo:
1279
1297
  ]
1280
1298
  ```
1281
1299
  """
1282
- options = _default(options, {})
1300
+ options = default(options, {})
1283
1301
  rateLimitingWeight = 25
1284
1302
  if "market" in options:
1285
1303
  rateLimitingWeight = 1
@@ -1796,7 +1814,7 @@ class Bitvavo:
1796
1814
  ]
1797
1815
  ```
1798
1816
  """ # noqa: E501
1799
- options = _default(options, {})
1817
+ options = default(options, {})
1800
1818
  options["market"] = market
1801
1819
  postfix = create_postfix(options)
1802
1820
  return self.private_request("/orders", postfix, {}, "GET", 5) # type: ignore[return-value]
@@ -1894,7 +1912,7 @@ class Bitvavo:
1894
1912
  ]
1895
1913
  ```
1896
1914
  """
1897
- options = _default(options, {})
1915
+ options = default(options, {})
1898
1916
  rateLimitingWeight = 25
1899
1917
  if "market" in options:
1900
1918
  rateLimitingWeight = 1
@@ -1960,7 +1978,7 @@ class Bitvavo:
1960
1978
  ]
1961
1979
  ```
1962
1980
  """ # noqa: E501
1963
- options = _default(options, {})
1981
+ options = default(options, {})
1964
1982
  options["market"] = market
1965
1983
  postfix = create_postfix(options)
1966
1984
  result = self.private_request("/trades", postfix, {}, "GET", 5) # type: ignore[return-value]
@@ -2489,7 +2507,6 @@ class Bitvavo:
2489
2507
  time.sleep(0.1)
2490
2508
 
2491
2509
  def do_send(self, ws: WebSocketApp, message: str, private: bool = False) -> None: # noqa: FBT001, FBT002
2492
- # TODO(NostraDavid): add nap-time to the websocket, or do it here; I don't know yet.
2493
2510
  if private and self.APIKEY == "":
2494
2511
  logger.error(
2495
2512
  "no-apikey",
@@ -3961,8 +3978,6 @@ class Bitvavo:
3961
3978
  self.do_send(self.ws, json.dumps(options), True)
3962
3979
 
3963
3980
  def subscription_ticker(self, market: str, callback: Callable[[Any], None]) -> None:
3964
- # TODO(NostraDavid): one possible improvement here is to turn `market` into a list of markets, so we can sub
3965
- # to all of them at once. Same goes for other `subscription*()`
3966
3981
  """
3967
3982
  Subscribe to the ticker channel, which means `callback` gets passed the new best bid or ask whenever they
3968
3983
  change (server-side).
@@ -165,7 +165,9 @@ def convert_candles_to_dataframe(data: Any, output_format: str | OutputFormat) -
165
165
  # Convert list of lists to list of dicts first
166
166
  columns = ["timestamp", "open", "high", "low", "close", "volume"]
167
167
  dict_data = [
168
- dict(zip(columns, candle)) for candle in data if isinstance(candle, list) and len(candle) >= len(columns)
168
+ dict(zip(columns, candle, strict=True))
169
+ for candle in data
170
+ if isinstance(candle, list) and len(candle) >= len(columns)
169
171
  ]
170
172
 
171
173
  if not dict_data:
@@ -105,7 +105,7 @@ class BitvavoSettings(BaseSettings):
105
105
  PREFER_KEYLESS: bool = Field(default=True)
106
106
 
107
107
  # Configuration for Pydantic Settings
108
- model_config: SettingsConfigDict = SettingsConfigDict(
108
+ model_config = SettingsConfigDict(
109
109
  env_file=Path.cwd() / ".env",
110
110
  env_file_encoding="utf-8",
111
111
  env_prefix="BITVAVO_",
@@ -4,7 +4,7 @@ to clearify the intention or semantics/meaning/unit of a variable
4
4
  """
5
5
 
6
6
  import sys
7
- from typing import Any, Union
7
+ from typing import Any
8
8
 
9
9
  if sys.version_info >= (3, 11):
10
10
  from enum import StrEnum
@@ -26,7 +26,7 @@ anydict = dict[str, Any]
26
26
  strdict = dict[str, str]
27
27
  intdict = dict[str, int]
28
28
  # can't use | here, with __future__. Not sure why.
29
- strintdict = dict[str, Union[str, int]]
29
+ strintdict = dict[str, str | int]
30
30
  errordict = dict[str, Any] # same type as anydict, but the semantics/meaning is different
31
31
 
32
32
  # note: You can also use these for type conversion, so instead of int(some_float / 1000), you can just do ms(some_float
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bitvavo-api-upgraded
3
- Version: 4.0.0
3
+ Version: 4.1.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>
@@ -21,11 +21,12 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python
23
23
  Classifier: Typing :: Typed
24
- Requires-Dist: pydantic-settings==2.*,>=2.6
25
- Requires-Dist: requests==2.*,>=2.26
24
+ Requires-Dist: httpx>=0.28.1
25
+ Requires-Dist: pydantic-settings>=2.6
26
+ Requires-Dist: requests>=2.26
26
27
  Requires-Dist: returns>=0.23.0
27
- Requires-Dist: structlog>=21.5,==25.*
28
- Requires-Dist: websocket-client==1.*,>=1.2
28
+ Requires-Dist: structlog>=21.5
29
+ Requires-Dist: websocket-client>=1.2
29
30
  Requires-Dist: cudf-cu12>=24.0.0 ; extra == 'cudf'
30
31
  Requires-Dist: narwhals>=2.0.0 ; extra == 'cudf'
31
32
  Requires-Dist: narwhals[dask]>=2.0.0 ; extra == 'dask'
@@ -34,6 +35,9 @@ Requires-Dist: narwhals[ibis]>=2.0.0 ; extra == 'ibis'
34
35
  Requires-Dist: narwhals[modin]>=2.0.0 ; extra == 'modin'
35
36
  Requires-Dist: narwhals[pandas]>=2.0.0 ; extra == 'pandas'
36
37
  Requires-Dist: narwhals[polars]>=2.0.0 ; extra == 'polars'
38
+ Requires-Dist: cudf-polars-cu12>=24.0.0 ; extra == 'polars-gpu'
39
+ Requires-Dist: polars ; extra == 'polars-gpu'
40
+ Requires-Dist: narwhals>=2.0.0 ; extra == 'polars-gpu'
37
41
  Requires-Dist: narwhals[pyarrow]>=2.0.0 ; extra == 'pyarrow'
38
42
  Requires-Dist: narwhals[pyspark]>=2.0.0 ; extra == 'pyspark'
39
43
  Requires-Dist: narwhals[pyspark-connect]>=2.0.0 ; extra == 'pyspark-connect'
@@ -51,6 +55,7 @@ Provides-Extra: ibis
51
55
  Provides-Extra: modin
52
56
  Provides-Extra: pandas
53
57
  Provides-Extra: polars
58
+ Provides-Extra: polars-gpu
54
59
  Provides-Extra: pyarrow
55
60
  Provides-Extra: pyspark
56
61
  Provides-Extra: pyspark-connect
@@ -59,7 +64,7 @@ Description-Content-Type: text/markdown
59
64
 
60
65
  # Bitvavo API (upgraded)
61
66
 
62
- A **typed, tested, and enhanced** Python wrapper for the Bitvavo cryptocurrency exchange API. This is an "upgraded" fork of the official Bitvavo SDK with comprehensive type hints, unit tests, and improved developer experience.
67
+ A **typed, tested, and enhanced** Python wrapper for the Bitvavo cryptocurrency exchange API. This is an "upgraded" fork of the official Bitvavo SDK with comprehensive type hints, unit tests, modern architecture, and improved developer experience.
63
68
 
64
69
  ## Quick Start
65
70
 
@@ -67,6 +72,23 @@ A **typed, tested, and enhanced** Python wrapper for the Bitvavo cryptocurrency
67
72
  pip install bitvavo_api_upgraded
68
73
  ```
69
74
 
75
+ ### Basic Usage
76
+
77
+ ```python
78
+ # Option 1: Original Bitvavo interface (legacy)
79
+ from bitvavo_api_upgraded import Bitvavo
80
+
81
+ bitvavo = Bitvavo({'APIKEY': 'your-key', 'APISECRET': 'your-secret'})
82
+ balance = bitvavo.balance({})
83
+
84
+ # Option 2: New modular BitvavoClient interface (recommended)
85
+ from bitvavo_client import BitvavoClient, BitvavoSettings
86
+
87
+ client = BitvavoClient()
88
+ result = client.public.time() # No authentication needed
89
+ result = client.private.balance() # Authentication required
90
+ ```
91
+
70
92
  ### Optional Dataframe Support
71
93
 
72
94
  This package supports multiple dataframe libraries via [Narwhals](https://narwhals-dev.github.io/narwhals/), providing a unified interface across:
@@ -113,66 +135,127 @@ Scroll down for detailed usage examples and configuration instructions.
113
135
 
114
136
  This wrapper improves upon the official Bitvavo SDK with:
115
137
 
116
- - 🎯 **Complete type annotations** for all functions and classes
117
- - 🧪 **Comprehensive test suite** (found and fixed multiple bugs in the original)
118
- - 📋 **Detailed changelog** tracking all changes and improvements
119
- - 🔄 **Up-to-date API compliance** including MiCA regulatory requirements
120
- - 📚 **Enhanced documentation** with examples and clear usage patterns
121
- - 🐍 **Modern Python support** (3.9+, dropped EOL versions)
122
- - 🔑 **Multi-key & keyless support** for enhanced rate limiting and public access
123
- - **Better error handling** and rate limiting
124
- - 🔧 **Developer-friendly tooling** (ruff, mypy, pre-commit hooks)
125
- - 📊 **Unified dataframe support** via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, SQLFrame)
138
+ ### Modern Architecture
139
+
140
+ - **Modular design**: Clean separation between public/private APIs, transport, and authentication
141
+ - **Two interfaces**: Legacy `Bitvavo` class for backward compatibility + new `BitvavoClient` for modern development
142
+ - **Dependency injection**: Testable, maintainable, and extensible codebase
143
+ - **Type safety**: Comprehensive type annotations with generics and precise return types
144
+
145
+ ### Quality & Reliability
146
+
147
+ - **Comprehensive test suite** (found and fixed multiple bugs in the original)
148
+ - **100% type coverage** with mypy strict mode
149
+ - **Enhanced error handling** with detailed validation messages
150
+ - **Rate limiting** with automatic throttling and multi-key support
151
+
152
+ ### Data Format Flexibility
153
+
154
+ - **Unified dataframe support** via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, SQLFrame)
155
+ - **Pydantic models** for validated, structured data
156
+ - **Raw dictionary access** for backward compatibility
157
+ - **Result types** for functional error handling
158
+
159
+ ### Enhanced Performance
160
+
161
+ - **Multi-key support** for better rate limiting and load distribution
162
+ - **Keyless access** for public endpoints (doesn't count against your API limits)
163
+ - **Connection pooling** and retry logic
164
+ - **Async-ready architecture** (async support coming in future release)
165
+
166
+ ### Developer Experience
167
+
168
+ - **Modern Python support** (3.9+, dropped EOL versions)
169
+ - **Configuration via environment variables** or Pydantic settings
170
+ - **Detailed changelog** tracking all changes and improvements
171
+ - **Enhanced documentation** with examples and clear usage patterns
172
+ - **Developer-friendly tooling** (ruff, mypy, pre-commit hooks)
126
173
 
127
174
  ## Features
128
175
 
129
176
  ### Full API Coverage
130
177
 
131
- - All REST endpoints (public and private)
132
- - ✅ **Multiple API key support** with automatic load balancing
133
- - ✅ **Keyless access** for public endpoints without authentication
134
- - ✅ **Comprehensive dataframe support** via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, and more)
135
- - WebSocket support with reconnection logic
136
- - Rate limiting with automatic throttling
137
- - MiCA compliance reporting endpoints
178
+ - All REST endpoints (public and private)
179
+ - Multiple API key support with automatic load balancing
180
+ - Keyless access for public endpoints without authentication
181
+ - Comprehensive dataframe support via Narwhals (pandas, polars, cuDF, modin, PyArrow, Dask, DuckDB, Ibis, PySpark, and more)
182
+ - WebSocket support with reconnection logic
183
+ - Rate limiting with automatic throttling
184
+ - MiCA compliance reporting endpoints
138
185
 
139
186
  ### Developer Experience
140
187
 
141
- - Type hints for better IDE support
142
- - Comprehensive error handling
143
- - Detailed logging with `structlog`
144
- - Configuration via `.env` files
145
- - Extensive test coverage
188
+ - Type hints for better IDE support
189
+ - Comprehensive error handling
190
+ - Detailed logging with `structlog`
191
+ - Configuration via `.env` files
192
+ - Extensive test coverage
146
193
 
147
194
  ### Production Ready
148
195
 
149
- - Automatic rate limit management
150
- - Multi-key failover support
151
- - Connection retry logic
152
- - Proper error responses
153
- - Memory efficient WebSocket handling
196
+ - Automatic rate limit management
197
+ - Multi-key failover support
198
+ - Connection retry logic
199
+ - Proper error responses
200
+ - Memory efficient WebSocket handling
154
201
 
155
202
  ## Configuration
156
203
 
204
+ ### Environment Variables
205
+
157
206
  Create a `.env` file in your project root:
158
207
 
159
208
  ```env
160
- # Single API key (traditional)
161
- BITVAVO_APIKEY=your-api-key-here
162
- BITVAVO_APISECRET=your-api-secret-here
209
+ # API authentication
210
+ BITVAVO_API_KEY=your-api-key-here
211
+ BITVAVO_API_SECRET=your-api-secret-here
212
+
213
+ # Multi-key support (JSON array as string)
214
+ # BITVAVO_API_KEYS='[{"key": "key1", "secret": "secret1"}, {"key": "key2", "secret": "secret2"}]'
215
+
216
+ # Client behavior
217
+ BITVAVO_PREFER_KEYLESS=true # Use keyless for public endpoints
218
+ BITVAVO_DEFAULT_RATE_LIMIT=1000 # Rate limit per key
219
+ BITVAVO_RATE_LIMIT_BUFFER=50 # Buffer to avoid hitting limits
220
+ BITVAVO_DEBUGGING=false # Enable debug logging
221
+
222
+ # API endpoints (usually not needed to change)
223
+ BITVAVO_REST_URL=https://api.bitvavo.com/v2
224
+ BITVAVO_WS_URL=wss://ws.bitvavo.com/v2/
225
+ ```
226
+
227
+ ### Usage Examples
228
+
229
+ #### New BitvavoClient (Recommended)
230
+
231
+ ```python
232
+ from bitvavo_client import BitvavoClient, BitvavoSettings
233
+
234
+ # Option 1: Auto-load from .env file
235
+ client = BitvavoClient()
236
+
237
+ # Option 2: Custom settings
238
+ settings = BitvavoSettings(
239
+ api_key="your-key",
240
+ api_secret="your-secret",
241
+ prefer_keyless=True,
242
+ debugging=True
243
+ )
244
+ client = BitvavoClient(settings)
163
245
 
164
- # Multiple API keys (for rate limiting)
165
- # BITVAVO_APIKEYS='[{"key": "key1", "secret": "secret1"}, {"key": "key2", "secret": "secret2"}]'
246
+ # Option 3: Manual settings override
247
+ client = BitvavoClient(BitvavoSettings(default_rate_limit=750))
166
248
 
167
- # Keyless access (public endpoints only)
168
- BITVAVO_PREFER_KEYLESS=true
249
+ # Access public endpoints (no auth needed)
250
+ time_result = client.public.time()
251
+ markets_result = client.public.markets()
169
252
 
170
- # Enhanced settings
171
- BITVAVO_API_UPGRADED_DEFAULT_RATE_LIMIT=750
172
- BITVAVO_API_UPGRADED_PREFER_KEYLESS=false
253
+ # Access private endpoints (auth required)
254
+ balance_result = client.private.balance()
255
+ account_result = client.private.account()
173
256
  ```
174
257
 
175
- Then use the settings:
258
+ #### Legacy Bitvavo Class (Backward Compatibility)
176
259
 
177
260
  ```python
178
261
  from bitvavo_api_upgraded import Bitvavo, BitvavoSettings
@@ -199,6 +282,68 @@ bitvavo = Bitvavo({
199
282
  bitvavo = Bitvavo({'PREFER_KEYLESS': True})
200
283
  ```
201
284
 
285
+ ## Data Format Flexibility
286
+
287
+ The new BitvavoClient supports multiple output formats to match your workflow:
288
+
289
+ ### Model Preferences
290
+
291
+ ```python
292
+ from bitvavo_client import BitvavoClient
293
+ from bitvavo_client.core.model_preferences import ModelPreference
294
+
295
+ # Option 1: Raw dictionaries (default, backward compatible)
296
+ client = BitvavoClient(preferred_model=ModelPreference.RAW)
297
+ result = client.public.time() # Returns: {"time": 1609459200000}
298
+
299
+ # Option 2: Validated Pydantic models
300
+ client = BitvavoClient(preferred_model=ModelPreference.PYDANTIC)
301
+ result = client.public.time() # Returns: ServerTime(time=1609459200000)
302
+
303
+ # Option 3: DataFrame format (pandas, polars, etc.)
304
+ client = BitvavoClient(preferred_model=ModelPreference.DATAFRAME)
305
+ result = client.public.markets() # Returns: polars.DataFrame with market data
306
+ ```
307
+
308
+ ### Per-Request Format Override
309
+
310
+ ```python
311
+ # Set a default preference but override per request
312
+ client = BitvavoClient(preferred_model=ModelPreference.RAW)
313
+
314
+ # Get raw dict (uses default)
315
+ raw_data = client.public.markets()
316
+
317
+ # Override to get DataFrame for this request
318
+ import polars as pl
319
+ df_data = client.public.markets(model=pl.DataFrame)
320
+
321
+ # Override to get Pydantic model
322
+ from bitvavo_client.core.public_models import Markets
323
+ validated_data = client.public.markets(model=Markets)
324
+ ```
325
+
326
+ ### Result Types for Error Handling
327
+
328
+ ```python
329
+ from returns.result import Success, Failure
330
+
331
+ # Use result types for functional error handling
332
+ result = client.public.time()
333
+
334
+ if isinstance(result, Success):
335
+ print(f"Server time: {result.unwrap()}")
336
+ elif isinstance(result, Failure):
337
+ print(f"Error: {result.failure()}")
338
+
339
+ # Or use match-case (Python 3.10+)
340
+ match result:
341
+ case Success(value):
342
+ print(f"Success: {value}")
343
+ case Failure(error):
344
+ print(f"Error: {error}")
345
+ ```
346
+
202
347
  ## WebSocket Usage
203
348
 
204
349
  ```python
@@ -288,9 +433,44 @@ balance = bitvavo.balance({})
288
433
 
289
434
  ## API Examples
290
435
 
291
- ### Public Endpoints (No Authentication)
436
+ ### Public Endpoints (No Authentication Required)
437
+
438
+ #### New BitvavoClient Interface
292
439
 
293
440
  ```python
441
+ from bitvavo_client import BitvavoClient
442
+
443
+ client = BitvavoClient()
444
+
445
+ # Get server time
446
+ time_result = client.public.time()
447
+
448
+ # Get all markets
449
+ markets_result = client.public.markets()
450
+
451
+ # Get specific market
452
+ btc_market = client.public.markets(market='BTC-EUR')
453
+
454
+ # Get order book
455
+ book_result = client.public.book('BTC-EUR')
456
+
457
+ # Get recent trades
458
+ trades_result = client.public.trades('BTC-EUR')
459
+
460
+ # Get 24h ticker
461
+ ticker_result = client.public.ticker_24h(market='BTC-EUR')
462
+
463
+ # Get candlestick data
464
+ candles_result = client.public.candles('BTC-EUR', '1h')
465
+ ```
466
+
467
+ #### Legacy Bitvavo Interface
468
+
469
+ ```python
470
+ from bitvavo_api_upgraded import Bitvavo
471
+
472
+ bitvavo = Bitvavo({'PREFER_KEYLESS': True}) # For public endpoints
473
+
294
474
  # Get server time
295
475
  time_resp = bitvavo.time()
296
476
 
@@ -312,7 +492,50 @@ ticker = bitvavo.ticker24h({'market': 'BTC-EUR'})
312
492
 
313
493
  ### Private Endpoints (Authentication Required)
314
494
 
495
+ #### New BitvavoClient Interface
496
+
497
+ ```python
498
+ from bitvavo_client import BitvavoClient, BitvavoSettings
499
+
500
+ # Configure with API credentials
501
+ settings = BitvavoSettings(api_key="your-key", api_secret="your-secret")
502
+ client = BitvavoClient(settings)
503
+
504
+ # Get account info
505
+ account_result = client.private.account()
506
+
507
+ # Get balance
508
+ balance_result = client.private.balance()
509
+
510
+ # Place order
511
+ order_result = client.private.place_order(
512
+ market="BTC-EUR",
513
+ side="buy",
514
+ order_type="limit",
515
+ amount="0.01",
516
+ price="45000"
517
+ )
518
+
519
+ # Get order history
520
+ orders_result = client.private.orders('BTC-EUR')
521
+
522
+ # Cancel order
523
+ cancel_result = client.private.cancel_order(
524
+ market="BTC-EUR",
525
+ order_id="order-id-here"
526
+ )
527
+
528
+ # Get trades
529
+ trades_result = client.private.trades('BTC-EUR')
530
+ ```
531
+
532
+ #### Legacy Bitvavo Interface
533
+
315
534
  ```python
535
+ from bitvavo_api_upgraded import Bitvavo
536
+
537
+ bitvavo = Bitvavo({'APIKEY': 'your-key', 'APISECRET': 'your-secret'})
538
+
316
539
  # Get account info
317
540
  account = bitvavo.account()
318
541
 
@@ -524,15 +747,49 @@ uv run ruff format
524
747
  ### Project Structure
525
748
 
526
749
  ```text
527
- src/bitvavo_api_upgraded/ # Source code
528
- ├── __init__.py # Main exports
529
- ├── bitvavo.py # Core API class
530
- ├── settings.py # Pydantic settings
531
- ├── helper_funcs.py # Utility functions
532
- └── type_aliases.py # Type definitions
533
-
534
- tests/ # Comprehensive test suite
535
- docs/ # Documentation
750
+ src/
751
+ ├── bitvavo_api_upgraded/ # Legacy interface (backward compatibility)
752
+ ├── __init__.py # Main exports
753
+ ├── bitvavo.py # Original monolithic API class
754
+ ├── settings.py # Pydantic settings
755
+ │ ├── helper_funcs.py # Utility functions
756
+ │ └── type_aliases.py # Type definitions
757
+ └── bitvavo_client/ # Modern modular interface
758
+ ├── __init__.py # New client exports
759
+ ├── facade.py # Main BitvavoClient class
760
+ ├── core/ # Core functionality
761
+ │ ├── settings.py # Settings management
762
+ │ ├── models.py # Pydantic data models
763
+ │ ├── validation_helpers.py # Enhanced error handling
764
+ │ └── types.py # Type definitions
765
+ ├── endpoints/ # API endpoint handlers
766
+ │ ├── public.py # Public API endpoints
767
+ │ ├── private.py # Private API endpoints
768
+ │ └── common.py # Shared endpoint utilities
769
+ ├── transport/ # HTTP transport layer
770
+ │ └── http.py # HTTP client with connection pooling
771
+ ├── auth/ # Authentication & authorization
772
+ │ ├── signing.py # Request signing
773
+ │ └── rate_limit.py # Rate limiting management
774
+ ├── adapters/ # External integrations
775
+ │ └── returns_adapter.py # Result type adapters
776
+ ├── schemas/ # DataFrame schemas
777
+ │ ├── public_schemas.py # Public endpoint schemas
778
+ │ └── private_schemas.py # Private endpoint schemas
779
+ └── df/ # DataFrame conversion
780
+ └── convert.py # Narwhals-based converters
781
+
782
+ tests/ # Comprehensive test suite
783
+ ├── bitvavo_api_upgraded/ # Legacy interface tests
784
+ └── bitvavo_client/ # Modern interface tests
785
+ ├── core/ # Core functionality tests
786
+ ├── endpoints/ # Endpoint tests
787
+ ├── transport/ # Transport layer tests
788
+ ├── auth/ # Authentication tests
789
+ ├── adapters/ # Adapter tests
790
+ └── df/ # DataFrame tests
791
+
792
+ docs/ # Documentation
536
793
  ```
537
794
 
538
795
  ### Semantic Versioning
@@ -547,47 +804,110 @@ This project follows [semantic versioning](https://semver.org/):
547
804
 
548
805
  This package includes a `py.typed` file to enable type checking. Reference: [Don't forget py.typed for your typed Python package](https://blog.whtsky.me/tech/2021/dont-forget-py.typed-for-your-typed-python-package/)
549
806
 
550
- ## Migration from Official SDK
807
+ ## Migration & Architecture Options
808
+
809
+ This package provides **two interfaces** to suit different use cases:
810
+
811
+ ### 1. Legacy Bitvavo Class (Backward Compatibility)
812
+
813
+ For existing users migrating from the official SDK:
551
814
 
552
- ### Key Differences
815
+ ```python
816
+ from bitvavo_api_upgraded import Bitvavo
553
817
 
554
- - Import: `from bitvavo_api_upgraded import Bitvavo` (instead of `from python_bitvavo_api.bitvavo import Bitvavo`)
555
- - **Breaking Change**: Trading operations require `operatorId` parameter
556
- - **New**: Multiple API key support for better rate limiting
818
+ # Drop-in replacement for python_bitvavo_api.bitvavo
819
+ bitvavo = Bitvavo({'APIKEY': 'key', 'APISECRET': 'secret'})
820
+ balance = bitvavo.balance({})
821
+ ```
822
+
823
+ ### 2. New BitvavoClient (Modern Architecture)
824
+
825
+ For new projects or those wanting better architecture:
826
+
827
+ ```python
828
+ from bitvavo_client import BitvavoClient, BitvavoSettings
829
+
830
+ # Modern, typed, modular interface
831
+ client = BitvavoClient()
832
+ result = client.public.time()
833
+ result = client.private.balance()
834
+ ```
835
+
836
+ ### Migration from Official SDK
837
+
838
+ #### Key Changes
839
+
840
+ - **Import**: `from bitvavo_api_upgraded import Bitvavo` (instead of `from python_bitvavo_api.bitvavo import Bitvavo`)
841
+ - **Breaking**: Trading operations require `operatorId` parameter
842
+ - **Enhanced**: Better error handling and type safety
843
+ - **New**: Modern `BitvavoClient` interface available
844
+ - **New**: Multiple API key support for rate limiting
557
845
  - **New**: Keyless access for public endpoints
558
846
  - **New**: Comprehensive dataframe support
559
- - Enhanced error handling and type safety
560
- - Better configuration management with `.env` support
847
+ - **New**: Configuration via `.env` files
561
848
 
562
- ### Migration Steps
849
+ #### Migration Steps
563
850
 
564
- 1. Update import statements
565
- 2. Add `operatorId` to trading method calls
566
- 3. Optional: Migrate to `.env` configuration
567
- 4. Optional: Configure multiple API keys for better rate limits
568
- 5. Optional: Enable keyless mode for public endpoint efficiency
569
- 6. Enjoy improved type hints and error handling!
851
+ 1. **Update import statements**
570
852
 
571
- ### Enhanced Features (Optional)
853
+ ```python
854
+ # Old
855
+ from python_bitvavo_api.bitvavo import Bitvavo
572
856
 
573
- ```python
574
- # Traditional single key (works as before)
575
- bitvavo = Bitvavo({'APIKEY': 'key', 'APISECRET': 'secret'})
857
+ # New (legacy interface)
858
+ from bitvavo_api_upgraded import Bitvavo
576
859
 
577
- # New: Multiple keys for rate limiting
578
- bitvavo = Bitvavo({
579
- 'APIKEYS': [
580
- {'key': 'key1', 'secret': 'secret1'},
581
- {'key': 'key2', 'secret': 'secret2'}
582
- ]
583
- })
860
+ # New (modern interface)
861
+ from bitvavo_client import BitvavoClient
862
+ ```
584
863
 
585
- # New: Keyless for public endpoints
586
- bitvavo = Bitvavo({'PREFER_KEYLESS': True})
864
+ 2. **Add operatorId to trading operations**
587
865
 
588
- # New: Dataframe support
589
- markets_df = bitvavo.markets({}, output_format='pandas')
590
- ```
866
+ ```python
867
+ # Add operatorId parameter to placeOrder, cancelOrder, etc.
868
+ order = bitvavo.placeOrder("BTC-EUR", "buy", "limit", {...}, operatorId=12345)
869
+ ```
870
+
871
+ 3. **Optional: Migrate to modern interface**
872
+
873
+ ```python
874
+ # Legacy style
875
+ bitvavo = Bitvavo({'APIKEY': 'key', 'APISECRET': 'secret'})
876
+
877
+ # Modern style
878
+ client = BitvavoClient(BitvavoSettings(api_key='key', api_secret='secret'))
879
+ ```
880
+
881
+ 4. **Optional: Use new features**
882
+
883
+ ```python
884
+ # Multi-key support
885
+ bitvavo = Bitvavo({'APIKEYS': [{'key': 'k1', 'secret': 's1'}, {'key': 'k2', 'secret': 's2'}]})
886
+
887
+ # Keyless for public endpoints
888
+ bitvavo = Bitvavo({'PREFER_KEYLESS': True})
889
+
890
+ # DataFrame support
891
+ markets_df = bitvavo.markets({}, output_format='pandas')
892
+ ```
893
+
894
+ ### Choosing an Interface
895
+
896
+ | Feature | Legacy `Bitvavo` | Modern `BitvavoClient` |
897
+ | -------------------------- | ---------------------- | -------------------------- |
898
+ | **Backward compatibility** | ✅ Drop-in replacement | ❌ New interface |
899
+ | **Type safety** | ✅ Typed responses | ✅ Full generics support |
900
+ | **Error handling** | ✅ Enhanced errors | ✅ Result types + enhanced |
901
+ | **Modular design** | ❌ Monolithic | ✅ Separated concerns |
902
+ | **Testing** | ✅ Testable | ✅ Highly testable |
903
+ | **DataFrame support** | ✅ Via output_format | ✅ Via model preferences |
904
+ | **Result types** | ❌ Exceptions only | ✅ Success/Failure pattern |
905
+ | **WebSocket support** | ✅ Full support | 🚧 Coming soon |
906
+
907
+ **Recommendation**:
908
+
909
+ - Use **Legacy `Bitvavo`** for quick migrations and WebSocket usage
910
+ - Use **Modern `BitvavoClient`** for new projects requiring clean architecture
591
911
 
592
912
  ---
593
913
 
@@ -0,0 +1,10 @@
1
+ bitvavo_api_upgraded/__init__.py,sha256=J_HdGBmZOfb1eOydaxsPmXfOIZ58hVa1qAfE6QErUHs,301
2
+ bitvavo_api_upgraded/bitvavo.py,sha256=_3FRVVPg7_1HrALyGPjcuokCsHF5oz6itN_GKx7yTMo,166155
3
+ bitvavo_api_upgraded/dataframe_utils.py,sha256=UvcDM0HeE-thUvsm9EjCmddmGBzZ9Puu40UVa0fR_p8,5821
4
+ bitvavo_api_upgraded/helper_funcs.py,sha256=4oBdQ1xB-C2XkQTmN-refzIzWfO-IUowDSWhOSFdCRU,3212
5
+ bitvavo_api_upgraded/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ bitvavo_api_upgraded/settings.py,sha256=I1fogU6_kb1hOe_0YDzOgDhzKfnnYFoIR2OXbwtyD4E,5291
7
+ bitvavo_api_upgraded/type_aliases.py,sha256=SbPBcuKWJZPZ8DSDK-Uycu5O-TUO6ejVaTt_7oyGyIU,1979
8
+ bitvavo_api_upgraded-4.1.0.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
9
+ bitvavo_api_upgraded-4.1.0.dist-info/METADATA,sha256=Go7SrHcYNBc3aZKxf09IBC8nudujCyN_Ze7tZST3mZI,35857
10
+ bitvavo_api_upgraded-4.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.13
2
+ Generator: uv 0.8.15
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +0,0 @@
1
- bitvavo_api_upgraded/__init__.py,sha256=J_HdGBmZOfb1eOydaxsPmXfOIZ58hVa1qAfE6QErUHs,301
2
- bitvavo_api_upgraded/bitvavo.py,sha256=PHN4lPlA2wWB5e6LAOewQLGqN--x_N_BLg1A25z5TxA,164948
3
- bitvavo_api_upgraded/dataframe_utils.py,sha256=Jf-2zkYuK5Zs9kiMFJlCmO-OykjZyFIvW2aaMJYPUpo,5792
4
- bitvavo_api_upgraded/helper_funcs.py,sha256=4oBdQ1xB-C2XkQTmN-refzIzWfO-IUowDSWhOSFdCRU,3212
5
- bitvavo_api_upgraded/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- bitvavo_api_upgraded/settings.py,sha256=br1oqN3K8OoimmKa1JlvzMgpYNHfr2kK2f14tC72dl8,5311
7
- bitvavo_api_upgraded/type_aliases.py,sha256=EOd3LhruyM1aZYn4xyKYhdoSql8mZH88acN0qGzMMck,1992
8
- bitvavo_api_upgraded-4.0.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
9
- bitvavo_api_upgraded-4.0.0.dist-info/METADATA,sha256=v5Xi6jH4qRBhuZVS_TtNv23Jd6HnmRsXMXKZvV-_15s,25510
10
- bitvavo_api_upgraded-4.0.0.dist-info/RECORD,,