bitvavo-api-upgraded 4.1.2__tar.gz → 4.2.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 (39) hide show
  1. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/PKG-INFO +1 -1
  2. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/pyproject.toml +2 -2
  3. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/rate_limit.py +29 -1
  4. bitvavo_api_upgraded-4.2.1/src/bitvavo_client/endpoints/base.py +305 -0
  5. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/private.py +28 -365
  6. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/public.py +23 -220
  7. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/facade.py +60 -9
  8. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/transport/http.py +12 -1
  9. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/README.md +0 -0
  10. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/__init__.py +0 -0
  11. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/bitvavo.py +0 -0
  12. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
  13. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
  14. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/py.typed +0 -0
  15. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/settings.py +0 -0
  16. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
  17. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/__init__.py +0 -0
  18. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/adapters/__init__.py +0 -0
  19. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/adapters/returns_adapter.py +0 -0
  20. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/__init__.py +0 -0
  21. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/signing.py +0 -0
  22. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/__init__.py +0 -0
  23. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/errors.py +0 -0
  24. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/model_preferences.py +0 -0
  25. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/private_models.py +0 -0
  26. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/public_models.py +0 -0
  27. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/settings.py +0 -0
  28. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/types.py +0 -0
  29. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/validation_helpers.py +0 -0
  30. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/df/__init__.py +0 -0
  31. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/df/convert.py +0 -0
  32. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/__init__.py +0 -0
  33. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/common.py +0 -0
  34. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/py.typed +0 -0
  35. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/schemas/__init__.py +0 -0
  36. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/schemas/private_schemas.py +0 -0
  37. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/schemas/public_schemas.py +0 -0
  38. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/transport/__init__.py +0 -0
  39. {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/ws/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bitvavo-api-upgraded
3
- Version: 4.1.2
3
+ Version: 4.2.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.2"
9
+ version = "4.2.1"
10
10
  description = "A unit-tested fork of the Bitvavo API"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -108,7 +108,7 @@ dev-dependencies = [
108
108
  ]
109
109
 
110
110
  [tool.bumpversion]
111
- current_version = "4.1.2"
111
+ current_version = "4.2.1"
112
112
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
113
113
  serialize = ["{major}.{minor}.{patch}"]
114
114
  search = "{current_version}"
@@ -3,6 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import time
6
+ from typing import Protocol
7
+
8
+
9
+ class RateLimitStrategy(Protocol):
10
+ """Protocol for custom rate limit handling strategies."""
11
+
12
+ def __call__(self, manager: RateLimitManager, idx: int, weight: int) -> None: ...
13
+
14
+
15
+ class DefaultRateLimitStrategy(RateLimitStrategy):
16
+ """Default RateLimitStrategy implementation that sleeps until the key's rate limit resets."""
17
+
18
+ def __call__(self, manager: RateLimitManager, idx: int, _: int) -> None:
19
+ manager.sleep_until_reset(idx)
6
20
 
7
21
 
8
22
  class RateLimitManager:
@@ -12,16 +26,20 @@ class RateLimitManager:
12
26
  for keyless requests.
13
27
  """
14
28
 
15
- def __init__(self, default_remaining: int, buffer: int) -> None:
29
+ def __init__(self, default_remaining: int, buffer: int, strategy: RateLimitStrategy | None = None) -> None:
16
30
  """Initialize rate limit manager.
17
31
 
18
32
  Args:
19
33
  default_remaining: Default rate limit amount
20
34
  buffer: Buffer to keep before hitting limit
35
+ strategy: Optional strategy callback when rate limit exceeded
21
36
  """
37
+ self.default_remaining: int = default_remaining
22
38
  self.state: dict[int, dict[str, int]] = {-1: {"remaining": default_remaining, "resetAt": 0}}
23
39
  self.buffer: int = buffer
24
40
 
41
+ self._strategy: RateLimitStrategy = strategy or DefaultRateLimitStrategy()
42
+
25
43
  def ensure_key(self, idx: int) -> None:
26
44
  """Ensure a key index exists in the state."""
27
45
  if idx not in self.state:
@@ -79,6 +97,16 @@ class RateLimitManager:
79
97
  ms_left = max(0, self.state[idx]["resetAt"] - now)
80
98
  time.sleep(ms_left / 1000 + 1)
81
99
 
100
+ def handle_limit(self, idx: int, weight: int) -> None:
101
+ """Invoke the configured strategy when rate limit is exceeded."""
102
+ self._strategy(self, idx, weight)
103
+
104
+ def reset_key(self, idx: int) -> None:
105
+ """Reset the remaining budget and reset time for a key index."""
106
+ self.ensure_key(idx)
107
+ self.state[idx]["remaining"] = self.default_remaining
108
+ self.state[idx]["resetAt"] = 0
109
+
82
110
  def get_remaining(self, idx: int) -> int:
83
111
  """Get remaining rate limit for key index.
84
112
 
@@ -0,0 +1,305 @@
1
+ """Shared endpoint utilities for model resolution and DataFrame handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from typing import TYPE_CHECKING, Any, TypeVar
7
+
8
+ from returns.result import Failure, Result, Success
9
+
10
+ from bitvavo_client.adapters.returns_adapter import BitvavoError
11
+ from bitvavo_client.core.model_preferences import ModelPreference
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Mapping
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ # DataFrames preference to library mapping
20
+ _DATAFRAME_LIBRARY_MAP = {
21
+ ModelPreference.POLARS: ("polars", "pl.DataFrame"),
22
+ ModelPreference.PANDAS: ("pandas", "pd.DataFrame"),
23
+ ModelPreference.PYARROW: ("pyarrow", "pa.Table"),
24
+ ModelPreference.DASK: ("dask", "dd.DataFrame"),
25
+ ModelPreference.MODIN: ("modin", "mpd.DataFrame"),
26
+ ModelPreference.CUDF: ("cudf", "cudf.DataFrame"),
27
+ ModelPreference.IBIS: ("ibis", "ibis.Table"),
28
+ }
29
+
30
+
31
+ def _extract_dataframe_data(data: Any, *, items_key: str | None = None) -> list[dict] | dict:
32
+ """Extract the meaningful data for DataFrame creation from API responses."""
33
+ if items_key and isinstance(data, dict) and items_key in data:
34
+ items = data[items_key]
35
+ if not isinstance(items, list):
36
+ msg = f"Expected {items_key} to be a list, got {type(items)}"
37
+ raise ValueError(msg)
38
+ return items
39
+ if isinstance(data, list):
40
+ return data
41
+ if isinstance(data, dict):
42
+ return [data]
43
+ msg = f"Unexpected data type for DataFrame creation: {type(data)}"
44
+ raise ValueError(msg)
45
+
46
+
47
+ def _get_dataframe_constructor(preference: ModelPreference) -> tuple[Any, str]: # noqa: PLR0911 (Too many return statements)
48
+ """Get the appropriate dataframe constructor and library name based on preference."""
49
+ if preference not in _DATAFRAME_LIBRARY_MAP:
50
+ msg = f"Unsupported dataframe preference: {preference}"
51
+ raise ValueError(msg)
52
+
53
+ library_name, _ = _DATAFRAME_LIBRARY_MAP[preference]
54
+
55
+ try:
56
+ if preference == ModelPreference.POLARS:
57
+ import polars as pl # noqa: PLC0415
58
+
59
+ return pl.DataFrame, library_name
60
+ if preference == ModelPreference.PANDAS:
61
+ import pandas as pd # noqa: PLC0415
62
+
63
+ return pd.DataFrame, library_name
64
+ if preference == ModelPreference.PYARROW:
65
+ import pyarrow as pa # noqa: PLC0415
66
+
67
+ return pa.Table.from_pylist, library_name
68
+ if preference == ModelPreference.DASK:
69
+ import dask.dataframe as dd # noqa: PLC0415
70
+ import pandas as pd # noqa: PLC0415
71
+
72
+ return lambda data, **_: dd.from_pandas(pd.DataFrame(data), npartitions=1), library_name
73
+ if preference == ModelPreference.MODIN:
74
+ import modin.pandas as mpd # noqa: PLC0415
75
+
76
+ return mpd.DataFrame, library_name
77
+ if preference == ModelPreference.CUDF:
78
+ import cudf # noqa: PLC0415
79
+
80
+ return cudf.DataFrame, library_name
81
+ import ibis # noqa: PLC0415
82
+
83
+ return lambda data, **_: ibis.memtable(data), library_name
84
+ except ImportError as e: # pragma: no cover - import failure is environment dependent
85
+ msg = f"{library_name} is not installed. Install with appropriate package manager."
86
+ raise ImportError(msg) from e
87
+
88
+
89
+ def _create_dataframe_with_constructor( # noqa: C901 (is too complex)
90
+ constructor: Any, library_name: str, df_data: list | dict, empty_schema: dict | None
91
+ ) -> Any:
92
+ """Create DataFrame with data using the appropriate constructor."""
93
+
94
+ # Helper function to check if data is array-like and schema exists
95
+ def _is_array_data_with_schema() -> bool:
96
+ return bool(empty_schema and isinstance(df_data, list) and df_data and isinstance(df_data[0], (list, tuple)))
97
+
98
+ if library_name == "polars":
99
+ # Handle special case for array data (like candles) with schema
100
+ if _is_array_data_with_schema() and empty_schema:
101
+ # Transform array data into named columns based on schema
102
+ column_names = list(empty_schema.keys())
103
+ if len(df_data[0]) == len(column_names): # type: ignore[index]
104
+ # Create DataFrame with explicit column names
105
+ import polars as pl # noqa: PLC0415
106
+
107
+ df = pl.DataFrame(df_data, schema=column_names, orient="row")
108
+ df = _apply_polars_schema(df, empty_schema)
109
+ return df
110
+
111
+ df = constructor(df_data, strict=False)
112
+ if empty_schema:
113
+ df = _apply_polars_schema(df, empty_schema)
114
+ return df
115
+
116
+ if library_name in ("pandas", "modin", "cudf"):
117
+ # Handle special case for array data (like candles) with schema
118
+ if _is_array_data_with_schema() and empty_schema:
119
+ # Transform array data into named columns based on schema
120
+ column_names = list(empty_schema.keys())
121
+ if len(df_data[0]) == len(column_names): # type: ignore[index]
122
+ # Create DataFrame with explicit column names
123
+ df = constructor(df_data, columns=column_names)
124
+ if empty_schema:
125
+ df = _apply_pandas_like_schema(df, empty_schema)
126
+ return df
127
+
128
+ df = constructor(df_data)
129
+ if empty_schema:
130
+ df = _apply_pandas_like_schema(df, empty_schema)
131
+ return df
132
+
133
+ if library_name in ("pyarrow", "dask"):
134
+ return constructor(df_data)
135
+
136
+ return constructor(df_data)
137
+
138
+
139
+ def _create_empty_dataframe(constructor: Any, library_name: str, empty_schema: dict | None) -> Any:
140
+ """Create empty DataFrame using the appropriate constructor."""
141
+ if empty_schema is None:
142
+ empty_schema = {"id": str} if library_name != "polars" else {"id": "pl.String"}
143
+
144
+ if library_name == "polars":
145
+ import polars as pl # noqa: PLC0415
146
+
147
+ schema = {k: pl.String if v == "pl.String" else v for k, v in empty_schema.items()}
148
+ return constructor([], schema=schema)
149
+ if library_name in ("pandas", "modin", "cudf", "dask") or library_name == "pyarrow":
150
+ return constructor([])
151
+ return constructor([dict.fromkeys(empty_schema.keys())])
152
+
153
+
154
+ def _apply_polars_schema(df: Any, schema: dict) -> Any:
155
+ """Apply schema to polars DataFrame."""
156
+ import polars as pl # noqa: PLC0415
157
+
158
+ for col, expected_dtype in schema.items():
159
+ if col in df.columns:
160
+ with contextlib.suppress(Exception):
161
+ df = df.with_columns(pl.col(col).cast(expected_dtype))
162
+ return df
163
+
164
+
165
+ def _apply_pandas_like_schema(df: Any, schema: dict) -> Any:
166
+ """Apply schema to pandas-like DataFrame."""
167
+ pandas_schema = {}
168
+ for col, dtype in schema.items():
169
+ if col in df.columns:
170
+ if "String" in str(dtype) or "Utf8" in str(dtype):
171
+ pandas_schema[col] = "string"
172
+ elif "Int" in str(dtype):
173
+ pandas_schema[col] = "int64"
174
+ elif "Float" in str(dtype):
175
+ pandas_schema[col] = "float64"
176
+
177
+ if pandas_schema and hasattr(df, "astype"):
178
+ with contextlib.suppress(Exception):
179
+ df = df.astype(pandas_schema)
180
+ return df
181
+
182
+
183
+ def _create_dataframe_from_data(
184
+ data: Any, preference: ModelPreference, *, items_key: str | None = None, empty_schema: dict[str, Any] | None = None
185
+ ) -> Result[Any, BitvavoError]:
186
+ """Create a DataFrame from API response data using the specified preference."""
187
+ try:
188
+ df_data = _extract_dataframe_data(data, items_key=items_key)
189
+ constructor, library_name = _get_dataframe_constructor(preference)
190
+
191
+ if df_data:
192
+ df = _create_dataframe_with_constructor(constructor, library_name, df_data, empty_schema)
193
+ return Success(df) # type: ignore[return-value]
194
+
195
+ df = _create_empty_dataframe(constructor, library_name, empty_schema)
196
+ return Success(df) # type: ignore[return-value]
197
+
198
+ except (ValueError, TypeError, ImportError) as exc:
199
+ error = BitvavoError(
200
+ http_status=500,
201
+ error_code=-1,
202
+ reason="DataFrame creation failed",
203
+ message=f"Failed to create DataFrame from API response: {exc}",
204
+ raw={"data_type": type(data).__name__, "data_sample": str(data)[:200]},
205
+ )
206
+ return Failure(error)
207
+
208
+
209
+ class BaseAPI:
210
+ """Base class for API endpoint handlers providing model conversion utilities."""
211
+
212
+ _endpoint_models: Mapping[str, Any] = {}
213
+ _default_schemas: Mapping[str, object] = {}
214
+
215
+ def __init__(
216
+ self,
217
+ http_client: Any,
218
+ *,
219
+ preferred_model: ModelPreference | str | None = None,
220
+ default_schema: Mapping[str, object] | None = None,
221
+ ) -> None:
222
+ self.http = http_client
223
+
224
+ if preferred_model is None:
225
+ self.preferred_model = None
226
+ elif isinstance(preferred_model, ModelPreference):
227
+ self.preferred_model = preferred_model
228
+ elif isinstance(preferred_model, str):
229
+ try:
230
+ self.preferred_model = ModelPreference(preferred_model)
231
+ except ValueError:
232
+ self.preferred_model = preferred_model
233
+ else:
234
+ self.preferred_model = preferred_model
235
+
236
+ self.default_schema = default_schema
237
+
238
+ def _get_effective_model(
239
+ self,
240
+ endpoint_type: str,
241
+ model: type[T] | Any | None,
242
+ schema: Mapping[str, object] | None,
243
+ ) -> tuple[type[T] | Any | None, Mapping[str, object] | None]:
244
+ if model is not None:
245
+ return model, schema
246
+
247
+ if self.preferred_model is None or self.preferred_model == ModelPreference.RAW:
248
+ return Any, schema
249
+
250
+ if isinstance(self.preferred_model, ModelPreference) and self.preferred_model in _DATAFRAME_LIBRARY_MAP:
251
+ effective_schema = schema or self.default_schema or self._default_schemas.get(endpoint_type)
252
+ if effective_schema is not None and not isinstance(effective_schema, dict):
253
+ effective_schema = dict(effective_schema)
254
+ return self.preferred_model, effective_schema
255
+
256
+ if self.preferred_model == ModelPreference.PYDANTIC:
257
+ model_cls = self._endpoint_models.get(endpoint_type, dict)
258
+ return model_cls, schema
259
+
260
+ return None, schema
261
+
262
+ def _convert_raw_result(
263
+ self,
264
+ raw_result: Result[Any, BitvavoError | Any],
265
+ endpoint_type: str,
266
+ model: type[T] | Any | None,
267
+ schema: Mapping[str, object] | None,
268
+ *,
269
+ items_key: str | None = None,
270
+ ) -> Result[Any, BitvavoError | Any]:
271
+ if isinstance(raw_result, Failure):
272
+ return raw_result
273
+
274
+ effective_model, effective_schema = self._get_effective_model(endpoint_type, model, schema)
275
+
276
+ if effective_model is Any or effective_model is None:
277
+ return raw_result
278
+
279
+ raw_data = raw_result.unwrap()
280
+
281
+ if isinstance(effective_model, ModelPreference) and effective_model in _DATAFRAME_LIBRARY_MAP:
282
+ return _create_dataframe_from_data(
283
+ raw_data,
284
+ effective_model,
285
+ items_key=items_key,
286
+ empty_schema=effective_schema, # type: ignore[arg-type]
287
+ )
288
+
289
+ try:
290
+ if hasattr(effective_model, "model_validate"):
291
+ parsed = effective_model.model_validate(raw_data) # type: ignore[misc]
292
+ elif effective_schema is None:
293
+ parsed = effective_model(raw_data) # type: ignore[misc]
294
+ else:
295
+ parsed = effective_model(raw_data, schema=effective_schema) # type: ignore[misc]
296
+ return Success(parsed)
297
+ except (ValueError, TypeError, AttributeError) as exc:
298
+ error = BitvavoError(
299
+ http_status=500,
300
+ error_code=-1,
301
+ reason="Model conversion failed",
302
+ message=str(exc),
303
+ raw=raw_data if isinstance(raw_data, dict) else {"raw": raw_data},
304
+ )
305
+ return Failure(error)