vatly-api-python 0.1.0a2__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.
- vatly/__init__.py +43 -0
- vatly/_model.py +13 -0
- vatly/_transport.py +284 -0
- vatly/_version.py +1 -0
- vatly/client.py +100 -0
- vatly/errors.py +86 -0
- vatly/models.py +3009 -0
- vatly/pagination.py +29 -0
- vatly/py.typed +1 -0
- vatly/resources.py +2003 -0
- vatly/webhooks.py +146 -0
- vatly_api_python-0.1.0a2.dist-info/METADATA +144 -0
- vatly_api_python-0.1.0a2.dist-info/RECORD +15 -0
- vatly_api_python-0.1.0a2.dist-info/WHEEL +4 -0
- vatly_api_python-0.1.0a2.dist-info/licenses/LICENSE +21 -0
vatly/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Vatly Python SDK."""
|
|
2
|
+
|
|
3
|
+
from . import models, webhooks
|
|
4
|
+
from ._transport import VATLY_BASE_URL, RequestOptions
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .client import AsyncVatlyClient, VatlyClient
|
|
7
|
+
from .errors import (
|
|
8
|
+
VatlyApiError,
|
|
9
|
+
VatlyAuthError,
|
|
10
|
+
VatlyConflictError,
|
|
11
|
+
VatlyConnectionError,
|
|
12
|
+
VatlyError,
|
|
13
|
+
VatlyForbiddenError,
|
|
14
|
+
VatlyInvalidSignatureError,
|
|
15
|
+
VatlyNotFoundError,
|
|
16
|
+
VatlyRateLimitError,
|
|
17
|
+
VatlyTimeoutError,
|
|
18
|
+
VatlyValidationError,
|
|
19
|
+
)
|
|
20
|
+
from .pagination import Page, VatlyPaginationError
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"VATLY_BASE_URL",
|
|
24
|
+
"AsyncVatlyClient",
|
|
25
|
+
"Page",
|
|
26
|
+
"RequestOptions",
|
|
27
|
+
"VatlyApiError",
|
|
28
|
+
"VatlyAuthError",
|
|
29
|
+
"VatlyClient",
|
|
30
|
+
"VatlyConflictError",
|
|
31
|
+
"VatlyConnectionError",
|
|
32
|
+
"VatlyError",
|
|
33
|
+
"VatlyForbiddenError",
|
|
34
|
+
"VatlyInvalidSignatureError",
|
|
35
|
+
"VatlyNotFoundError",
|
|
36
|
+
"VatlyPaginationError",
|
|
37
|
+
"VatlyRateLimitError",
|
|
38
|
+
"VatlyTimeoutError",
|
|
39
|
+
"VatlyValidationError",
|
|
40
|
+
"__version__",
|
|
41
|
+
"models",
|
|
42
|
+
"webhooks",
|
|
43
|
+
]
|
vatly/_model.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, model_validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VatlyModel(BaseModel):
|
|
7
|
+
@model_validator(mode="before")
|
|
8
|
+
@classmethod
|
|
9
|
+
def normalize_empty_metadata(cls, value: Any) -> Any:
|
|
10
|
+
# PHP's empty associative arrays can serialize as [] instead of {}.
|
|
11
|
+
if isinstance(value, dict) and value.get("metadata") == []:
|
|
12
|
+
return {**value, "metadata": {}}
|
|
13
|
+
return value
|
vatly/_transport.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from email.utils import parsedate_to_datetime
|
|
14
|
+
from typing import Any, TypeVar
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
19
|
+
from typing_extensions import Self
|
|
20
|
+
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
from .errors import (
|
|
23
|
+
VatlyApiError,
|
|
24
|
+
VatlyConnectionError,
|
|
25
|
+
VatlyError,
|
|
26
|
+
VatlyTimeoutError,
|
|
27
|
+
error_from_response,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
VATLY_BASE_URL = "https://api.vatly.com/v1"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RequestOptions:
|
|
36
|
+
"""Per-request overrides. Timeouts are seconds; zero disables the timeout."""
|
|
37
|
+
|
|
38
|
+
idempotency_key: str | None = None
|
|
39
|
+
timeout: float | None = None
|
|
40
|
+
headers: Mapping[str, str] | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _timeout(value: float) -> float | None:
|
|
44
|
+
if not math.isfinite(value) or value < 0:
|
|
45
|
+
raise ValueError("timeout must be finite and non-negative")
|
|
46
|
+
return value or None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _retry_after(value: str | None) -> float | None:
|
|
50
|
+
if not value:
|
|
51
|
+
return None
|
|
52
|
+
if value.strip().isdigit():
|
|
53
|
+
return float(value)
|
|
54
|
+
try:
|
|
55
|
+
date = parsedate_to_datetime(value)
|
|
56
|
+
return max(0, (date - datetime.now(timezone.utc)).total_seconds())
|
|
57
|
+
except (ValueError, TypeError, OverflowError):
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Transport:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
api_key: str | None = None,
|
|
65
|
+
*,
|
|
66
|
+
base_url: str = VATLY_BASE_URL,
|
|
67
|
+
timeout: float = 30,
|
|
68
|
+
max_retries: int = 2,
|
|
69
|
+
default_headers: Mapping[str, str] | None = None,
|
|
70
|
+
idempotency_key_generator: Callable[[], str] | None = lambda: str(uuid4()),
|
|
71
|
+
client_info: Mapping[str, str] | bool = True,
|
|
72
|
+
transport: httpx.BaseTransport | httpx.AsyncBaseTransport | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
key = api_key if api_key is not None else os.environ.get("VATLY_API_KEY", "")
|
|
75
|
+
if not re.fullmatch(r"(live|test)_[\w|]{18,}", key, flags=re.ASCII):
|
|
76
|
+
raise ValueError("Pass a valid live_ or test_ API key or set VATLY_API_KEY")
|
|
77
|
+
_timeout(timeout)
|
|
78
|
+
if isinstance(max_retries, bool) or not isinstance(max_retries, int) or max_retries < 0:
|
|
79
|
+
raise ValueError("max_retries must be a non-negative integer")
|
|
80
|
+
self.mode = "test" if key.startswith("test_") else "live"
|
|
81
|
+
self.base_url = base_url.rstrip("/")
|
|
82
|
+
url = httpx.URL(self.base_url)
|
|
83
|
+
if url.scheme not in {"http", "https"} or not url.host or url.query or url.fragment:
|
|
84
|
+
raise ValueError("base_url must be an HTTP(S) URL without query or fragment")
|
|
85
|
+
self.timeout = timeout
|
|
86
|
+
self.max_retries = max_retries
|
|
87
|
+
self._key_generator = idempotency_key_generator
|
|
88
|
+
self._headers = httpx.Headers(
|
|
89
|
+
{
|
|
90
|
+
"Authorization": f"Bearer {key}",
|
|
91
|
+
"Accept": "application/json",
|
|
92
|
+
"User-Agent": f"vatly-api-python/{__version__}",
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
self._headers.update(default_headers or {})
|
|
96
|
+
if client_info is not False:
|
|
97
|
+
info = f"sdk=vatly-api-python/{__version__}; runtime=python/{platform.python_version()}; os={platform.system()}; arch={platform.machine()}"
|
|
98
|
+
if isinstance(client_info, Mapping):
|
|
99
|
+
for name in ("app", "framework"):
|
|
100
|
+
value = re.sub(r"[\r\n;,]|\s+", " ", client_info.get(name, "")).strip()[:64]
|
|
101
|
+
if value:
|
|
102
|
+
info += f"; {name}={value}"
|
|
103
|
+
self._headers["Vatly-Client-Info"] = info
|
|
104
|
+
self._custom_transport = transport
|
|
105
|
+
self._init_http()
|
|
106
|
+
|
|
107
|
+
def _init_http(self) -> None:
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def serialize(value: BaseModel | dict[str, Any] | None) -> dict[str, Any] | None:
|
|
112
|
+
if isinstance(value, BaseModel):
|
|
113
|
+
return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
114
|
+
return dict(value) if value is not None else None
|
|
115
|
+
|
|
116
|
+
def _prepare(
|
|
117
|
+
self,
|
|
118
|
+
method: str,
|
|
119
|
+
path: str,
|
|
120
|
+
body: BaseModel | dict[str, Any] | None,
|
|
121
|
+
params: BaseModel | dict[str, Any] | None,
|
|
122
|
+
options: RequestOptions | None,
|
|
123
|
+
) -> tuple[dict[str, Any], bool]:
|
|
124
|
+
opts = options or RequestOptions()
|
|
125
|
+
headers = self._headers.copy()
|
|
126
|
+
headers.update(opts.headers or {})
|
|
127
|
+
key = opts.idempotency_key
|
|
128
|
+
if key is None:
|
|
129
|
+
key = headers.get("Idempotency-Key")
|
|
130
|
+
if key is None and method in {"POST", "PATCH", "PUT", "DELETE"} and self._key_generator:
|
|
131
|
+
key = self._key_generator()
|
|
132
|
+
if key:
|
|
133
|
+
headers["Idempotency-Key"] = key
|
|
134
|
+
elif "Idempotency-Key" in headers:
|
|
135
|
+
del headers["Idempotency-Key"]
|
|
136
|
+
query = self.serialize(params) or {}
|
|
137
|
+
if query.get("startingAfter") is not None and query.get("endingBefore") is not None:
|
|
138
|
+
raise ValueError("startingAfter and endingBefore are mutually exclusive")
|
|
139
|
+
query = {k: int(v) if isinstance(v, bool) else v for k, v in query.items() if v is not None}
|
|
140
|
+
return {
|
|
141
|
+
"method": method,
|
|
142
|
+
"url": self.base_url + path,
|
|
143
|
+
"headers": headers,
|
|
144
|
+
"params": query,
|
|
145
|
+
"json": self.serialize(body),
|
|
146
|
+
"timeout": _timeout(self.timeout if opts.timeout is None else opts.timeout),
|
|
147
|
+
}, method in {"GET", "HEAD", "OPTIONS"} or bool(key)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _parse(response: httpx.Response, response_type: type[T]) -> T:
|
|
151
|
+
if not response.is_success:
|
|
152
|
+
try:
|
|
153
|
+
body = response.json()
|
|
154
|
+
except ValueError:
|
|
155
|
+
body = response.text
|
|
156
|
+
raise error_from_response(
|
|
157
|
+
response.status_code,
|
|
158
|
+
body,
|
|
159
|
+
str(response.request.url),
|
|
160
|
+
response.request.method,
|
|
161
|
+
_retry_after(response.headers.get("Retry-After")),
|
|
162
|
+
)
|
|
163
|
+
try:
|
|
164
|
+
if response_type is type(None) and not response.content:
|
|
165
|
+
return None
|
|
166
|
+
return TypeAdapter(response_type).validate_json(response.content)
|
|
167
|
+
except (ValueError, ValidationError) as exc:
|
|
168
|
+
raise VatlyApiError(
|
|
169
|
+
"Invalid Vatly response",
|
|
170
|
+
status=response.status_code,
|
|
171
|
+
body=response.text,
|
|
172
|
+
url=str(response.request.url),
|
|
173
|
+
method=response.request.method,
|
|
174
|
+
) from exc
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _network_error(exc: httpx.TransportError, kwargs: dict[str, Any]) -> VatlyConnectionError:
|
|
178
|
+
try:
|
|
179
|
+
request = exc.request
|
|
180
|
+
except RuntimeError:
|
|
181
|
+
request = httpx.Request(kwargs["method"], kwargs["url"], params=kwargs["params"])
|
|
182
|
+
cls = VatlyTimeoutError if isinstance(exc, httpx.TimeoutException) else VatlyConnectionError
|
|
183
|
+
error = cls(
|
|
184
|
+
"Vatly request timed out" if cls is VatlyTimeoutError else "Vatly connection failed",
|
|
185
|
+
method=request.method,
|
|
186
|
+
url=str(request.url),
|
|
187
|
+
)
|
|
188
|
+
error.__cause__ = exc
|
|
189
|
+
return error
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _delay(error: VatlyError, attempt: int, retryable: bool, max_retries: int) -> float:
|
|
193
|
+
can_retry = (
|
|
194
|
+
isinstance(error, VatlyConnectionError)
|
|
195
|
+
or error.status == 429
|
|
196
|
+
or error.status >= 500
|
|
197
|
+
or (error.status == 409 and error.details.get("idempotency_outcome") == "in_progress")
|
|
198
|
+
)
|
|
199
|
+
if not retryable or attempt >= max_retries or not can_retry:
|
|
200
|
+
raise error
|
|
201
|
+
if error.retry_after is not None:
|
|
202
|
+
return error.retry_after
|
|
203
|
+
base = min(0.25 * 2.0 ** min(attempt, 6), 8.0)
|
|
204
|
+
return base + random.random() * base * 0.25
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class SyncTransport(_Transport):
|
|
208
|
+
def _init_http(self) -> None:
|
|
209
|
+
if self._custom_transport is not None and not isinstance(
|
|
210
|
+
self._custom_transport, httpx.BaseTransport
|
|
211
|
+
):
|
|
212
|
+
raise TypeError("Sync client requires an httpx.BaseTransport")
|
|
213
|
+
self._http = httpx.Client(transport=self._custom_transport, follow_redirects=False)
|
|
214
|
+
|
|
215
|
+
def request(
|
|
216
|
+
self,
|
|
217
|
+
method: str,
|
|
218
|
+
path: str,
|
|
219
|
+
response_type: type[T],
|
|
220
|
+
*,
|
|
221
|
+
body: BaseModel | dict[str, Any] | None = None,
|
|
222
|
+
params: BaseModel | dict[str, Any] | None = None,
|
|
223
|
+
options: RequestOptions | None = None,
|
|
224
|
+
) -> T:
|
|
225
|
+
kwargs, retryable = self._prepare(method, path, body, params, options)
|
|
226
|
+
for attempt in range(self.max_retries + 1):
|
|
227
|
+
try:
|
|
228
|
+
response = self._http.request(**kwargs)
|
|
229
|
+
return self._parse(response, response_type)
|
|
230
|
+
except httpx.TransportError as exc:
|
|
231
|
+
error: VatlyError = self._network_error(exc, kwargs)
|
|
232
|
+
except VatlyError as exc:
|
|
233
|
+
error = exc
|
|
234
|
+
time.sleep(self._delay(error, attempt, retryable, self.max_retries))
|
|
235
|
+
raise AssertionError("unreachable")
|
|
236
|
+
|
|
237
|
+
def close(self) -> None:
|
|
238
|
+
self._http.close()
|
|
239
|
+
|
|
240
|
+
def __enter__(self) -> Self:
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def __exit__(self, *args: object) -> None:
|
|
244
|
+
self.close()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class AsyncTransport(_Transport):
|
|
248
|
+
def _init_http(self) -> None:
|
|
249
|
+
if self._custom_transport is not None and not isinstance(
|
|
250
|
+
self._custom_transport, httpx.AsyncBaseTransport
|
|
251
|
+
):
|
|
252
|
+
raise TypeError("Async client requires an httpx.AsyncBaseTransport")
|
|
253
|
+
self._http = httpx.AsyncClient(transport=self._custom_transport, follow_redirects=False)
|
|
254
|
+
|
|
255
|
+
async def request(
|
|
256
|
+
self,
|
|
257
|
+
method: str,
|
|
258
|
+
path: str,
|
|
259
|
+
response_type: type[T],
|
|
260
|
+
*,
|
|
261
|
+
body: BaseModel | dict[str, Any] | None = None,
|
|
262
|
+
params: BaseModel | dict[str, Any] | None = None,
|
|
263
|
+
options: RequestOptions | None = None,
|
|
264
|
+
) -> T:
|
|
265
|
+
kwargs, retryable = self._prepare(method, path, body, params, options)
|
|
266
|
+
for attempt in range(self.max_retries + 1):
|
|
267
|
+
try:
|
|
268
|
+
response = await self._http.request(**kwargs)
|
|
269
|
+
return self._parse(response, response_type)
|
|
270
|
+
except httpx.TransportError as exc:
|
|
271
|
+
error: VatlyError = self._network_error(exc, kwargs)
|
|
272
|
+
except VatlyError as exc:
|
|
273
|
+
error = exc
|
|
274
|
+
await asyncio.sleep(self._delay(error, attempt, retryable, self.max_retries))
|
|
275
|
+
raise AssertionError("unreachable")
|
|
276
|
+
|
|
277
|
+
async def aclose(self) -> None:
|
|
278
|
+
await self._http.aclose()
|
|
279
|
+
|
|
280
|
+
async def __aenter__(self) -> Self:
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
async def __aexit__(self, *args: object) -> None:
|
|
284
|
+
await self.aclose()
|
vatly/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0a2"
|
vatly/client.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Vatly synchronous and asynchronous clients."""
|
|
2
|
+
|
|
3
|
+
from . import resources
|
|
4
|
+
from ._transport import AsyncTransport, SyncTransport
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VatlyClient(SyncTransport):
|
|
8
|
+
"""Resources are initialized lazily and share this client's connection pool."""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def checkouts(self) -> resources.Checkouts:
|
|
12
|
+
return resources.Checkouts(self)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def customers(self) -> resources.Customers:
|
|
16
|
+
return resources.Customers(self)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def one_off_products(self) -> resources.OneOffProducts:
|
|
20
|
+
return resources.OneOffProducts(self)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def subscription_plans(self) -> resources.SubscriptionPlans:
|
|
24
|
+
return resources.SubscriptionPlans(self)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def orders(self) -> resources.Orders:
|
|
28
|
+
return resources.Orders(self)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def refunds(self) -> resources.Refunds:
|
|
32
|
+
return resources.Refunds(self)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def chargebacks(self) -> resources.Chargebacks:
|
|
36
|
+
return resources.Chargebacks(self)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def webhook_events(self) -> resources.WebhookEvents:
|
|
40
|
+
return resources.WebhookEvents(self)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def webhook_endpoints(self) -> resources.WebhookEndpoints:
|
|
44
|
+
return resources.WebhookEndpoints(self)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def subscriptions(self) -> resources.Subscriptions:
|
|
48
|
+
return resources.Subscriptions(self)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def test_helpers(self) -> resources.TestHelpers:
|
|
52
|
+
return resources.TestHelpers(self)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AsyncVatlyClient(AsyncTransport):
|
|
56
|
+
"""Resources are initialized lazily and share this client's connection pool."""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def checkouts(self) -> resources.AsyncCheckouts:
|
|
60
|
+
return resources.AsyncCheckouts(self)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def customers(self) -> resources.AsyncCustomers:
|
|
64
|
+
return resources.AsyncCustomers(self)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def one_off_products(self) -> resources.AsyncOneOffProducts:
|
|
68
|
+
return resources.AsyncOneOffProducts(self)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def subscription_plans(self) -> resources.AsyncSubscriptionPlans:
|
|
72
|
+
return resources.AsyncSubscriptionPlans(self)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def orders(self) -> resources.AsyncOrders:
|
|
76
|
+
return resources.AsyncOrders(self)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def refunds(self) -> resources.AsyncRefunds:
|
|
80
|
+
return resources.AsyncRefunds(self)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def chargebacks(self) -> resources.AsyncChargebacks:
|
|
84
|
+
return resources.AsyncChargebacks(self)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def webhook_events(self) -> resources.AsyncWebhookEvents:
|
|
88
|
+
return resources.AsyncWebhookEvents(self)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def webhook_endpoints(self) -> resources.AsyncWebhookEndpoints:
|
|
92
|
+
return resources.AsyncWebhookEndpoints(self)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def subscriptions(self) -> resources.AsyncSubscriptions:
|
|
96
|
+
return resources.AsyncSubscriptions(self)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def test_helpers(self) -> resources.AsyncTestHelpers:
|
|
100
|
+
return resources.AsyncTestHelpers(self)
|
vatly/errors.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Typed Vatly API, transport, and webhook errors."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VatlyError(Exception):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
*,
|
|
11
|
+
status: int = 0,
|
|
12
|
+
body: Any = None,
|
|
13
|
+
url: str | None = None,
|
|
14
|
+
method: str | None = None,
|
|
15
|
+
retry_after: float | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status = status
|
|
19
|
+
self.body = body
|
|
20
|
+
self.url = url
|
|
21
|
+
self.method = method
|
|
22
|
+
self.retry_after = retry_after
|
|
23
|
+
self.details: dict[str, Any] = body.get("details", {}) if isinstance(body, dict) else {}
|
|
24
|
+
self.errors: dict[str, list[str]] = body.get("errors", {}) if isinstance(body, dict) else {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VatlyAuthError(VatlyError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VatlyForbiddenError(VatlyError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VatlyNotFoundError(VatlyError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VatlyConflictError(VatlyError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VatlyValidationError(VatlyError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VatlyRateLimitError(VatlyError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class VatlyApiError(VatlyError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class VatlyConnectionError(VatlyError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class VatlyTimeoutError(VatlyConnectionError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class VatlyInvalidSignatureError(Exception):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def error_from_response(
|
|
68
|
+
status: int, body: Any, url: str, method: str, retry_after: float | None
|
|
69
|
+
) -> VatlyError:
|
|
70
|
+
cls = {
|
|
71
|
+
401: VatlyAuthError,
|
|
72
|
+
403: VatlyForbiddenError,
|
|
73
|
+
404: VatlyNotFoundError,
|
|
74
|
+
409: VatlyConflictError,
|
|
75
|
+
422: VatlyValidationError,
|
|
76
|
+
429: VatlyRateLimitError,
|
|
77
|
+
}.get(status, VatlyApiError)
|
|
78
|
+
message = body.get("message") if isinstance(body, dict) else None
|
|
79
|
+
return cls(
|
|
80
|
+
message if isinstance(message, str) else f"Vatly API error ({status}).",
|
|
81
|
+
status=status,
|
|
82
|
+
body=body,
|
|
83
|
+
url=url,
|
|
84
|
+
method=method,
|
|
85
|
+
retry_after=retry_after,
|
|
86
|
+
)
|