vendorval-sdk 0.2.0__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.
- vendorval_sdk/__init__.py +44 -0
- vendorval_sdk/_async_client.py +84 -0
- vendorval_sdk/_client.py +87 -0
- vendorval_sdk/_errors.py +205 -0
- vendorval_sdk/_idempotency.py +10 -0
- vendorval_sdk/_models.py +59 -0
- vendorval_sdk/_pagination.py +34 -0
- vendorval_sdk/_request.py +309 -0
- vendorval_sdk/_retry.py +50 -0
- vendorval_sdk/_version.py +2 -0
- vendorval_sdk/_webhooks.py +85 -0
- vendorval_sdk/py.typed +0 -0
- vendorval_sdk/resources/__init__.py +0 -0
- vendorval_sdk/resources/_entities.py +134 -0
- vendorval_sdk/resources/_meta.py +88 -0
- vendorval_sdk/resources/_monitors.py +97 -0
- vendorval_sdk/resources/_simple.py +77 -0
- vendorval_sdk/resources/_verifications.py +311 -0
- vendorval_sdk/types.py +199 -0
- vendorval_sdk-0.2.0.dist-info/METADATA +114 -0
- vendorval_sdk-0.2.0.dist-info/RECORD +23 -0
- vendorval_sdk-0.2.0.dist-info/WHEEL +4 -0
- vendorval_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Official Python SDK for the VendorVal API."""
|
|
2
|
+
|
|
3
|
+
from ._async_client import AsyncVendorval
|
|
4
|
+
from ._client import Vendorval
|
|
5
|
+
from ._errors import (
|
|
6
|
+
APIConnectionError,
|
|
7
|
+
APIError,
|
|
8
|
+
APITimeoutError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
ConflictError,
|
|
11
|
+
CountryError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
PermissionError,
|
|
14
|
+
ProviderError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
VendorvalError,
|
|
18
|
+
)
|
|
19
|
+
from ._idempotency import generate_idempotency_key
|
|
20
|
+
from ._pagination import Page
|
|
21
|
+
from ._version import API_VERSION, VERSION
|
|
22
|
+
from ._webhooks import construct_event
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"API_VERSION",
|
|
26
|
+
"APIConnectionError",
|
|
27
|
+
"APIError",
|
|
28
|
+
"APITimeoutError",
|
|
29
|
+
"AsyncVendorval",
|
|
30
|
+
"AuthenticationError",
|
|
31
|
+
"ConflictError",
|
|
32
|
+
"CountryError",
|
|
33
|
+
"NotFoundError",
|
|
34
|
+
"Page",
|
|
35
|
+
"PermissionError",
|
|
36
|
+
"ProviderError",
|
|
37
|
+
"RateLimitError",
|
|
38
|
+
"VERSION",
|
|
39
|
+
"ValidationError",
|
|
40
|
+
"Vendorval",
|
|
41
|
+
"VendorvalError",
|
|
42
|
+
"construct_event",
|
|
43
|
+
"generate_idempotency_key",
|
|
44
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Asynchronous client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._request import resolve_config
|
|
10
|
+
from ._version import API_VERSION, VERSION
|
|
11
|
+
from ._webhooks import construct_event
|
|
12
|
+
from .resources._entities import AsyncEntitiesResource
|
|
13
|
+
from .resources._meta import AsyncMetaResource
|
|
14
|
+
from .resources._monitors import AsyncMonitorsResource
|
|
15
|
+
from .resources._simple import AsyncJobsResource, AsyncProvidersResource, AsyncUsageResource
|
|
16
|
+
from .resources._verifications import AsyncVerificationsResource
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Webhooks:
|
|
20
|
+
construct_event = staticmethod(construct_event)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncVendorval:
|
|
24
|
+
"""Asynchronous VendorVal client.
|
|
25
|
+
|
|
26
|
+
Use as an async context manager so the underlying httpx client is closed:
|
|
27
|
+
|
|
28
|
+
async with AsyncVendorval() as client:
|
|
29
|
+
await client.entities.lookup(identifiers={"uei": "ABCD12345678"})
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
VERSION = VERSION
|
|
33
|
+
API_VERSION = API_VERSION
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
api_key: str | None = None,
|
|
39
|
+
base_url: str | None = None,
|
|
40
|
+
timeout: float | None = None,
|
|
41
|
+
max_retries: int | None = None,
|
|
42
|
+
validate_api_key: bool = True,
|
|
43
|
+
http_client: httpx.AsyncClient | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._cfg = resolve_config(
|
|
46
|
+
api_key=api_key,
|
|
47
|
+
base_url=base_url,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
max_retries=max_retries,
|
|
50
|
+
validate_api_key=validate_api_key,
|
|
51
|
+
)
|
|
52
|
+
self._owns_http = http_client is None
|
|
53
|
+
self._http = http_client or httpx.AsyncClient(timeout=self._cfg.timeout)
|
|
54
|
+
self.entities = AsyncEntitiesResource(self._cfg, self._http)
|
|
55
|
+
self.verifications = AsyncVerificationsResource(self._cfg, self._http)
|
|
56
|
+
self.monitors = AsyncMonitorsResource(self._cfg, self._http)
|
|
57
|
+
self.providers = AsyncProvidersResource(self._cfg, self._http)
|
|
58
|
+
self.meta = AsyncMetaResource(self._cfg, self._http)
|
|
59
|
+
self.usage = AsyncUsageResource(self._cfg, self._http)
|
|
60
|
+
self.jobs = AsyncJobsResource(self._cfg, self._http)
|
|
61
|
+
self.webhooks = _Webhooks()
|
|
62
|
+
|
|
63
|
+
async def aclose(self) -> None:
|
|
64
|
+
if self._owns_http:
|
|
65
|
+
await self._http.aclose()
|
|
66
|
+
|
|
67
|
+
async def __aenter__(self) -> AsyncVendorval:
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __aexit__(
|
|
71
|
+
self,
|
|
72
|
+
exc_type: type[BaseException] | None,
|
|
73
|
+
exc: BaseException | None,
|
|
74
|
+
tb: TracebackType | None,
|
|
75
|
+
) -> None:
|
|
76
|
+
await self.aclose()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def api_key(self) -> str:
|
|
80
|
+
return self._cfg.api_key
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def base_url(self) -> str:
|
|
84
|
+
return self._cfg.base_url
|
vendorval_sdk/_client.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Synchronous client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._request import resolve_config
|
|
10
|
+
from ._version import API_VERSION, VERSION
|
|
11
|
+
from ._webhooks import construct_event
|
|
12
|
+
from .resources._entities import EntitiesResource
|
|
13
|
+
from .resources._meta import MetaResource
|
|
14
|
+
from .resources._monitors import MonitorsResource
|
|
15
|
+
from .resources._simple import JobsResource, ProvidersResource, UsageResource
|
|
16
|
+
from .resources._verifications import VerificationsResource
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Webhooks:
|
|
20
|
+
construct_event = staticmethod(construct_event)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Vendorval:
|
|
24
|
+
"""Synchronous VendorVal client.
|
|
25
|
+
|
|
26
|
+
Construct directly or use as a context manager:
|
|
27
|
+
|
|
28
|
+
with Vendorval() as client:
|
|
29
|
+
client.entities.lookup(identifiers={"uei": "ABCD12345678"})
|
|
30
|
+
|
|
31
|
+
The constructor reads `VENDORVAL_API_KEY` and `VENDORVAL_BASE_URL` from
|
|
32
|
+
the environment if not supplied.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
VERSION = VERSION
|
|
36
|
+
API_VERSION = API_VERSION
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
api_key: str | None = None,
|
|
42
|
+
base_url: str | None = None,
|
|
43
|
+
timeout: float | None = None,
|
|
44
|
+
max_retries: int | None = None,
|
|
45
|
+
validate_api_key: bool = True,
|
|
46
|
+
http_client: httpx.Client | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._cfg = resolve_config(
|
|
49
|
+
api_key=api_key,
|
|
50
|
+
base_url=base_url,
|
|
51
|
+
timeout=timeout,
|
|
52
|
+
max_retries=max_retries,
|
|
53
|
+
validate_api_key=validate_api_key,
|
|
54
|
+
)
|
|
55
|
+
self._owns_http = http_client is None
|
|
56
|
+
self._http = http_client or httpx.Client(timeout=self._cfg.timeout)
|
|
57
|
+
self.entities = EntitiesResource(self._cfg, self._http)
|
|
58
|
+
self.verifications = VerificationsResource(self._cfg, self._http)
|
|
59
|
+
self.monitors = MonitorsResource(self._cfg, self._http)
|
|
60
|
+
self.providers = ProvidersResource(self._cfg, self._http)
|
|
61
|
+
self.meta = MetaResource(self._cfg, self._http)
|
|
62
|
+
self.usage = UsageResource(self._cfg, self._http)
|
|
63
|
+
self.jobs = JobsResource(self._cfg, self._http)
|
|
64
|
+
self.webhooks = _Webhooks()
|
|
65
|
+
|
|
66
|
+
def close(self) -> None:
|
|
67
|
+
if self._owns_http:
|
|
68
|
+
self._http.close()
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> Vendorval:
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(
|
|
74
|
+
self,
|
|
75
|
+
exc_type: type[BaseException] | None,
|
|
76
|
+
exc: BaseException | None,
|
|
77
|
+
tb: TracebackType | None,
|
|
78
|
+
) -> None:
|
|
79
|
+
self.close()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def api_key(self) -> str:
|
|
83
|
+
return self._cfg.api_key
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def base_url(self) -> str:
|
|
87
|
+
return self._cfg.base_url
|
vendorval_sdk/_errors.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Error classes mirroring the VendorVal API error envelope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from email.utils import parsedate_to_datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VendorvalError(Exception):
|
|
12
|
+
"""Base class for all SDK errors."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
*,
|
|
18
|
+
status: int = 0,
|
|
19
|
+
type: str = "api_error",
|
|
20
|
+
code: str = "api_error",
|
|
21
|
+
request_id: str | None = None,
|
|
22
|
+
param: str | None = None,
|
|
23
|
+
details: Any = None,
|
|
24
|
+
headers: httpx.Headers | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.message = message
|
|
28
|
+
self.status = status
|
|
29
|
+
self.type = type
|
|
30
|
+
self.code = code
|
|
31
|
+
self.request_id = request_id
|
|
32
|
+
self.param = param
|
|
33
|
+
self.details = details
|
|
34
|
+
self.headers = headers
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class APIError(VendorvalError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AuthenticationError(VendorvalError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PermissionError(VendorvalError): # noqa: A001 - shadowing builtin is intentional
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidationError(VendorvalError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CountryError(ValidationError):
|
|
54
|
+
"""422 raised when a request can't be routed to a country/provider.
|
|
55
|
+
|
|
56
|
+
Subclass of :class:`ValidationError` so existing 4xx catch-all handlers
|
|
57
|
+
continue to match. Use ``err.code`` to switch on the specific failure:
|
|
58
|
+
|
|
59
|
+
- ``country_required`` — no explicit country and nothing inferable
|
|
60
|
+
- ``country_not_supported`` — resolved country isn't in SUPPORTED_COUNTRIES
|
|
61
|
+
- ``identifier_not_supported_for_country`` — e.g. ``tin`` with ``country='DE'``
|
|
62
|
+
- ``check_not_supported_for_country`` — e.g. ``sam_registration`` for an EU country
|
|
63
|
+
- ``country_mismatch`` — explicit country contradicts identifier inference
|
|
64
|
+
|
|
65
|
+
The ``details`` attribute carries a :data:`CountryErrorDetails`-shaped dict
|
|
66
|
+
with ``country_resolved``, ``identifiers_seen``, ``recommended_action``,
|
|
67
|
+
``supported_countries``, and (for ``country_mismatch``) ``candidates``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_COUNTRY_ERROR_CODES = frozenset(
|
|
72
|
+
{
|
|
73
|
+
"country_required",
|
|
74
|
+
"country_not_supported",
|
|
75
|
+
"identifier_not_supported_for_country",
|
|
76
|
+
"check_not_supported_for_country",
|
|
77
|
+
"country_mismatch",
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NotFoundError(VendorvalError):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ConflictError(VendorvalError):
|
|
87
|
+
def __init__(self, *args: Any, candidates: list[Any] | None = None, **kwargs: Any) -> None:
|
|
88
|
+
super().__init__(*args, **kwargs)
|
|
89
|
+
self.candidates = candidates
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RateLimitError(VendorvalError):
|
|
93
|
+
def __init__(self, *args: Any, retry_after: float | None = None, **kwargs: Any) -> None:
|
|
94
|
+
super().__init__(*args, **kwargs)
|
|
95
|
+
self.retry_after = retry_after
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ProviderError(VendorvalError):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class APIConnectionError(VendorvalError):
|
|
103
|
+
def __init__(self, message: str, *, request_id: str | None = None) -> None:
|
|
104
|
+
super().__init__(
|
|
105
|
+
message,
|
|
106
|
+
status=0,
|
|
107
|
+
type="connection_error",
|
|
108
|
+
code="connection_error",
|
|
109
|
+
request_id=request_id,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class APITimeoutError(APIConnectionError):
|
|
114
|
+
def __init__(self, timeout: float, *, request_id: str | None = None) -> None:
|
|
115
|
+
super().__init__(f"Request timed out after {timeout}s", request_id=request_id)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_STATUS_TO_CLS: dict[int, type[VendorvalError]] = {
|
|
119
|
+
400: ValidationError,
|
|
120
|
+
401: AuthenticationError,
|
|
121
|
+
403: PermissionError,
|
|
122
|
+
404: NotFoundError,
|
|
123
|
+
# 422 is an invalid-request status the API uses for semantic-validation
|
|
124
|
+
# failures (Phase J country routing emits 422 + CountryError, but other
|
|
125
|
+
# semantic violations also land here). Mapping to ValidationError keeps
|
|
126
|
+
# catch-all 4xx handlers working consistently.
|
|
127
|
+
422: ValidationError,
|
|
128
|
+
429: RateLimitError,
|
|
129
|
+
502: ProviderError,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_retry_after(value: str | None) -> float | None:
|
|
134
|
+
if not value:
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
return float(value)
|
|
138
|
+
except ValueError:
|
|
139
|
+
pass
|
|
140
|
+
try:
|
|
141
|
+
dt = parsedate_to_datetime(value)
|
|
142
|
+
from datetime import datetime, timezone
|
|
143
|
+
|
|
144
|
+
return max(0.0, (dt - datetime.now(timezone.utc)).total_seconds())
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def error_from_response(
|
|
150
|
+
*,
|
|
151
|
+
status: int,
|
|
152
|
+
payload: Any,
|
|
153
|
+
headers: httpx.Headers,
|
|
154
|
+
request_id: str | None,
|
|
155
|
+
) -> VendorvalError:
|
|
156
|
+
envelope = None
|
|
157
|
+
if isinstance(payload, dict) and isinstance(payload.get("error"), dict):
|
|
158
|
+
envelope = payload["error"]
|
|
159
|
+
|
|
160
|
+
msg = (envelope or {}).get("message") or f"VendorVal API error (status {status})"
|
|
161
|
+
type_ = (envelope or {}).get("type") or "api_error"
|
|
162
|
+
code = (envelope or {}).get("code") or f"http_{status}"
|
|
163
|
+
param = (envelope or {}).get("param")
|
|
164
|
+
details = (envelope or {}).get("details")
|
|
165
|
+
|
|
166
|
+
common: dict[str, Any] = dict(
|
|
167
|
+
message=msg,
|
|
168
|
+
status=status,
|
|
169
|
+
type=type_,
|
|
170
|
+
code=code,
|
|
171
|
+
request_id=request_id,
|
|
172
|
+
param=param,
|
|
173
|
+
details=details,
|
|
174
|
+
headers=headers,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if status == 409:
|
|
178
|
+
candidates = None
|
|
179
|
+
if envelope:
|
|
180
|
+
if isinstance(envelope.get("candidates"), list):
|
|
181
|
+
candidates = envelope["candidates"]
|
|
182
|
+
elif isinstance(envelope.get("details"), dict):
|
|
183
|
+
inner = envelope["details"].get("candidates")
|
|
184
|
+
if isinstance(inner, list):
|
|
185
|
+
candidates = inner
|
|
186
|
+
return ConflictError(**common, candidates=candidates)
|
|
187
|
+
|
|
188
|
+
if status == 429:
|
|
189
|
+
return RateLimitError(
|
|
190
|
+
**common,
|
|
191
|
+
retry_after=parse_retry_after(headers.get("retry-after")),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Phase J: 422 envelopes with a country-routing code surface as CountryError
|
|
195
|
+
# (a ValidationError subclass) so consumers can switch on `err.code` and
|
|
196
|
+
# inspect the structured `err.details` payload. The `isinstance(code, str)`
|
|
197
|
+
# guard handles malformed payloads where `code` is a non-string (e.g. a list
|
|
198
|
+
# or dict) — set membership on a frozenset[str] would otherwise TypeError
|
|
199
|
+
# on unhashable values, breaking error normalization for the rest of the
|
|
200
|
+
# response.
|
|
201
|
+
if status == 422 and isinstance(code, str) and code in _COUNTRY_ERROR_CODES:
|
|
202
|
+
return CountryError(**common)
|
|
203
|
+
|
|
204
|
+
cls = _STATUS_TO_CLS.get(status, APIError)
|
|
205
|
+
return cls(**common)
|
vendorval_sdk/_models.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Lightweight response wrappers that expose `request_id`.
|
|
2
|
+
|
|
3
|
+
Kept as `__getattr__`-backed dict facades so we don't tie the SDK to a strict
|
|
4
|
+
schema while the API is still stabilizing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Response:
|
|
13
|
+
"""Dict-backed object that exposes the API response plus `_request_id`."""
|
|
14
|
+
|
|
15
|
+
__slots__ = ("_data", "_request_id", "_status")
|
|
16
|
+
|
|
17
|
+
def __init__(self, data: Any, request_id: str | None, status: int) -> None:
|
|
18
|
+
self._data = data if isinstance(data, dict) else {}
|
|
19
|
+
self._request_id = request_id
|
|
20
|
+
self._status = status
|
|
21
|
+
|
|
22
|
+
def __getattr__(self, name: str) -> Any:
|
|
23
|
+
try:
|
|
24
|
+
return self._data[name]
|
|
25
|
+
except KeyError as err:
|
|
26
|
+
raise AttributeError(name) from err
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, key: str) -> Any:
|
|
29
|
+
return self._data[key]
|
|
30
|
+
|
|
31
|
+
def __contains__(self, key: str) -> bool:
|
|
32
|
+
return key in self._data
|
|
33
|
+
|
|
34
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
35
|
+
return self._data.get(key, default)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
return dict(self._data)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def request_id(self) -> str | None:
|
|
42
|
+
return self._request_id
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def status(self) -> int:
|
|
46
|
+
return self._status
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"<Response request_id={self._request_id!r} status={self._status}>"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VerificationBundleResponse(Response):
|
|
53
|
+
@property
|
|
54
|
+
def entity(self) -> Response:
|
|
55
|
+
return Response(self._data.get("entity"), self._request_id, self._status)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def verification(self) -> Response:
|
|
59
|
+
return Response(self._data.get("verification"), self._request_id, self._status)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Pagination helpers.
|
|
2
|
+
|
|
3
|
+
Today list endpoints return arrays. Wrapping in `Page` keeps the
|
|
4
|
+
`for item in page` and `async for item in page` shapes stable for when
|
|
5
|
+
the API ships cursor pagination.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import AsyncIterator, Iterator
|
|
11
|
+
from typing import Generic, TypeVar
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Page(Generic[T]):
|
|
17
|
+
def __init__(self, items: list[T]) -> None:
|
|
18
|
+
self._items = list(items)
|
|
19
|
+
|
|
20
|
+
def __iter__(self) -> Iterator[T]:
|
|
21
|
+
return iter(self._items)
|
|
22
|
+
|
|
23
|
+
async def __aiter__(self) -> AsyncIterator[T]:
|
|
24
|
+
for item in self._items:
|
|
25
|
+
yield item
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
return len(self._items)
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, index: int) -> T:
|
|
31
|
+
return self._items[index]
|
|
32
|
+
|
|
33
|
+
def all(self) -> list[T]:
|
|
34
|
+
return list(self._items)
|