bitvavo-api-upgraded 4.1.0__py3-none-any.whl → 4.1.1__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.
@@ -0,0 +1,66 @@
1
+ """Main facade for the Bitvavo client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, TypeVar
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
+ T = TypeVar("T")
17
+
18
+
19
+ class BitvavoClient:
20
+ """
21
+ Main Bitvavo API client facade providing backward-compatible interface.
22
+
23
+ TODO(NostraDavid): add mechanisms to get a ton of data efficiently, which then uses the public and private APIs.
24
+ Otherwise, users can just grab the data themselves via the public and private API endpoints.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ settings: BitvavoSettings | None = None,
30
+ *,
31
+ preferred_model: ModelPreference | str | None = None,
32
+ default_schema: dict | None = None,
33
+ ) -> None:
34
+ """Initialize Bitvavo client.
35
+
36
+ Args:
37
+ settings: Optional settings override. If None, uses defaults.
38
+ preferred_model: Preferred model format for responses
39
+ default_schema: Default schema for DataFrame conversion
40
+ """
41
+ self.settings = settings or BitvavoSettings()
42
+ self.rate_limiter = RateLimitManager(
43
+ self.settings.default_rate_limit,
44
+ self.settings.rate_limit_buffer,
45
+ )
46
+ self.http = HTTPClient(self.settings, self.rate_limiter)
47
+
48
+ # Initialize API endpoint handlers with preferred model settings
49
+ self.public = PublicAPI(self.http, preferred_model=preferred_model, default_schema=default_schema)
50
+ self.private = PrivateAPI(self.http, preferred_model=preferred_model, default_schema=default_schema)
51
+
52
+ # Configure API keys if available
53
+ self._configure_api_keys()
54
+
55
+ def _configure_api_keys(self) -> None:
56
+ """Configure API keys for authentication."""
57
+ if self.settings.api_key and self.settings.api_secret:
58
+ # Single API key configuration
59
+ self.http.configure_key(self.settings.api_key, self.settings.api_secret, 0)
60
+ self.rate_limiter.ensure_key(0)
61
+ elif self.settings.api_keys:
62
+ # Multiple API keys - configure the first one by default
63
+ if self.settings.api_keys:
64
+ first_key = self.settings.api_keys[0]
65
+ self.http.configure_key(first_key["key"], first_key["secret"], 0)
66
+ self.rate_limiter.ensure_key(0)
File without changes
@@ -0,0 +1,50 @@
1
+ """Schema definitions for bitvavo_client."""
2
+
3
+ from bitvavo_client.schemas import private_schemas, public_schemas
4
+ from bitvavo_client.schemas.private_schemas import (
5
+ BALANCE_SCHEMA,
6
+ DEPOSIT_HISTORY_SCHEMA,
7
+ DEPOSIT_SCHEMA,
8
+ FEES_SCHEMA,
9
+ ORDER_SCHEMA,
10
+ ORDERS_SCHEMA,
11
+ WITHDRAWAL_SCHEMA,
12
+ WITHDRAWALS_SCHEMA,
13
+ )
14
+ from bitvavo_client.schemas.public_schemas import (
15
+ ASSETS_SCHEMA,
16
+ BOOK_SCHEMA,
17
+ CANDLES_SCHEMA,
18
+ COMBINED_DEFAULT_SCHEMA,
19
+ DEFAULT_SCHEMAS,
20
+ MARKETS_SCHEMA,
21
+ TICKER_24H_SCHEMA,
22
+ TICKER_BOOK_SCHEMA,
23
+ TICKER_PRICE_SCHEMA,
24
+ TIME_SCHEMA,
25
+ TRADES_SCHEMA,
26
+ )
27
+
28
+ __all__ = [
29
+ "ASSETS_SCHEMA",
30
+ "BALANCE_SCHEMA",
31
+ "BOOK_SCHEMA",
32
+ "CANDLES_SCHEMA",
33
+ "COMBINED_DEFAULT_SCHEMA",
34
+ "DEFAULT_SCHEMAS",
35
+ "DEPOSIT_HISTORY_SCHEMA",
36
+ "DEPOSIT_SCHEMA",
37
+ "FEES_SCHEMA",
38
+ "MARKETS_SCHEMA",
39
+ "ORDERS_SCHEMA",
40
+ "ORDER_SCHEMA",
41
+ "TICKER_24H_SCHEMA",
42
+ "TICKER_BOOK_SCHEMA",
43
+ "TICKER_PRICE_SCHEMA",
44
+ "TIME_SCHEMA",
45
+ "TRADES_SCHEMA",
46
+ "WITHDRAWALS_SCHEMA",
47
+ "WITHDRAWAL_SCHEMA",
48
+ "private_schemas",
49
+ "public_schemas",
50
+ ]
@@ -0,0 +1,191 @@
1
+ """Polars DataFrame schemas for private API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import polars as pl
8
+
9
+ if TYPE_CHECKING: # pragma: no cover
10
+ from collections.abc import Mapping
11
+
12
+
13
+ # Balance endpoint schema
14
+ BALANCE_SCHEMA: dict[str, type[pl.Categorical | pl.Float64]] = {
15
+ "symbol": pl.Categorical,
16
+ "available": pl.Float64,
17
+ "inOrder": pl.Float64,
18
+ }
19
+
20
+ # Order endpoint schema (for individual orders)
21
+ ORDER_SCHEMA: dict[str, type[pl.String | pl.Int64 | pl.Boolean | pl.Categorical | pl.Float64] | pl.List] = {
22
+ "orderId": pl.String,
23
+ "market": pl.Categorical,
24
+ "created": pl.Int64,
25
+ "updated": pl.Int64,
26
+ "status": pl.Categorical,
27
+ "side": pl.Categorical,
28
+ "orderType": pl.Categorical,
29
+ "clientOrderId": pl.String,
30
+ "selfTradePrevention": pl.Categorical,
31
+ "visible": pl.Boolean,
32
+ "onHold": pl.Float64,
33
+ "onHoldCurrency": pl.Categorical,
34
+ "fills": pl.List(
35
+ pl.Struct(
36
+ {
37
+ "id": pl.String,
38
+ "timestamp": pl.Int64,
39
+ "amount": pl.Float64,
40
+ "price": pl.Float64,
41
+ "taker": pl.Boolean,
42
+ "fee": pl.String,
43
+ "feeCurrency": pl.Categorical,
44
+ "settled": pl.Boolean,
45
+ }
46
+ )
47
+ ),
48
+ "feePaid": pl.Float64,
49
+ "feeCurrency": pl.Categorical,
50
+ "operatorId": pl.Int64,
51
+ "price": pl.Float64,
52
+ "timeInForce": pl.Categorical,
53
+ "postOnly": pl.Boolean,
54
+ "amount": pl.Float64,
55
+ "amountRemaining": pl.Float64,
56
+ "filledAmount": pl.Float64,
57
+ "filledAmountQuote": pl.Float64,
58
+ "createdNs": pl.Int64,
59
+ "updatedNs": pl.Int64,
60
+ }
61
+
62
+ # Orders endpoint schema (for lists of orders)
63
+ ORDERS_SCHEMA = ORDER_SCHEMA.copy()
64
+
65
+ # Cancel order response schema
66
+ CANCEL_ORDER_SCHEMA: dict[str, type[pl.String | pl.Int64]] = {
67
+ "orderId": pl.String,
68
+ "clientOrderId": pl.String,
69
+ "operatorId": pl.Int64,
70
+ }
71
+
72
+ # Trade endpoint schema (for private trades)
73
+ TRADE_SCHEMA: dict[str, type[pl.String | pl.Int64 | pl.Categorical | pl.Boolean]] = {
74
+ "id": pl.String,
75
+ "timestamp": pl.Int64,
76
+ "amount": pl.String,
77
+ "price": pl.String,
78
+ "side": pl.Categorical,
79
+ "market": pl.Categorical,
80
+ "fee": pl.String,
81
+ "feeCurrency": pl.Categorical,
82
+ "settled": pl.Boolean,
83
+ }
84
+
85
+ # Trades endpoint schema (for lists of trades)
86
+ TRADES_SCHEMA = TRADE_SCHEMA.copy()
87
+
88
+ # Fees endpoint schema
89
+ FEES_SCHEMA: dict[str, type[pl.Int32 | pl.String]] = {
90
+ "tier": pl.Int32,
91
+ "volume": pl.String,
92
+ "maker": pl.String,
93
+ "taker": pl.String,
94
+ }
95
+
96
+ # Deposit endpoint schema
97
+ DEPOSIT_SCHEMA: dict[str, type[pl.String | pl.Int64 | pl.Categorical]] = {
98
+ "timestamp": pl.Int64,
99
+ "symbol": pl.Categorical,
100
+ "amount": pl.String,
101
+ "fee": pl.String,
102
+ "status": pl.Categorical,
103
+ "address": pl.String,
104
+ "paymentId": pl.String,
105
+ "txId": pl.String,
106
+ }
107
+
108
+ # Deposits endpoint schema (for lists of deposits)
109
+ DEPOSIT_HISTORY_SCHEMA = DEPOSIT_SCHEMA.copy()
110
+
111
+ # Withdrawal endpoint schema (for withdrawal history)
112
+ WITHDRAWAL_SCHEMA: dict[str, type[pl.String | pl.Int64 | pl.Categorical]] = {
113
+ "timestamp": pl.Int64,
114
+ "symbol": pl.Categorical,
115
+ "amount": pl.String,
116
+ "fee": pl.String,
117
+ "status": pl.Categorical,
118
+ "address": pl.String,
119
+ "txId": pl.String,
120
+ }
121
+
122
+ # Withdraw response schema (for withdraw operation response)
123
+ WITHDRAW_RESPONSE_SCHEMA: dict[str, type[pl.String | pl.Boolean | pl.Categorical]] = {
124
+ "success": pl.Boolean,
125
+ "symbol": pl.Categorical,
126
+ "amount": pl.String,
127
+ }
128
+
129
+ # Withdrawals endpoint schema (for lists of withdrawals)
130
+ WITHDRAWALS_SCHEMA = WITHDRAWAL_SCHEMA.copy()
131
+
132
+ # Deposit data endpoint schema (for deposit information)
133
+ DEPOSIT_DATA_SCHEMA: dict[str, type[pl.String]] = {
134
+ "address": pl.String,
135
+ "paymentid": pl.String,
136
+ "iban": pl.String,
137
+ "bic": pl.String,
138
+ "description": pl.String,
139
+ }
140
+
141
+ # Transaction history item schema (minimal - only core fields that are always present)
142
+ TRANSACTION_HISTORY_ITEM_SCHEMA: dict[str, type[pl.String | pl.Categorical]] = {
143
+ "transactionId": pl.String,
144
+ "executedAt": pl.String,
145
+ "type": pl.Categorical,
146
+ # Optional fields that may or may not be present depending on transaction type:
147
+ # priceCurrency, priceAmount, sentCurrency, sentAmount,
148
+ # receivedCurrency, receivedAmount, feesCurrency, feesAmount, address
149
+ }
150
+
151
+ # Alternative comprehensive schema for cases where all fields are known to be present
152
+ TRANSACTION_HISTORY_ITEM_FULL_SCHEMA: dict[str, type[pl.String | pl.Categorical]] = {
153
+ "transactionId": pl.String,
154
+ "executedAt": pl.String,
155
+ "type": pl.Categorical,
156
+ "priceCurrency": pl.Categorical,
157
+ "priceAmount": pl.String,
158
+ "sentCurrency": pl.Categorical,
159
+ "sentAmount": pl.String,
160
+ "receivedCurrency": pl.Categorical,
161
+ "receivedAmount": pl.String,
162
+ "feesCurrency": pl.Categorical,
163
+ "feesAmount": pl.String,
164
+ "address": pl.String,
165
+ }
166
+
167
+ # Transaction history response schema (for DataFrame containing transaction items)
168
+ # Since transaction_history now returns tuple(items_df, metadata_dict),
169
+ # the DataFrame contains transaction items, not pagination metadata
170
+ TRANSACTION_HISTORY_SCHEMA = TRANSACTION_HISTORY_ITEM_SCHEMA.copy()
171
+
172
+ # Default schemas mapping for each private endpoint
173
+ # note that it doesn't always make sense for certain schemas to exist.
174
+ DEFAULT_SCHEMAS: dict[str, Mapping[str, object]] = {
175
+ "account": {}, # placeholder - method will return Failure for DataFrame requests
176
+ "balance": BALANCE_SCHEMA,
177
+ "order": ORDER_SCHEMA,
178
+ "orders": ORDERS_SCHEMA,
179
+ "cancel_order": CANCEL_ORDER_SCHEMA,
180
+ "trade_history": TRADES_SCHEMA,
181
+ "transaction_history": TRANSACTION_HISTORY_SCHEMA,
182
+ "fees": FEES_SCHEMA,
183
+ "deposit": {}, # placeholder - method will return Failure for DataFrame requests
184
+ "deposit_history": DEPOSIT_HISTORY_SCHEMA,
185
+ "withdraw": WITHDRAW_RESPONSE_SCHEMA,
186
+ "withdrawal": WITHDRAWAL_SCHEMA,
187
+ "withdrawals": WITHDRAWALS_SCHEMA,
188
+ }
189
+
190
+ # Combined default schema for when you want all endpoints to use DataFrames
191
+ COMBINED_DEFAULT_SCHEMA: dict[str, Mapping[str, object]] = DEFAULT_SCHEMAS.copy()
@@ -0,0 +1,149 @@
1
+ """Polars DataFrame schemas for public API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import polars as pl
8
+
9
+ if TYPE_CHECKING: # pragma: no cover
10
+ from collections.abc import Mapping
11
+
12
+ # Time endpoint schema
13
+ TIME_SCHEMA: dict[str, type[pl.Int64]] = {
14
+ "time": pl.Int64,
15
+ "timeNs": pl.Int64,
16
+ }
17
+
18
+ # Markets endpoint schema
19
+ MARKETS_SCHEMA: dict[str, type[pl.Categorical | pl.Int8 | pl.Float64] | pl.List] = {
20
+ "market": pl.Categorical,
21
+ "status": pl.Categorical,
22
+ "base": pl.Categorical,
23
+ "quote": pl.Categorical,
24
+ "pricePrecision": pl.Int8,
25
+ "minOrderInBaseAsset": pl.Float64,
26
+ "minOrderInQuoteAsset": pl.Float64,
27
+ "maxOrderInBaseAsset": pl.Float64,
28
+ "maxOrderInQuoteAsset": pl.Float64,
29
+ "quantityDecimals": pl.Int8,
30
+ "notionalDecimals": pl.Int8,
31
+ "tickSize": pl.Float64,
32
+ "maxOpenOrders": pl.Int8,
33
+ "feeCategory": pl.Categorical,
34
+ "orderTypes": pl.List(pl.String),
35
+ }
36
+
37
+ # Assets endpoint schema
38
+ ASSETS_SCHEMA: dict[str, type[pl.Categorical | pl.String | pl.Int8 | pl.Int16 | pl.Float64] | pl.List] = {
39
+ "symbol": pl.Categorical,
40
+ "name": pl.String,
41
+ "decimals": pl.Int8,
42
+ "depositFee": pl.Int8,
43
+ "depositConfirmations": pl.Int16,
44
+ "depositStatus": pl.Categorical,
45
+ "withdrawalFee": pl.Float64,
46
+ "withdrawalMinAmount": pl.Float64,
47
+ "withdrawalStatus": pl.Categorical,
48
+ "networks": pl.List(pl.Categorical),
49
+ "message": pl.String,
50
+ }
51
+
52
+ # Order book endpoint schema
53
+ BOOK_SCHEMA: dict[str, type[pl.Categorical | pl.Int32 | pl.Int64] | pl.List] = {
54
+ "market": pl.Categorical,
55
+ "nonce": pl.Int32,
56
+ "bids": pl.List(pl.String),
57
+ "asks": pl.List(pl.String),
58
+ "timestamp": pl.Int64,
59
+ }
60
+ # Public trades endpoint schema
61
+ TRADES_SCHEMA: dict[str, type[pl.String | pl.Int64 | pl.Float64 | pl.Categorical]] = {
62
+ "id": pl.String,
63
+ "timestamp": pl.Int64,
64
+ "amount": pl.Float64,
65
+ "price": pl.Float64,
66
+ "side": pl.Categorical,
67
+ }
68
+
69
+ # Candles endpoint schema
70
+ CANDLES_SCHEMA: dict[str, type[pl.Int64 | pl.Float64]] = {
71
+ "timestamp": pl.Int64,
72
+ "open": pl.Float64,
73
+ "high": pl.Float64,
74
+ "low": pl.Float64,
75
+ "close": pl.Float64,
76
+ "volume": pl.Float64,
77
+ }
78
+
79
+ # Ticker price endpoint schema
80
+ TICKER_PRICE_SCHEMA: dict[str, type[pl.Categorical | pl.Float64]] = {
81
+ "market": pl.Categorical,
82
+ "price": pl.Float64,
83
+ }
84
+
85
+ # Ticker book endpoint schema
86
+ TICKER_BOOK_SCHEMA: dict[str, type[pl.Categorical | pl.Float64]] = {
87
+ "market": pl.Categorical,
88
+ "bid": pl.Float64,
89
+ "bidSize": pl.Float64,
90
+ "ask": pl.Float64,
91
+ "askSize": pl.Float64,
92
+ }
93
+
94
+ # Ticker 24h endpoint schema
95
+ TICKER_24H_SCHEMA: dict[str, type[pl.Categorical | pl.Int64 | pl.Float64]] = {
96
+ "market": pl.Categorical,
97
+ "startTimestamp": pl.Int64,
98
+ "timestamp": pl.Int64,
99
+ "open": pl.Float64,
100
+ "openTimestamp": pl.Int64,
101
+ "high": pl.Float64,
102
+ "low": pl.Float64,
103
+ "last": pl.Float64,
104
+ "closeTimestamp": pl.Int64,
105
+ "bid": pl.Float64,
106
+ "bidSize": pl.Float64,
107
+ "ask": pl.Float64,
108
+ "askSize": pl.Float64,
109
+ "volume": pl.Float64,
110
+ "volumeQuote": pl.Float64,
111
+ }
112
+
113
+ # Order book report endpoint schema (MiCA-compliant)
114
+ # Note: This uses the API field names (camelCase) since DataFrames are created
115
+ # directly from the raw API response, not from Pydantic model instances
116
+ # Note: bids and asks are complex nested structures that may need flattening for DataFrame use
117
+ REPORT_BOOK_SCHEMA: dict[str, type | object] = {
118
+ "submissionTimestamp": pl.String, # ISO 8601 timestamp
119
+ "assetCode": pl.Categorical,
120
+ "assetName": pl.String,
121
+ "priceCurrency": pl.Categorical,
122
+ "priceNotation": pl.Categorical, # Always "MONE"
123
+ "quantityCurrency": pl.Categorical,
124
+ "quantityNotation": pl.Categorical, # Always "CRYP"
125
+ "venue": pl.Categorical, # Always "VAVO"
126
+ "tradingSystem": pl.Categorical, # Always "VAVO"
127
+ "publicationTimestamp": pl.String, # ISO 8601 timestamp
128
+ # Note: Nested structures for bids/asks - Polars will handle these as struct arrays
129
+ "bids": pl.Object, # Complex nested structure
130
+ "asks": pl.Object, # Complex nested structure
131
+ }
132
+
133
+ # Default schemas mapping for each endpoint
134
+ DEFAULT_SCHEMAS: dict[str, Mapping[str, object]] = {
135
+ "time": TIME_SCHEMA,
136
+ "markets": MARKETS_SCHEMA,
137
+ "assets": ASSETS_SCHEMA,
138
+ "book": BOOK_SCHEMA,
139
+ "trades": TRADES_SCHEMA,
140
+ "candles": CANDLES_SCHEMA,
141
+ "ticker_price": TICKER_PRICE_SCHEMA,
142
+ "ticker_book": TICKER_BOOK_SCHEMA,
143
+ "ticker_24h": TICKER_24H_SCHEMA,
144
+ "report_book": REPORT_BOOK_SCHEMA,
145
+ }
146
+
147
+ # Combined default schema for when you want all endpoints to use DataFrames
148
+ COMBINED_DEFAULT_SCHEMA: dict[str, Mapping[str, object]] = DEFAULT_SCHEMAS.copy()
149
+ COMBINED_DEFAULT_SCHEMA = DEFAULT_SCHEMAS.copy()
@@ -0,0 +1 @@
1
+ """Transport modules for bitvavo_client."""
@@ -0,0 +1,159 @@
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.key_index: int = -1
36
+ self.api_key: str = ""
37
+ self.api_secret: str = ""
38
+
39
+ def configure_key(self, key: str, secret: str, index: int) -> None:
40
+ """Configure API key for authenticated requests.
41
+
42
+ Args:
43
+ key: API key
44
+ secret: API secret
45
+ index: Key index for rate limiting
46
+ """
47
+ self.api_key = key
48
+ self.api_secret = secret
49
+ self.key_index = index
50
+
51
+ def request(
52
+ self,
53
+ method: str,
54
+ endpoint: str,
55
+ *,
56
+ body: AnyDict | None = None,
57
+ weight: int = 1,
58
+ ) -> Result[Any, BitvavoError | httpx.HTTPError]:
59
+ """Make HTTP request and return raw JSON data as a Result.
60
+
61
+ Args:
62
+ method: HTTP method (GET, POST, PUT, DELETE)
63
+ endpoint: API endpoint path
64
+ body: Request body for POST/PUT requests
65
+ weight: Rate limit weight of the request
66
+
67
+ Returns:
68
+ Result containing raw JSON response or error
69
+
70
+ Raises:
71
+ HTTPError: On transport-level failures
72
+ """
73
+ # Check rate limits
74
+ if not self.rate_limiter.has_budget(self.key_index, weight):
75
+ self.rate_limiter.sleep_until_reset(self.key_index)
76
+
77
+ url = f"{self.settings.rest_url}{endpoint}"
78
+ headers = self._create_auth_headers(method, endpoint, body)
79
+
80
+ try:
81
+ response = self._make_http_request(method, url, headers, body)
82
+ except httpx.HTTPError as exc:
83
+ return Failure(exc)
84
+
85
+ self._update_rate_limits(response)
86
+ # Always return raw data - let the caller handle model conversion
87
+ return decode_response_result(response, model=Any)
88
+
89
+ def _create_auth_headers(self, method: str, endpoint: str, body: AnyDict | None) -> dict[str, str]:
90
+ """Create authentication headers if API key is configured."""
91
+ headers: dict[str, str] = {}
92
+
93
+ if self.api_key:
94
+ timestamp = int(time.time() * 1000) + self.settings.lag_ms
95
+ signature = create_signature(timestamp, method, endpoint, body, self.api_secret)
96
+
97
+ headers.update(
98
+ {
99
+ "bitvavo-access-key": self.api_key,
100
+ "bitvavo-access-signature": signature,
101
+ "bitvavo-access-timestamp": str(timestamp),
102
+ "bitvavo-access-window": str(self.settings.access_window_ms),
103
+ },
104
+ )
105
+ return headers
106
+
107
+ def _make_http_request(
108
+ self,
109
+ method: str,
110
+ url: str,
111
+ headers: dict[str, str],
112
+ body: AnyDict | None,
113
+ ) -> httpx.Response:
114
+ """Make the actual HTTP request."""
115
+ timeout = self.settings.access_window_ms / 1000
116
+
117
+ match method:
118
+ case "GET":
119
+ return httpx.get(url, headers=headers, timeout=timeout)
120
+ case "POST":
121
+ return httpx.post(url, headers=headers, json=body, timeout=timeout)
122
+ case "PUT":
123
+ return httpx.put(url, headers=headers, json=body, timeout=timeout)
124
+ case "DELETE":
125
+ return httpx.delete(url, headers=headers, timeout=timeout)
126
+ case _:
127
+ msg = f"Unsupported HTTP method: {method}"
128
+ raise ValueError(msg)
129
+
130
+ def _update_rate_limits(self, response: httpx.Response) -> None:
131
+ """Update rate limits based on response."""
132
+ try:
133
+ json_data = response.json()
134
+ except ValueError:
135
+ json_data = {}
136
+
137
+ if isinstance(json_data, dict) and "error" in json_data:
138
+ if self._is_rate_limit_error(response, json_data):
139
+ self.rate_limiter.update_from_error(self.key_index, json_data)
140
+ else:
141
+ self.rate_limiter.update_from_headers(self.key_index, dict(response.headers))
142
+ else:
143
+ self.rate_limiter.update_from_headers(self.key_index, dict(response.headers))
144
+
145
+ def _is_rate_limit_error(self, response: httpx.Response, json_data: dict[str, Any]) -> bool:
146
+ """Check if response indicates a rate limit error."""
147
+ status = getattr(response, "status_code", None)
148
+ if status == httpx.codes.TOO_MANY_REQUESTS:
149
+ return True
150
+
151
+ err = json_data.get("error")
152
+ if isinstance(err, dict):
153
+ code = str(err.get("code", "")).lower()
154
+ message = str(err.get("message", "")).lower()
155
+ else:
156
+ code = ""
157
+ message = str(err).lower()
158
+
159
+ return any(k in code or k in message for k in ("rate", "limit", "too_many"))
@@ -0,0 +1 @@
1
+ """WebSocket modules for bitvavo_client."""
@@ -1,10 +0,0 @@
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,,