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.
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/PKG-INFO +1 -1
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/pyproject.toml +2 -2
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/rate_limit.py +29 -1
- bitvavo_api_upgraded-4.2.1/src/bitvavo_client/endpoints/base.py +305 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/private.py +28 -365
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/public.py +23 -220
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/facade.py +60 -9
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/transport/http.py +12 -1
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/README.md +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/bitvavo.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/dataframe_utils.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/helper_funcs.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/py.typed +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/settings.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_api_upgraded/type_aliases.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/adapters/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/adapters/returns_adapter.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/signing.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/errors.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/model_preferences.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/private_models.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/public_models.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/settings.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/types.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/core/validation_helpers.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/df/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/df/convert.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/endpoints/common.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/py.typed +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/schemas/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/schemas/private_schemas.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/schemas/public_schemas.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/transport/__init__.py +0 -0
- {bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/ws/__init__.py +0 -0
@@ -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
|
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
|
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}"
|
{bitvavo_api_upgraded-4.1.2 → bitvavo_api_upgraded-4.2.1}/src/bitvavo_client/auth/rate_limit.py
RENAMED
@@ -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)
|