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.
- {bitvavo_api_upgraded-4.1.0.dist-info → bitvavo_api_upgraded-4.1.1.dist-info}/METADATA +1 -1
- bitvavo_api_upgraded-4.1.1.dist-info/RECORD +38 -0
- bitvavo_client/__init__.py +9 -0
- bitvavo_client/adapters/__init__.py +1 -0
- bitvavo_client/adapters/returns_adapter.py +363 -0
- bitvavo_client/auth/__init__.py +1 -0
- bitvavo_client/auth/rate_limit.py +104 -0
- bitvavo_client/auth/signing.py +33 -0
- bitvavo_client/core/__init__.py +1 -0
- bitvavo_client/core/errors.py +17 -0
- bitvavo_client/core/model_preferences.py +33 -0
- bitvavo_client/core/private_models.py +886 -0
- bitvavo_client/core/public_models.py +1087 -0
- bitvavo_client/core/settings.py +52 -0
- bitvavo_client/core/types.py +11 -0
- bitvavo_client/core/validation_helpers.py +90 -0
- bitvavo_client/df/__init__.py +1 -0
- bitvavo_client/df/convert.py +86 -0
- bitvavo_client/endpoints/__init__.py +1 -0
- bitvavo_client/endpoints/common.py +88 -0
- bitvavo_client/endpoints/private.py +1090 -0
- bitvavo_client/endpoints/public.py +658 -0
- bitvavo_client/facade.py +66 -0
- bitvavo_client/py.typed +0 -0
- bitvavo_client/schemas/__init__.py +50 -0
- bitvavo_client/schemas/private_schemas.py +191 -0
- bitvavo_client/schemas/public_schemas.py +149 -0
- bitvavo_client/transport/__init__.py +1 -0
- bitvavo_client/transport/http.py +159 -0
- bitvavo_client/ws/__init__.py +1 -0
- bitvavo_api_upgraded-4.1.0.dist-info/RECORD +0 -10
- {bitvavo_api_upgraded-4.1.0.dist-info → bitvavo_api_upgraded-4.1.1.dist-info}/WHEEL +0 -0
bitvavo_client/facade.py
ADDED
@@ -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)
|
bitvavo_client/py.typed
ADDED
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,,
|
File without changes
|