bitvavo-api-upgraded 4.1.0__tar.gz → 4.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/PKG-INFO +1 -1
  2. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/pyproject.toml +5 -2
  3. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/__init__.py +9 -0
  4. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/adapters/__init__.py +1 -0
  5. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/adapters/returns_adapter.py +363 -0
  6. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/auth/__init__.py +1 -0
  7. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/auth/rate_limit.py +104 -0
  8. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/auth/signing.py +33 -0
  9. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/__init__.py +1 -0
  10. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/errors.py +17 -0
  11. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/model_preferences.py +33 -0
  12. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/private_models.py +886 -0
  13. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/public_models.py +1087 -0
  14. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/settings.py +52 -0
  15. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/types.py +11 -0
  16. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/core/validation_helpers.py +90 -0
  17. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/df/__init__.py +1 -0
  18. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/df/convert.py +86 -0
  19. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/endpoints/__init__.py +1 -0
  20. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/endpoints/common.py +88 -0
  21. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/endpoints/private.py +1090 -0
  22. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/endpoints/public.py +658 -0
  23. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/facade.py +66 -0
  24. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/py.typed +0 -0
  25. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/schemas/__init__.py +50 -0
  26. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/schemas/private_schemas.py +191 -0
  27. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/schemas/public_schemas.py +149 -0
  28. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/transport/__init__.py +1 -0
  29. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/transport/http.py +159 -0
  30. bitvavo_api_upgraded-4.1.1/src/bitvavo_client/ws/__init__.py +1 -0
  31. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/README.md +0 -0
  32. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/__init__.py +0 -0
  33. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/bitvavo.py +0 -0
  34. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
  35. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
  36. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/py.typed +0 -0
  37. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/settings.py +0 -0
  38. {bitvavo_api_upgraded-4.1.0 → bitvavo_api_upgraded-4.1.1}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bitvavo-api-upgraded
3
- Version: 4.1.0
3
+ Version: 4.1.1
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>
@@ -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.1.0"
9
+ version = "4.1.1"
10
10
  description = "A unit-tested fork of the Bitvavo API"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -45,6 +45,9 @@ dependencies = [
45
45
  "websocket-client >=1.2", # something something websocket
46
46
  ]
47
47
 
48
+ [tool.uv.build-backend]
49
+ module-name = ["bitvavo_api_upgraded", "bitvavo_client"]
50
+
48
51
  [[tool.uv.index]]
49
52
  # Optional name for the index.
50
53
  name = "nvidia"
@@ -105,7 +108,7 @@ dev-dependencies = [
105
108
  ]
106
109
 
107
110
  [tool.bumpversion]
108
- current_version = "4.1.0"
111
+ current_version = "4.1.1"
109
112
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
110
113
  serialize = ["{major}.{minor}.{patch}"]
111
114
  search = "{current_version}"
