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
@@ -0,0 +1,52 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from pydantic import Field, model_validator
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
7
|
+
|
8
|
+
|
9
|
+
class BitvavoSettings(BaseSettings):
|
10
|
+
"""
|
11
|
+
Core Bitvavo API settings using Pydantic v2.
|
12
|
+
|
13
|
+
Provides both snake_case (modern) and UPPERCASE (original library compatibility) field access.
|
14
|
+
Enhanced with multi-API key support and comprehensive validation.
|
15
|
+
"""
|
16
|
+
|
17
|
+
model_config = SettingsConfigDict(
|
18
|
+
env_file=Path.cwd() / ".env",
|
19
|
+
env_file_encoding="utf-8",
|
20
|
+
env_prefix="BITVAVO_",
|
21
|
+
extra="ignore",
|
22
|
+
)
|
23
|
+
|
24
|
+
# Core API endpoints
|
25
|
+
rest_url: str = Field(default="https://api.bitvavo.com/v2", description="Bitvavo REST API base URL")
|
26
|
+
ws_url: str = Field(default="wss://ws.bitvavo.com/v2/", description="Bitvavo WebSocket API URL")
|
27
|
+
|
28
|
+
# Timing and rate limiting
|
29
|
+
access_window_ms: int = Field(default=10_000, description="API access window in milliseconds")
|
30
|
+
|
31
|
+
# Client behavior
|
32
|
+
prefer_keyless: bool = Field(default=True, description="Prefer keyless requests when possible")
|
33
|
+
default_rate_limit: int = Field(default=1_000, description="Default rate limit for new API keys")
|
34
|
+
rate_limit_buffer: int = Field(default=0, description="Rate limit buffer to avoid hitting limits")
|
35
|
+
lag_ms: int = Field(default=0, description="Artificial lag to add to requests in milliseconds")
|
36
|
+
debugging: bool = Field(default=False, description="Enable debug logging")
|
37
|
+
|
38
|
+
# API key configuration
|
39
|
+
api_key: str = Field(default="", alias="BITVAVO_APIKEY", description="Primary API key")
|
40
|
+
api_secret: str = Field(default="", alias="BITVAVO_APISECRET", description="Primary API secret")
|
41
|
+
|
42
|
+
# Multiple API keys support
|
43
|
+
api_keys: list[dict[str, str]] = Field(
|
44
|
+
default_factory=list, description="List of API key/secret pairs for multi-key support"
|
45
|
+
)
|
46
|
+
|
47
|
+
@model_validator(mode="after")
|
48
|
+
def process_api_keys(self) -> BitvavoSettings:
|
49
|
+
"""Process API keys from single key/secret into multi-key list."""
|
50
|
+
if self.api_key and self.api_secret and not self.api_keys:
|
51
|
+
object.__setattr__(self, "api_keys", [{"key": self.api_key, "secret": self.api_secret}])
|
52
|
+
return self
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""Type definitions for bitvavo_client."""
|
2
|
+
|
3
|
+
from typing import Any # pragma: no cover
|
4
|
+
|
5
|
+
# Type aliases for better readability
|
6
|
+
Result = dict[str, Any] | list[dict[str, Any]] # pragma: no cover
|
7
|
+
ErrorDict = dict[str, Any] # pragma: no cover
|
8
|
+
AnyDict = dict[str, Any] # pragma: no cover
|
9
|
+
StrDict = dict[str, str] # pragma: no cover
|
10
|
+
IntDict = dict[str, int] # pragma: no cover
|
11
|
+
StrIntDict = dict[str, str | int] # pragma: no cover
|
@@ -0,0 +1,90 @@
|
|
1
|
+
"""Simple validation utilities for better error reporting."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import json
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from pydantic import ValidationError
|
9
|
+
|
10
|
+
|
11
|
+
def format_validation_error(error: ValidationError, input_data: Any = None) -> str: # noqa: C901 (too complex)
|
12
|
+
"""
|
13
|
+
Format a Pydantic ValidationError into a human-readable message.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
error: The ValidationError to format
|
17
|
+
input_data: Optional input data that caused the error
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Formatted error message with context
|
21
|
+
"""
|
22
|
+
# Get model name from the error title or use a default
|
23
|
+
model_name = getattr(error, "title", "Model")
|
24
|
+
error_lines = [f"🚫 Validation failed for {model_name}:"]
|
25
|
+
|
26
|
+
for err in error.errors():
|
27
|
+
location = " -> ".join(str(loc) for loc in err["loc"])
|
28
|
+
error_type = err["type"]
|
29
|
+
message = err["msg"]
|
30
|
+
|
31
|
+
# Add context about the input value if available
|
32
|
+
input_value = err.get("input", "N/A")
|
33
|
+
|
34
|
+
error_lines.append(f" 📍 Field '{location}': {message}")
|
35
|
+
error_lines.append(f" Type: {error_type}")
|
36
|
+
error_lines.append(f" Input: {input_value!r}")
|
37
|
+
|
38
|
+
# Add suggestions for common errors
|
39
|
+
if error_type == "string_type":
|
40
|
+
error_lines.append(" 💡 Expected a string value")
|
41
|
+
elif error_type == "value_error":
|
42
|
+
error_lines.append(" 💡 Check the value format and constraints")
|
43
|
+
elif error_type == "missing":
|
44
|
+
error_lines.append(" 💡 This field is required")
|
45
|
+
elif "decimal" in message.lower() or "numeric" in message.lower():
|
46
|
+
error_lines.append(" 💡 Use a decimal string like '123.45'")
|
47
|
+
elif "side" in location.lower():
|
48
|
+
error_lines.append(" 💡 Order side must be 'BUY' or 'SELL'")
|
49
|
+
|
50
|
+
error_lines.append("") # Empty line between errors
|
51
|
+
|
52
|
+
# Add the original input data if provided (truncated for readability)
|
53
|
+
if input_data is not None:
|
54
|
+
error_lines.append("📋 Original input data:")
|
55
|
+
try:
|
56
|
+
data_str = json.dumps(input_data, indent=2, default=str)
|
57
|
+
# Truncate if too long
|
58
|
+
if len(data_str) > 500: # noqa: PLR2004 (magic var)
|
59
|
+
data_str = data_str[:500] + "...\n (truncated)"
|
60
|
+
error_lines.append(data_str)
|
61
|
+
except (TypeError, ValueError):
|
62
|
+
data_repr = repr(input_data)
|
63
|
+
if len(data_repr) > 200: # noqa: PLR2004 (magic var)
|
64
|
+
data_repr = data_repr[:200] + "...(truncated)"
|
65
|
+
error_lines.append(data_repr)
|
66
|
+
|
67
|
+
return "\n".join(error_lines)
|
68
|
+
|
69
|
+
|
70
|
+
def safe_validate(model_class: Any, data: Any, operation_name: str = "validation") -> Any:
|
71
|
+
"""
|
72
|
+
Safely validate data with enhanced error reporting.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
model_class: The Pydantic model class to validate against
|
76
|
+
data: The data to validate
|
77
|
+
operation_name: Name of the operation for error context
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Validated model instance
|
81
|
+
|
82
|
+
Raises:
|
83
|
+
ValueError: With enhanced error message if validation fails
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
return model_class.model_validate(data)
|
87
|
+
except ValidationError as e:
|
88
|
+
enhanced_error = format_validation_error(e, data)
|
89
|
+
msg = f"❌ {operation_name.title()} failed:\n{enhanced_error}"
|
90
|
+
raise ValueError(msg) from e
|
@@ -0,0 +1 @@
|
|
1
|
+
"""DataFrame modules for bitvavo_client."""
|
@@ -0,0 +1,86 @@
|
|
1
|
+
"""DataFrame conversion utilities using Narwhals for multi-library support."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
|
8
|
+
def is_narwhals_available() -> bool:
|
9
|
+
"""Check if narwhals is available."""
|
10
|
+
try:
|
11
|
+
import narwhals # noqa: F401, PLC0415
|
12
|
+
except ImportError:
|
13
|
+
return False
|
14
|
+
else:
|
15
|
+
return True
|
16
|
+
|
17
|
+
|
18
|
+
def convert_to_dataframe(data: Any, output_format: str = "default") -> Any:
|
19
|
+
"""Convert API response data to DataFrame using narwhals.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
data: Response data from Bitvavo API
|
23
|
+
output_format: Target DataFrame library ('pandas', 'polars', etc.)
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
DataFrame in the requested format or original data if narwhals unavailable
|
27
|
+
|
28
|
+
Raises:
|
29
|
+
ImportError: If narwhals or target library is not available
|
30
|
+
"""
|
31
|
+
if not is_narwhals_available() or output_format == "default":
|
32
|
+
return data
|
33
|
+
|
34
|
+
# Convert dict to list for DataFrame conversion
|
35
|
+
if isinstance(data, dict):
|
36
|
+
data = [data]
|
37
|
+
|
38
|
+
try:
|
39
|
+
# Try to detect and use the requested library
|
40
|
+
if output_format == "pandas":
|
41
|
+
import pandas as pd # noqa: PLC0415
|
42
|
+
|
43
|
+
return pd.DataFrame(data)
|
44
|
+
if output_format == "polars":
|
45
|
+
import polars as pl # noqa: PLC0415
|
46
|
+
|
47
|
+
return pl.DataFrame(data)
|
48
|
+
# For other formats, try pandas as fallback
|
49
|
+
import pandas as pd # noqa: PLC0415
|
50
|
+
|
51
|
+
return pd.DataFrame(data)
|
52
|
+
|
53
|
+
except ImportError as e:
|
54
|
+
# If the target library is not available, return original data
|
55
|
+
msg = f"Library {output_format} not available: {e}"
|
56
|
+
raise ImportError(msg) from e
|
57
|
+
|
58
|
+
|
59
|
+
def convert_candles_to_dataframe(data: Any, output_format: str = "default") -> Any:
|
60
|
+
"""Convert candlestick data to DataFrame with proper column names.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
data: Candlestick data from Bitvavo API
|
64
|
+
output_format: Target DataFrame library
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
DataFrame with columns: timestamp, open, high, low, close, volume
|
68
|
+
"""
|
69
|
+
if not is_narwhals_available() or output_format == "default":
|
70
|
+
return data
|
71
|
+
|
72
|
+
# Convert to dict format with proper column names
|
73
|
+
candle_dicts = [
|
74
|
+
{
|
75
|
+
"timestamp": candle[0],
|
76
|
+
"open": candle[1],
|
77
|
+
"high": candle[2],
|
78
|
+
"low": candle[3],
|
79
|
+
"close": candle[4],
|
80
|
+
"volume": candle[5],
|
81
|
+
}
|
82
|
+
for candle in data
|
83
|
+
if len(candle) >= 6 # noqa: PLR2004
|
84
|
+
]
|
85
|
+
|
86
|
+
return convert_to_dataframe(candle_dicts, output_format)
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Endpoint modules for bitvavo_client."""
|
@@ -0,0 +1,88 @@
|
|
1
|
+
"""Common utilities for endpoints."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
7
|
+
if TYPE_CHECKING: # pragma: no cover
|
8
|
+
import datetime as dt
|
9
|
+
from collections.abc import Callable
|
10
|
+
|
11
|
+
from bitvavo_client.core.types import AnyDict
|
12
|
+
|
13
|
+
|
14
|
+
def default(value: AnyDict | None, fallback: AnyDict) -> AnyDict:
|
15
|
+
"""Return value if not None, otherwise fallback.
|
16
|
+
|
17
|
+
Note that this is close, but not actually equal to:
|
18
|
+
`return value or fallback`
|
19
|
+
|
20
|
+
Args:
|
21
|
+
value: Value to check
|
22
|
+
fallback: Fallback value if value is None
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
The value or fallback
|
26
|
+
"""
|
27
|
+
return value if value is not None else fallback
|
28
|
+
|
29
|
+
|
30
|
+
def create_postfix(options: AnyDict | None) -> str:
|
31
|
+
"""Generate a URL postfix based on the options dict.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
options: Dictionary of query parameters
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Query string with '?' prefix if options exist, empty string otherwise
|
38
|
+
"""
|
39
|
+
options = default(options, {})
|
40
|
+
params = [f"{key}={options[key]}" for key in options]
|
41
|
+
postfix = "&".join(params)
|
42
|
+
return f"?{postfix}" if len(options) > 0 else postfix
|
43
|
+
|
44
|
+
|
45
|
+
def epoch_millis(dt_obj: dt.datetime) -> int:
|
46
|
+
"""Convert datetime to milliseconds since epoch.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
dt_obj: Datetime object to convert
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Milliseconds since Unix epoch
|
53
|
+
"""
|
54
|
+
return int(dt_obj.timestamp() * 1000)
|
55
|
+
|
56
|
+
|
57
|
+
def asks_compare(a: float, b: float) -> bool:
|
58
|
+
return a < b
|
59
|
+
|
60
|
+
|
61
|
+
def bids_compare(a: float, b: float) -> bool:
|
62
|
+
return a > b
|
63
|
+
|
64
|
+
|
65
|
+
def sort_and_insert(
|
66
|
+
asks_or_bids: list[list[str]],
|
67
|
+
update: list[list[str]],
|
68
|
+
compareFunc: Callable[[float, float], bool],
|
69
|
+
) -> list[list[str]] | dict[str, Any]:
|
70
|
+
for updateEntry in update:
|
71
|
+
entrySet: bool = False
|
72
|
+
for j in range(len(asks_or_bids)):
|
73
|
+
bookItem = asks_or_bids[j]
|
74
|
+
if compareFunc(float(updateEntry[0]), float(bookItem[0])):
|
75
|
+
asks_or_bids.insert(j, updateEntry)
|
76
|
+
entrySet = True
|
77
|
+
break
|
78
|
+
if float(updateEntry[0]) == float(bookItem[0]):
|
79
|
+
if float(updateEntry[1]) > 0.0:
|
80
|
+
asks_or_bids[j] = updateEntry
|
81
|
+
entrySet = True
|
82
|
+
break
|
83
|
+
asks_or_bids.pop(j)
|
84
|
+
entrySet = True
|
85
|
+
break
|
86
|
+
if not entrySet:
|
87
|
+
asks_or_bids.append(updateEntry)
|
88
|
+
return asks_or_bids
|