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,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