@@ -0,0 +1,9 @@
1
+ """Modern, modular Bitvavo API client."""
2
+
3
+ from bitvavo_client.core.settings import BitvavoSettings
4
+ from bitvavo_client.facade import BitvavoClient
5
+
6
+ __all__ = [
7
+ "BitvavoClient",
8
+ "BitvavoSettings",
9
+ ]
@@ -0,0 +1 @@
1
+ """Optional adapters for alternative result-handling styles."""
@@ -0,0 +1,363 @@
1
+ """Adapter utilities that return `returns.result.Result` types.
2
+
3
+ This integrates clean Result-based decoding and lightweight request helpers on top
4
+ of httpx, mapping Bitvavo errors to a structured model.
5
+
6
+ The adapter is optional and doesn't alter the existing facade. Users who prefer
7
+ functional error handling can import and use these utilities directly.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
13
+
14
+ import httpx
15
+ from pydantic import BaseModel, Field
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+ from returns.result import Failure, Result, Success
18
+ from structlog.stdlib import get_logger
19
+
20
+ if TYPE_CHECKING: # pragma: no cover
21
+ from collections.abc import Mapping
22
+
23
+ T = TypeVar("T")
24
+
25
+ logger = get_logger("bitvavo.adapter")
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Settings (pydantic v2)
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ class BitvavoReturnsSettings(BaseSettings):
34
+ """Settings for the returns-based adapter.
35
+
36
+ These are intentionally separate from the core BitvavoSettings to avoid
37
+ coupling and to let users override independently via environment vars.
38
+ """
39
+
40
+ model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
41
+ env_prefix="BITVAVO_",
42
+ extra="ignore",
43
+ env_file=".env",
44
+ env_file_encoding="utf-8",
45
+ )
46
+
47
+ base_url: str = "https://api.bitvavo.com/v2"
48
+ timeout_seconds: float = 10.0
49
+
50
+
51
+ settings = BitvavoReturnsSettings()
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Domain models
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ class BitvavoErrorPayload(BaseModel):
60
+ """Typical Bitvavo error payload.
61
+
62
+ Example:
63
+ {"errorCode": 205, "error": "Invalid parameter value."}
64
+ """
65
+
66
+ error_code: int = Field(alias="errorCode")
67
+ error: str
68
+
69
+
70
+ class BitvavoError(BaseModel):
71
+ http_status: int
72
+ error_code: int
73
+ reason: str
74
+ message: str
75
+ raw: dict[str, Any]
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Error code directory (from Bitvavo docs)
80
+ # ---------------------------------------------------------------------------
81
+
82
+ _BITVAVO_ERROR_REASONS: dict[int, dict[int, str]] = {
83
+ 400: {
84
+ 102: "The JSON object you sent is invalid.",
85
+ 200: "Path parameters are not accepted for this endpoint.",
86
+ 201: "Body parameters are not accepted for this endpoint.",
87
+ 202: "A parameter is not supported for this orderType.",
88
+ 203: "Missing or incompatible parameters in your request.",
89
+ 204: "You used a parameter that is not valid for this endpoint.",
90
+ 205: "Invalid parameter value.",
91
+ 206: "Incompatible parameters in your call.",
92
+ 210: "amount exceeds the maximum allowed for the market.",
93
+ 211: "price exceeds the maximum allowed.",
94
+ 212: "amount is lower than the minimum allowed for the market.",
95
+ 213: "price is too low.",
96
+ 214: "price has too many significant digits.",
97
+ 215: "price has more than 15 decimal places.",
98
+ 216: "Insufficient balance to perform this operation.",
99
+ 217: "amountQuote is lower than the minimum allowed for the market.",
100
+ 218: "triggerAmount has too many significant digits.",
101
+ 219: "The market is no longer listed on Bitvavo.",
102
+ 220: "clientOrderId conflict within the market.",
103
+ 231: "timeInForce must be set to GTC when markets are paused.",
104
+ 232: "Changes required for a successful update to your order.",
105
+ 234: "Cannot update a market order type.",
106
+ 235: "Maximum of 100 open orders per market exceeded.",
107
+ 236: "Only one of amount, amountRemaining, or amountQuote is allowed.",
108
+ 237: "Required parameters missing for stopLoss order type.",
109
+ 238: "Required parameters missing for stopLossLimit order type.",
110
+ 239: "Cannot switch between amount and amountQuote during an update.",
111
+ 401: "Deposits for this asset are not available.",
112
+ 402: "Verify your identity to deposit or withdraw assets.",
113
+ 403: "Verify your phone number to deposit or withdraw assets.",
114
+ 404: "Could not complete the operation due to an internal error.",
115
+ 405: "Withdrawal not allowed during the cooldown period.",
116
+ 406: "amount is below the minimum allowed value.",
117
+ 407: "Internal transfer is not possible.",
118
+ 408: "Insufficient balance to perform this operation.",
119
+ 409: "Verify your bank account and try again.",
120
+ 410: "Withdrawals for this asset are not available.",
121
+ 411: "You cannot transfer assets to yourself.",
122
+ 412: "Error during deposit or withdrawal.",
123
+ 413: "IP address not in your whitelist.",
124
+ 414: "Cannot withdraw assets within 2 minutes of logging in.",
125
+ 422: "Invalid price tick size.",
126
+ 423: "Market halted due to maintenance or other reasons.",
127
+ 424: "Market is in cancelOnly status.",
128
+ 425: "Market is in an auction phase.",
129
+ 426: "Market is in auctionMatching status.",
130
+ 429: "Too many decimal places in a parameter value.",
131
+ },
132
+ 403: {
133
+ 300: "Authentication required to call this endpoint.",
134
+ 301: "Invalid API key length.",
135
+ 302: "Timestamp for authentication must be in milliseconds.",
136
+ 303: "Access window must be between 100 and 60000 milliseconds.",
137
+ 304: "Request not received within the access window.",
138
+ 305: "API key is not active.",
139
+ 306: "API key activation not confirmed.",
140
+ 307: "IP not whitelisted for this API key.",
141
+ 308: "Invalid signature format.",
142
+ 309: "Invalid signature.",
143
+ 310: "API key lacks trading endpoint permissions.",
144
+ 311: "API key lacks account endpoint permissions.",
145
+ 312: "API key lacks withdrawal permissions.",
146
+ 313: "Invalid Bitvavo session.",
147
+ 316: "Public asset information only via this WebSocket.",
148
+ 317: "Account locked. Contact support.",
149
+ 318: "Account verification required for API use.",
150
+ 319: "Feature unavailable in your region.",
151
+ 320: "Operation forbidden. Please contact support.",
152
+ },
153
+ 404: {
154
+ 240: "Order not found or no longer active.",
155
+ 415: "Unknown WebSocket action. Verify against the API reference.",
156
+ },
157
+ 409: {
158
+ 431: "Cannot get market data; market is halted/auction/auctionMatching.",
159
+ },
160
+ 429: {
161
+ 105: "Rate limit exceeded. Account or IP address blocked temporarily.",
162
+ 112: "Rate limit exceeded for WebSocket requests per second.",
163
+ },
164
+ 500: {
165
+ 101: "Unknown server error. Operation success uncertain.",
166
+ 400: "Unknown server error. Contact support.",
167
+ },
168
+ 503: {
169
+ 107: "Bitvavo is overloaded. Retry after 500ms.",
170
+ 108: "Processing issue. Increase execution window or retry after 500ms.",
171
+ 109: "Timeout. Operation success uncertain.",
172
+ 111: "Matching engine temporarily unavailable.",
173
+ 419: "The server is unavailable.",
174
+ 430: "Connection timed out due to no new market data events.",
175
+ },
176
+ }
177
+
178
+
179
+ # HTTP status helpers
180
+ HTTP_STATUS_OK_MIN = 200
181
+ HTTP_STATUS_OK_MAX = 299
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # Core decoding → Result
186
+ # ---------------------------------------------------------------------------
187
+
188
+
189
+ def _json_from_response(resp: httpx.Response) -> dict[str, Any]:
190
+ try:
191
+ data = resp.json() # type: ignore[no-any-return]
192
+ assert isinstance(data, dict), "Expected JSON response to be a dictionary"
193
+ except ValueError:
194
+ return {"raw": resp.text}
195
+ else:
196
+ return data
197
+
198
+
199
+ def _map_error(resp: httpx.Response) -> BitvavoError:
200
+ payload = _json_from_response(resp)
201
+ code = int(payload.get("errorCode", -1))
202
+ message = str(payload.get("error", payload.get("message", "")) or "")
203
+ reason_dir = _BITVAVO_ERROR_REASONS.get(resp.status_code, {})
204
+ reason = reason_dir.get(code, message or "Unknown error")
205
+ return BitvavoError(
206
+ http_status=resp.status_code,
207
+ error_code=code,
208
+ reason=reason,
209
+ message=message or reason,
210
+ raw=payload if isinstance(payload, dict) else {"raw": payload},
211
+ )
212
+
213
+
214
+ def _validation_failure(reason: str, payload: dict[str, Any]) -> BitvavoError:
215
+ return BitvavoError(
216
+ http_status=500,
217
+ error_code=-1,
218
+ reason="Model validation failed",
219
+ message=reason,
220
+ raw=payload,
221
+ )
222
+
223
+
224
+ def _enhance_dataframe_error(exc: Exception, data: Any, schema: Mapping[str, object] | None, model: type) -> str:
225
+ """Create enhanced error message for DataFrame schema mismatches."""
226
+ error_msg = str(exc)
227
+
228
+ if "column-schema names do not match the data dictionary" not in error_msg:
229
+ return error_msg
230
+
231
+ # Extract actual field names from the data
232
+ if isinstance(data, dict):
233
+ actual_fields = list(data.keys())
234
+ elif isinstance(data, list) and data and isinstance(data[0], dict):
235
+ actual_fields = list(data[0].keys())
236
+ else:
237
+ actual_fields = []
238
+
239
+ # Extract expected field names from schema
240
+ expected_fields = list(schema.keys()) if schema else []
241
+
242
+ model_name = getattr(model, "__name__", "DataFrame")
243
+ return (
244
+ f"DataFrame schema mismatch for {model_name}:\n"
245
+ f" Expected fields: {expected_fields}\n"
246
+ f" Actual fields: {actual_fields}\n"
247
+ f" Missing fields: {set(expected_fields) - set(actual_fields)}\n"
248
+ f" Extra fields: {set(actual_fields) - set(expected_fields)}\n"
249
+ f" Original error: {error_msg}"
250
+ )
251
+
252
+
253
+ def decode_response_result( # noqa: C901 (complexity)
254
+ resp: httpx.Response,
255
+ model: type[T] | None,
256
+ schema: Mapping[str, object] | None = None,
257
+ ) -> Result[T | Any, BitvavoError]:
258
+ if not (HTTP_STATUS_OK_MIN <= resp.status_code <= HTTP_STATUS_OK_MAX):
259
+ return Failure(_map_error(resp))
260
+
261
+ try:
262
+ data = resp.json() # type: ignore[no-any-return]
263
+ except ValueError:
264
+ data: Any = {"raw": resp.text}
265
+
266
+ if model is Any:
267
+ # data is invalid (an error of sorts)
268
+ if isinstance(data, dict) and any(key in data for key in ["errorCode", "error", "message"]):
269
+ return Failure(_map_error(resp))
270
+
271
+ # return the raw (valid) JSON
272
+ return Success(data)
273
+
274
+ assert model is not None, "Model must be provided or set to Any"
275
+ assert isinstance(data, (dict, list)), "Expected JSON response to be a dictionary or list"
276
+
277
+ try:
278
+ # Support pydantic model classes/instances and common DataFrame libraries
279
+ # (pandas / polars). Also fall back to calling a constructor if provided.
280
+ if (isinstance(model, type) and issubclass(model, BaseModel)) or isinstance(model, BaseModel):
281
+ parsed = model.model_validate(data)
282
+ elif schema is None:
283
+ parsed = model(data) # type: ignore[arg-type]
284
+ else:
285
+ # I don't like the complexity of this piece, but it's needed because the data from ticker_book may return an
286
+ # int when it should be a float... Why is their DB such a damned mess? Fuck me, man...
287
+ try:
288
+ import polars as pl # noqa: PLC0415
289
+
290
+ if model is pl.DataFrame:
291
+ parsed = model(data, schema=schema, strict=False) # type: ignore[arg-type]
292
+ else:
293
+ parsed = model(data, schema=schema) # type: ignore[arg-type]
294
+ except ImportError:
295
+ parsed = model(data, schema=schema) # type: ignore[arg-type]
296
+ return Success(parsed)
297
+ except Exception as exc: # noqa: BLE001
298
+ # If the payload looks like a Bitvavo error, map it so callers get a structured error.
299
+ if isinstance(data, dict) and any(key in data for key in ["errorCode", "error", "message"]):
300
+ return Failure(_map_error(resp))
301
+
302
+ # Enhanced error message for DataFrame schema mismatches
303
+ enhanced_error = _enhance_dataframe_error(exc, data, schema, model)
304
+
305
+ logger.warning(
306
+ "model_validation-failed",
307
+ error=enhanced_error,
308
+ exception_type=type(exc).__name__,
309
+ payload=data,
310
+ )
311
+ return Failure(_validation_failure(enhanced_error, data if isinstance(data, dict) else {"raw": data}))
312
+
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # Sync request wrappers returning Result
316
+ # ---------------------------------------------------------------------------
317
+
318
+
319
+ def get_json_result(
320
+ client: httpx.Client,
321
+ path: str,
322
+ *,
323
+ model: type[T],
324
+ ) -> Result[T | dict[str, Any], BitvavoError]:
325
+ url = f"{settings.base_url.rstrip('/')}/{path.lstrip('/')}"
326
+ try:
327
+ resp = client.get(url, timeout=settings.timeout_seconds)
328
+ except httpx.HTTPError as exc:
329
+ logger.error("HTTP request failed: %s", exc)
330
+ return Failure(
331
+ BitvavoError(
332
+ http_status=0,
333
+ error_code=-1,
334
+ reason="Transport error",
335
+ message=str(exc),
336
+ raw={},
337
+ ),
338
+ )
339
+ return decode_response_result(resp, model=model)
340
+
341
+
342
+ def post_json_result(
343
+ client: httpx.Client,
344
+ path: str,
345
+ payload: dict[str, Any] | None = None,
346
+ *,
347
+ model: type[T] | None = None,
348
+ ) -> Result[T | dict[str, Any], BitvavoError]:
349
+ url = f"{settings.base_url.rstrip('/')}/{path.lstrip('/')}"
350
+ try:
351
+ resp = client.post(url, json=payload or {}, timeout=settings.timeout_seconds)
352
+ except httpx.HTTPError as exc:
353
+ logger.error("HTTP request failed: %s", exc)
354
+ return Failure(
355
+ BitvavoError(
356
+ http_status=0,
357
+ error_code=-1,
358
+ reason="Transport error",
359
+ message=str(exc),
360
+ raw={"payload": payload or {}},
361
+ ),
362
+ )
363
+ return decode_response_result(resp, model=model)
@@ -0,0 +1 @@
1
+ """Auth modules for bitvavo_client."""
@@ -0,0 +1,104 @@
1
+ """Rate limiting manager for Bitvavo API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+
8
+ class RateLimitManager:
9
+ """Manages rate limiting for multiple API keys and keyless requests.
10
+
11
+ Each API key index has its own rate limit state. Index -1 is reserved
12
+ for keyless requests.
13
+ """
14
+
15
+ def __init__(self, default_remaining: int, buffer: int) -> None:
16
+ """Initialize rate limit manager.
17
+
18
+ Args:
19
+ default_remaining: Default rate limit amount
20
+ buffer: Buffer to keep before hitting limit
21
+ """
22
+ self.state: dict[int, dict[str, int]] = {-1: {"remaining": default_remaining, "resetAt": 0}}
23
+ self.buffer: int = buffer
24
+
25
+ def ensure_key(self, idx: int) -> None:
26
+ """Ensure a key index exists in the state."""
27
+ if idx not in self.state:
28
+ self.state[idx] = {"remaining": self.state[-1]["remaining"], "resetAt": 0}
29
+
30
+ def has_budget(self, idx: int, weight: int) -> bool:
31
+ """Check if there's enough rate limit budget for a request.
32
+
33
+ Args:
34
+ idx: API key index (-1 for keyless)
35
+ weight: Weight of the request
36
+
37
+ Returns:
38
+ True if request can be made within rate limits
39
+ """
40
+ self.ensure_key(idx)
41
+ return (self.state[idx]["remaining"] - weight) >= self.buffer
42
+
43
+ def update_from_headers(self, idx: int, headers: dict[str, str]) -> None:
44
+ """Update rate limit state from response headers.
45
+
46
+ Args:
47
+ idx: API key index
48
+ headers: HTTP response headers
49
+ """
50
+ self.ensure_key(idx)
51
+
52
+ remaining = headers.get("bitvavo-ratelimit-remaining")
53
+ reset_at = headers.get("bitvavo-ratelimit-resetat")
54
+
55
+ if remaining is not None:
56
+ self.state[idx]["remaining"] = int(remaining)
57
+ if reset_at is not None:
58
+ self.state[idx]["resetAt"] = int(reset_at)
59
+
60
+ def update_from_error(self, idx: int, _err: dict[str, object]) -> None:
61
+ """Update rate limit state from API error response.
62
+
63
+ Args:
64
+ idx: API key index
65
+ _err: Error response from API (unused but kept for interface compatibility)
66
+ """
67
+ self.ensure_key(idx)
68
+ self.state[idx]["remaining"] = 0
69
+ self.state[idx]["resetAt"] = int(time.time() * 1000) + 60_000
70
+
71
+ def sleep_until_reset(self, idx: int) -> None:
72
+ """Sleep until rate limit resets for given key index.
73
+
74
+ Args:
75
+ idx: API key index
76
+ """
77
+ self.ensure_key(idx)
78
+ now = int(time.time() * 1000)
79
+ ms_left = max(0, self.state[idx]["resetAt"] - now)
80
+ time.sleep(ms_left / 1000 + 1)
81
+
82
+ def get_remaining(self, idx: int) -> int:
83
+ """Get remaining rate limit for key index.
84
+
85
+ Args:
86
+ idx: API key index
87
+
88
+ Returns:
89
+ Remaining rate limit count
90
+ """
91
+ self.ensure_key(idx)
92
+ return self.state[idx]["remaining"]
93
+
94
+ def get_reset_at(self, idx: int) -> int:
95
+ """Get reset timestamp for key index.
96
+
97
+ Args:
98
+ idx: API key index
99
+
100
+ Returns:
101
+ Reset timestamp in milliseconds
102
+ """
103
+ self.ensure_key(idx)
104
+ return self.state[idx]["resetAt"]
@@ -0,0 +1,33 @@
1
+ """Signature creation utilities for Bitvavo API authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING: # pragma: no cover
11
+ from bitvavo_client.core.types import AnyDict
12
+
13
+
14
+ def create_signature(timestamp: int, method: str, url: str, body: AnyDict | None, api_secret: str) -> str:
15
+ """Create HMAC-SHA256 signature for Bitvavo API authentication.
16
+
17
+ Args:
18
+ timestamp: Unix timestamp in milliseconds
19
+ method: HTTP method (GET, POST, PUT, DELETE)
20
+ url: API endpoint URL without base URL
21
+ body: Request body as dictionary (optional)
22
+ api_secret: API secret key
23
+
24
+ Returns:
25
+ HMAC-SHA256 signature as hexadecimal string
26
+ """
27
+ string = f"{timestamp}{method}/v2{url}"
28
+ if body is not None and len(body) > 0:
29
+ string += json.dumps(body, separators=(",", ":"))
30
+
31
+ signature = hmac.new(api_secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha256).hexdigest()
32
+
33
+ return signature
@@ -0,0 +1 @@
1
+ """Core modules for bitvavo_client."""
@@ -0,0 +1,17 @@
1
+ """Error definitions for bitvavo_client."""
2
+
3
+
4
+ class BitvavoError(Exception): # pragma: no cover
5
+ """Base exception for Bitvavo API errors."""
6
+
7
+
8
+ class RateLimitError(BitvavoError): # pragma: no cover
9
+ """Raised when rate limit is exceeded."""
10
+
11
+
12
+ class AuthenticationError(BitvavoError): # pragma: no cover
13
+ """Raised when authentication fails."""
14
+
15
+
16
+ class NetworkError(BitvavoError): # pragma: no cover
17
+ """Raised when network operations fail."""
@@ -0,0 +1,33 @@
1
+ """Model preference enum for API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from enum import Enum
7
+
8
+ if sys.version_info >= (3, 11):
9
+ from enum import StrEnum
10
+ else:
11
+
12
+ class StrEnum(str, Enum):
13
+ """Compatibility StrEnum for Python < 3.11."""
14
+
15
+
16
+ class ModelPreference(StrEnum):
17
+ """Enumeration of available model preferences for API responses.
18
+
19
+ This enum allows users to specify their preferred response format across
20
+ all API methods without having to pass model parameters to each call.
21
+ """
22
+
23
+ # Return raw Python data structures (dict/list)
24
+ RAW = "raw"
25
+
26
+ # Return Polars DataFrame (requires schema)
27
+ DATAFRAME = "dataframe"
28
+
29
+ # Return appropriate Pydantic model for each endpoint
30
+ PYDANTIC = "pydantic"
31
+
32
+ # Let each method use its own default (legacy behavior)
33
+ AUTO = "auto"