raims 0.1.3__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.
raims/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ from ._client import VERSION as __version__
2
+ from ._client import AsyncRaimsClient as AsyncRaimsClient
3
+ from ._client import RaimsClient as RaimsClient
4
+ from ._errors import (
5
+ ApiError as ApiError,
6
+ )
7
+ from ._errors import (
8
+ AuthenticationError as AuthenticationError,
9
+ )
10
+ from ._errors import (
11
+ ConflictError as ConflictError,
12
+ )
13
+ from ._errors import (
14
+ ConnectionError as ConnectionError,
15
+ )
16
+ from ._errors import (
17
+ InvalidRequestError as InvalidRequestError,
18
+ )
19
+ from ._errors import (
20
+ NotFoundError as NotFoundError,
21
+ )
22
+ from ._errors import (
23
+ PermissionError as PermissionError,
24
+ )
25
+ from ._errors import (
26
+ RaimsError as RaimsError,
27
+ )
28
+ from ._errors import (
29
+ RateLimitError as RateLimitError,
30
+ )
31
+ from ._errors import (
32
+ SignatureVerificationError as SignatureVerificationError,
33
+ )
34
+ from ._errors import (
35
+ UnexpectedResponseError as UnexpectedResponseError,
36
+ )
37
+ from ._options import RequestDiagnostics as RequestDiagnostics
38
+ from ._options import RequestOptions as RequestOptions
39
+ from ._pagination import ApiList as ApiList
40
+ from ._pagination import AsyncApiList as AsyncApiList
41
+ from ._resource import ApiResource as ApiResource
42
+ from ._response import LastResponse as LastResponse
43
+ from ._webhook import Webhook as Webhook
44
+
45
+ __all__ = [
46
+ "ApiError",
47
+ "ApiList",
48
+ "ApiResource",
49
+ "AsyncApiList",
50
+ "AsyncRaimsClient",
51
+ "AuthenticationError",
52
+ "ConflictError",
53
+ "ConnectionError",
54
+ "InvalidRequestError",
55
+ "LastResponse",
56
+ "NotFoundError",
57
+ "PermissionError",
58
+ "RaimsClient",
59
+ "RaimsError",
60
+ "RateLimitError",
61
+ "RequestDiagnostics",
62
+ "RequestOptions",
63
+ "SignatureVerificationError",
64
+ "UnexpectedResponseError",
65
+ "Webhook",
66
+ "__version__",
67
+ ]
raims/_client.py ADDED
@@ -0,0 +1,560 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import ipaddress
5
+ import json
6
+ import random
7
+ import re
8
+ import time
9
+ from collections.abc import Mapping
10
+ from contextvars import ContextVar
11
+ from dataclasses import dataclass
12
+ from email.utils import parsedate_to_datetime
13
+ from types import TracebackType
14
+ from typing import Any, TypeVar
15
+ from urllib.parse import quote, urlencode, urlsplit
16
+
17
+ import httpx
18
+
19
+ from . import _errors as errors
20
+ from . import services
21
+ from ._options import (
22
+ RequestDiagnostics,
23
+ RequestObserver,
24
+ RequestOptions,
25
+ validate_retries,
26
+ validate_timeout,
27
+ visible_ascii,
28
+ )
29
+ from ._pagination import ApiList, AsyncApiList
30
+ from ._response import LastResponse
31
+ from ._validation import CONTRACT, hydrate, parse_json, serialize_params, validate_response
32
+ from ._version import __version__ as VERSION
33
+
34
+ _MAX_RESPONSE_BYTES = 16 * 1024 * 1024
35
+ _RETRY_STATUSES = {408, 429, 500, 502, 503, 504}
36
+ _SyncClientT = TypeVar("_SyncClientT", bound="RaimsClient")
37
+ _AsyncClientT = TypeVar("_AsyncClientT", bound="AsyncRaimsClient")
38
+
39
+
40
+ def _api_base(value: str) -> str:
41
+ if not isinstance(value, str) or re.search(r"[\x00-\x20\x7f\\]", value):
42
+ raise ValueError("api_base must be an HTTPS origin without credentials or a path.")
43
+ try:
44
+ url = urlsplit(value)
45
+ hostname = url.hostname
46
+ port = url.port
47
+ except ValueError:
48
+ raise ValueError("api_base is invalid.") from None
49
+ loopback = hostname == "localhost"
50
+ try:
51
+ loopback = loopback or ipaddress.ip_address(hostname or "").is_loopback
52
+ except ValueError:
53
+ pass
54
+ if (
55
+ not hostname
56
+ or url.username is not None
57
+ or url.password is not None
58
+ or url.path not in ("", "/")
59
+ or url.query
60
+ or url.fragment
61
+ or "?" in value
62
+ or "#" in value
63
+ or (url.scheme != "https" and not (url.scheme == "http" and loopback))
64
+ or port == 0
65
+ ):
66
+ raise ValueError(
67
+ "api_base requires HTTPS without credentials, path, query or fragment; HTTP is allowed only on loopback."
68
+ )
69
+ return value.rstrip("/")
70
+
71
+
72
+ def _retry_delay(attempt: int, response: LastResponse | None) -> float | None:
73
+ delay = min(2.0, 0.5 * 2**attempt) * random.uniform(0.5, 1.0)
74
+ header = response.retry_after if response else None
75
+ if header:
76
+ try:
77
+ if re.fullmatch(r"[0-9]+", header.strip()):
78
+ wanted = float(header)
79
+ else:
80
+ wanted = parsedate_to_datetime(header).timestamp() - time.time()
81
+ if wanted > 30:
82
+ return None
83
+ delay = max(delay, wanted)
84
+ except (ValueError, TypeError, OverflowError):
85
+ pass
86
+ return delay
87
+
88
+
89
+ @dataclass
90
+ class _Prepared:
91
+ operation: dict[str, Any]
92
+ path_params: dict[str, str]
93
+ params: dict[str, Any]
94
+ options: RequestOptions
95
+ url: str
96
+ headers: dict[str, str]
97
+ body: bytes | None
98
+ timeout: float
99
+ retries: int
100
+
101
+
102
+ class _ClientBase:
103
+ def __init__(
104
+ self,
105
+ api_key: str,
106
+ *,
107
+ api_base: str,
108
+ timeout: float,
109
+ connect_timeout: float,
110
+ max_network_retries: int,
111
+ request_observer: RequestObserver | None,
112
+ ) -> None:
113
+ self._api_key = visible_ascii(api_key, 4096, "api_key")
114
+ self._base = _api_base(api_base)
115
+ self._timeout = validate_timeout(timeout)
116
+ self._connect_timeout = validate_timeout(connect_timeout)
117
+ self._retries = validate_retries(max_network_retries)
118
+ if request_observer is not None and not callable(request_observer):
119
+ raise ValueError("request_observer must be callable.")
120
+ self._observer = request_observer
121
+ self._notifying: ContextVar[bool] = ContextVar("raims_observer", default=False)
122
+
123
+ def __repr__(self) -> str:
124
+ return f"<{type(self).__name__} version={VERSION}>"
125
+
126
+ def _prepare(
127
+ self,
128
+ operation_id: str,
129
+ path_params: dict[str, str],
130
+ params: Mapping[str, Any] | None,
131
+ options: RequestOptions | None,
132
+ ) -> _Prepared:
133
+ operation = CONTRACT["operations"][operation_id]
134
+ if options is not None and not isinstance(options, RequestOptions):
135
+ raise ValueError("options must be a RequestOptions instance.")
136
+ options = options or RequestOptions()
137
+ if options.idempotency_key is not None and not operation["idempotency"]:
138
+ raise ValueError("This operation does not accept an idempotency key.")
139
+ path = operation["path"]
140
+ if set(path_params) != set(operation["pathParams"]):
141
+ raise ValueError("The operation received invalid path parameters.")
142
+ for name, value in path_params.items():
143
+ if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", value):
144
+ raise ValueError("Resource identifiers must be non-empty URL-safe strings.")
145
+ path = path.replace("{" + name + "}", quote(value, safe=""))
146
+ if params is None:
147
+ if operation.get("body_required"):
148
+ raise ValueError("This operation requires request parameters.")
149
+ snapshot: dict[str, Any] = {}
150
+ elif isinstance(params, Mapping):
151
+ snapshot = json.loads(serialize_params(params))
152
+ else:
153
+ raise ValueError("params must be a mapping of JSON field names to values.")
154
+ if (
155
+ snapshot.get("after") is not None
156
+ and snapshot.get("before") is not None
157
+ and operation["paginated"]
158
+ ):
159
+ raise ValueError("after and before cannot be used together.")
160
+ headers = {
161
+ "Authorization": "Bearer " + (options.api_key or self._api_key),
162
+ "Accept": "application/json",
163
+ "User-Agent": "raims-python/" + VERSION,
164
+ }
165
+ if options.idempotency_key is not None:
166
+ headers["Idempotency-Key"] = options.idempotency_key
167
+ body = None
168
+ if operation["body"]:
169
+ body = serialize_params(snapshot)
170
+ if operation.get("maxBodyBytes") is not None and len(body) > operation["maxBodyBytes"]:
171
+ raise ValueError("Request body exceeds the published size limit.")
172
+ headers["Content-Type"] = "application/json"
173
+ else:
174
+ query: list[tuple[str, str]] = []
175
+ for name, value in snapshot.items():
176
+ if name not in operation["query"]:
177
+ raise ValueError(
178
+ "This operation received an unsupported query parameter or body."
179
+ )
180
+ if value is None:
181
+ continue
182
+ for item in value if isinstance(value, list) else [value]:
183
+ if type(item) not in (str, int, float, bool):
184
+ raise ValueError("Query values must be scalars or lists of scalars.")
185
+ query.append((name, str(item).lower() if isinstance(item, bool) else str(item)))
186
+ if query:
187
+ path += "?" + urlencode(query)
188
+ retries = (
189
+ options.max_network_retries
190
+ if options.max_network_retries is not None
191
+ else self._retries
192
+ )
193
+ return _Prepared(
194
+ operation,
195
+ dict(path_params),
196
+ snapshot,
197
+ options,
198
+ self._base + path,
199
+ headers,
200
+ body,
201
+ options.timeout if options.timeout is not None else self._timeout,
202
+ retries if operation["method"] in ("GET", "HEAD") else 0,
203
+ )
204
+
205
+ def _http_request(self, prepared: _Prepared) -> httpx.Request:
206
+ timeout = httpx.Timeout(
207
+ prepared.timeout, connect=min(prepared.timeout, self._connect_timeout)
208
+ )
209
+ return httpx.Request(
210
+ prepared.operation["method"],
211
+ prepared.url,
212
+ headers=prepared.headers,
213
+ content=prepared.body,
214
+ extensions={"timeout": timeout.as_dict()},
215
+ )
216
+
217
+ def _notify(
218
+ self,
219
+ prepared: _Prepared,
220
+ attempt: int,
221
+ start: float,
222
+ response: LastResponse | None,
223
+ delay: float | None,
224
+ ) -> None:
225
+ if self._observer is None or self._notifying.get():
226
+ return
227
+ token = self._notifying.set(True)
228
+ try:
229
+ self._observer(
230
+ RequestDiagnostics(
231
+ prepared.operation["id"],
232
+ prepared.operation["method"],
233
+ prepared.operation["path"],
234
+ attempt + 1,
235
+ (time.perf_counter() - start) * 1000,
236
+ response.status_code if response else None,
237
+ response.request_id if response else None,
238
+ delay is not None,
239
+ delay * 1000 if delay is not None else None,
240
+ )
241
+ )
242
+ except Exception: # noqa: BLE001, S110
243
+ pass # Observer failures must not change the result of financial requests.
244
+ finally:
245
+ self._notifying.reset(token)
246
+
247
+ def _interpret(self, prepared: _Prepared, response: LastResponse, *, asynchronous: bool) -> Any:
248
+ status = response.status_code
249
+ if 300 <= status < 400:
250
+ raise errors.UnexpectedResponseError(
251
+ "The API returned a redirect; redirects are not followed.", last_response=response
252
+ )
253
+ if not 200 <= status < 300:
254
+ api_error = None
255
+ try:
256
+ data = parse_json(response.body)
257
+ if isinstance(data, dict) and isinstance(data.get("error"), dict):
258
+ api_error = data["error"]
259
+ except (ValueError, UnicodeError, RecursionError):
260
+ pass
261
+ category = {
262
+ 400: errors.InvalidRequestError,
263
+ 405: errors.InvalidRequestError,
264
+ 413: errors.InvalidRequestError,
265
+ 415: errors.InvalidRequestError,
266
+ 422: errors.InvalidRequestError,
267
+ 401: errors.AuthenticationError,
268
+ 403: errors.PermissionError,
269
+ 404: errors.NotFoundError,
270
+ 409: errors.ConflictError,
271
+ 429: errors.RateLimitError,
272
+ }.get(status, errors.ApiError)
273
+ raise category(
274
+ f"The API request failed with HTTP {status}.",
275
+ last_response=response,
276
+ api_error=api_error,
277
+ )
278
+ responses = prepared.operation["responses"]
279
+ if str(status) not in responses:
280
+ raise errors.UnexpectedResponseError(
281
+ "The API returned an undocumented success status.", last_response=response
282
+ )
283
+ schema = responses[str(status)]
284
+ if schema is None:
285
+ if response.body:
286
+ raise errors.UnexpectedResponseError(
287
+ "The API returned a body for an empty response.", last_response=response
288
+ )
289
+ return None
290
+ content_type = response.headers.get("content-type")
291
+ if content_type and not re.match(
292
+ r"application/(?:[a-z0-9.+-]+\+)?json(?:\s*;|$)", content_type, re.IGNORECASE
293
+ ):
294
+ raise errors.UnexpectedResponseError(
295
+ "The API returned a non-JSON success response.", last_response=response
296
+ )
297
+ try:
298
+ data = parse_json(response.body)
299
+ except (ValueError, UnicodeError, RecursionError):
300
+ raise errors.UnexpectedResponseError(
301
+ "The API returned invalid JSON.", last_response=response
302
+ ) from None
303
+ validate_response(data, schema, response)
304
+ resource = hydrate(data, schema, response)
305
+ if prepared.operation["paginated"]:
306
+ page_type = AsyncApiList if asynchronous else ApiList
307
+ return page_type(
308
+ resource,
309
+ self,
310
+ prepared.operation["id"],
311
+ prepared.path_params,
312
+ prepared.params,
313
+ prepared.options,
314
+ )
315
+ return resource
316
+
317
+ def _check_size(self, size: int, status: int, headers: Mapping[str, str]) -> None:
318
+ if size > _MAX_RESPONSE_BYTES:
319
+ raise errors.UnexpectedResponseError(
320
+ "The API response exceeds the supported size.",
321
+ last_response=LastResponse(status, headers, b""),
322
+ )
323
+
324
+
325
+ @dataclass(frozen=True)
326
+ class Simulations:
327
+ charges: services.SimulationChargesService
328
+ refunds: services.SimulationRefundsService
329
+
330
+
331
+ @dataclass(frozen=True)
332
+ class AsyncSimulations:
333
+ charges: services.AsyncSimulationChargesService
334
+ refunds: services.AsyncSimulationRefundsService
335
+
336
+
337
+ class RaimsClient(_ClientBase):
338
+ def __init__(
339
+ self,
340
+ api_key: str,
341
+ *,
342
+ api_base: str = "https://api.raims.com",
343
+ timeout: float = 80.0,
344
+ connect_timeout: float = 10.0,
345
+ max_network_retries: int = 0,
346
+ transport: httpx.BaseTransport | None = None,
347
+ request_observer: RequestObserver | None = None,
348
+ ) -> None:
349
+ super().__init__(
350
+ api_key,
351
+ api_base=api_base,
352
+ timeout=timeout,
353
+ connect_timeout=connect_timeout,
354
+ max_network_retries=max_network_retries,
355
+ request_observer=request_observer,
356
+ )
357
+ self._http = httpx.Client(transport=transport, trust_env=False, follow_redirects=False)
358
+ self.accounts = services.AccountsService(self)
359
+ self.balances = services.BalancesService(self)
360
+ self.customers = services.CustomersService(self)
361
+ self.charges = services.ChargesService(self)
362
+ self.payment_links = services.PaymentLinksService(self)
363
+ self.refunds = services.RefundsService(self)
364
+ self.disputes = services.DisputesService(self)
365
+ self.statements = services.StatementsService(self)
366
+ self.withdrawals = services.WithdrawalsService(self)
367
+ self.withdrawal_destinations = services.WithdrawalDestinationsService(self)
368
+ self.webhooks = services.WebhooksService(self)
369
+ self.webhook_event_types = services.WebhookEventTypesService(self)
370
+ self.simulations = Simulations(
371
+ services.SimulationChargesService(self), services.SimulationRefundsService(self)
372
+ )
373
+
374
+ def _request(
375
+ self,
376
+ operation_id: str,
377
+ *,
378
+ path_params: dict[str, str],
379
+ params: Mapping[str, Any] | None,
380
+ options: RequestOptions | None,
381
+ ) -> Any:
382
+ if self._http.is_closed:
383
+ raise RuntimeError("The API client is closed.")
384
+ prepared = self._prepare(operation_id, path_params, params, options)
385
+ for attempt in range(prepared.retries + 1):
386
+ start = time.perf_counter()
387
+ info = None
388
+ try:
389
+ response = self._http.send(
390
+ self._http_request(prepared), stream=True, follow_redirects=False
391
+ )
392
+ info = LastResponse(response.status_code, response.headers, b"")
393
+ try:
394
+ chunks = bytearray()
395
+ for chunk in response.iter_bytes():
396
+ self._check_size(
397
+ len(chunks) + len(chunk), response.status_code, response.headers
398
+ )
399
+ chunks.extend(chunk)
400
+ info = LastResponse(response.status_code, response.headers, bytes(chunks))
401
+ finally:
402
+ response.close()
403
+ except httpx.DecodingError:
404
+ self._notify(prepared, attempt, start, info, None)
405
+ raise errors.UnexpectedResponseError(
406
+ "The API returned invalid compressed content.", last_response=info
407
+ ) from None
408
+ except httpx.TransportError:
409
+ delay = _retry_delay(attempt, info) if attempt < prepared.retries else None
410
+ self._notify(prepared, attempt, start, info, delay)
411
+ if delay is None:
412
+ raise errors.ConnectionError(
413
+ "The API connection failed or timed out.", last_response=info
414
+ ) from None
415
+ time.sleep(delay)
416
+ continue
417
+ except errors.UnexpectedResponseError as exc:
418
+ self._notify(prepared, attempt, start, exc.last_response, None)
419
+ raise
420
+ delay = (
421
+ _retry_delay(attempt, info)
422
+ if attempt < prepared.retries and info.status_code in _RETRY_STATUSES
423
+ else None
424
+ )
425
+ self._notify(prepared, attempt, start, info, delay)
426
+ if delay is not None:
427
+ time.sleep(delay)
428
+ continue
429
+ return self._interpret(prepared, info, asynchronous=False)
430
+ raise AssertionError("Unreachable retry state.")
431
+
432
+ def close(self) -> None:
433
+ self._http.close()
434
+
435
+ def __enter__(self: _SyncClientT) -> _SyncClientT: # noqa: PYI019
436
+ self._http.__enter__()
437
+ return self
438
+
439
+ def __exit__(
440
+ self,
441
+ exc_type: type[BaseException] | None,
442
+ exc_value: BaseException | None,
443
+ traceback: TracebackType | None,
444
+ ) -> None:
445
+ self._http.__exit__(exc_type, exc_value, traceback)
446
+
447
+
448
+ class AsyncRaimsClient(_ClientBase):
449
+ def __init__(
450
+ self,
451
+ api_key: str,
452
+ *,
453
+ api_base: str = "https://api.raims.com",
454
+ timeout: float = 80.0,
455
+ connect_timeout: float = 10.0,
456
+ max_network_retries: int = 0,
457
+ transport: httpx.AsyncBaseTransport | None = None,
458
+ request_observer: RequestObserver | None = None,
459
+ ) -> None:
460
+ super().__init__(
461
+ api_key,
462
+ api_base=api_base,
463
+ timeout=timeout,
464
+ connect_timeout=connect_timeout,
465
+ max_network_retries=max_network_retries,
466
+ request_observer=request_observer,
467
+ )
468
+ self._http = httpx.AsyncClient(transport=transport, trust_env=False, follow_redirects=False)
469
+ self.accounts = services.AsyncAccountsService(self)
470
+ self.balances = services.AsyncBalancesService(self)
471
+ self.customers = services.AsyncCustomersService(self)
472
+ self.charges = services.AsyncChargesService(self)
473
+ self.payment_links = services.AsyncPaymentLinksService(self)
474
+ self.refunds = services.AsyncRefundsService(self)
475
+ self.disputes = services.AsyncDisputesService(self)
476
+ self.statements = services.AsyncStatementsService(self)
477
+ self.withdrawals = services.AsyncWithdrawalsService(self)
478
+ self.withdrawal_destinations = services.AsyncWithdrawalDestinationsService(self)
479
+ self.webhooks = services.AsyncWebhooksService(self)
480
+ self.webhook_event_types = services.AsyncWebhookEventTypesService(self)
481
+ self.simulations = AsyncSimulations(
482
+ services.AsyncSimulationChargesService(self),
483
+ services.AsyncSimulationRefundsService(self),
484
+ )
485
+
486
+ async def _request(
487
+ self,
488
+ operation_id: str,
489
+ *,
490
+ path_params: dict[str, str],
491
+ params: Mapping[str, Any] | None,
492
+ options: RequestOptions | None,
493
+ ) -> Any:
494
+ if self._http.is_closed:
495
+ raise RuntimeError("The API client is closed.")
496
+ prepared = self._prepare(operation_id, path_params, params, options)
497
+ for attempt in range(prepared.retries + 1):
498
+ start = time.perf_counter()
499
+ info = None
500
+ try:
501
+ response = await self._http.send(
502
+ self._http_request(prepared), stream=True, follow_redirects=False
503
+ )
504
+ info = LastResponse(response.status_code, response.headers, b"")
505
+ try:
506
+ chunks = bytearray()
507
+ async for chunk in response.aiter_bytes():
508
+ self._check_size(
509
+ len(chunks) + len(chunk), response.status_code, response.headers
510
+ )
511
+ chunks.extend(chunk)
512
+ info = LastResponse(response.status_code, response.headers, bytes(chunks))
513
+ finally:
514
+ await response.aclose()
515
+ except httpx.DecodingError:
516
+ self._notify(prepared, attempt, start, info, None)
517
+ raise errors.UnexpectedResponseError(
518
+ "The API returned invalid compressed content.", last_response=info
519
+ ) from None
520
+ except httpx.TransportError:
521
+ delay = _retry_delay(attempt, info) if attempt < prepared.retries else None
522
+ self._notify(prepared, attempt, start, info, delay)
523
+ if delay is None:
524
+ raise errors.ConnectionError(
525
+ "The API connection failed or timed out.", last_response=info
526
+ ) from None
527
+ await asyncio.sleep(delay)
528
+ continue
529
+ except errors.UnexpectedResponseError as exc:
530
+ self._notify(prepared, attempt, start, exc.last_response, None)
531
+ raise
532
+ delay = (
533
+ _retry_delay(attempt, info)
534
+ if attempt < prepared.retries and info.status_code in _RETRY_STATUSES
535
+ else None
536
+ )
537
+ self._notify(prepared, attempt, start, info, delay)
538
+ if delay is not None:
539
+ await asyncio.sleep(delay)
540
+ continue
541
+ return self._interpret(prepared, info, asynchronous=True)
542
+ raise AssertionError("Unreachable retry state.")
543
+
544
+ async def close(self) -> None:
545
+ await self._http.aclose()
546
+
547
+ async def aclose(self) -> None:
548
+ await self.close()
549
+
550
+ async def __aenter__(self: _AsyncClientT) -> _AsyncClientT: # noqa: PYI019
551
+ await self._http.__aenter__()
552
+ return self
553
+
554
+ async def __aexit__(
555
+ self,
556
+ exc_type: type[BaseException] | None,
557
+ exc_value: BaseException | None,
558
+ traceback: TracebackType | None,
559
+ ) -> None:
560
+ await self._http.__aexit__(exc_type, exc_value, traceback)
raims/_errors.py ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ._response import LastResponse
6
+
7
+
8
+ class RaimsError(Exception):
9
+ """Base for failures reported by the SDK."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ last_response: LastResponse | None = None,
16
+ api_error: dict[str, Any] | None = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.last_response = last_response
20
+ self.api_error = api_error
21
+
22
+ @property
23
+ def status_code(self) -> int | None:
24
+ return self.last_response.status_code if self.last_response else None
25
+
26
+ @property
27
+ def request_id(self) -> str | None:
28
+ return self.last_response.request_id if self.last_response else None
29
+
30
+ @property
31
+ def code(self) -> str | None:
32
+ value = self.api_error.get("code") if self.api_error else None
33
+ return value if isinstance(value, str) else None
34
+
35
+ @property
36
+ def retry_after(self) -> str | None:
37
+ return self.last_response.retry_after if self.last_response else None
38
+
39
+ def __repr__(self) -> str:
40
+ return f"{type(self).__name__}(status_code={self.status_code!r})"
41
+
42
+
43
+ class ApiError(RaimsError):
44
+ pass
45
+
46
+
47
+ class AuthenticationError(ApiError):
48
+ pass
49
+
50
+
51
+ class PermissionError(ApiError):
52
+ pass
53
+
54
+
55
+ class NotFoundError(ApiError):
56
+ pass
57
+
58
+
59
+ class ConflictError(ApiError):
60
+ pass
61
+
62
+
63
+ class RateLimitError(ApiError):
64
+ pass
65
+
66
+
67
+ class InvalidRequestError(ApiError):
68
+ pass
69
+
70
+
71
+ class ConnectionError(RaimsError):
72
+ pass
73
+
74
+
75
+ class UnexpectedResponseError(RaimsError):
76
+ pass
77
+
78
+
79
+ class SignatureVerificationError(RaimsError):
80
+ pass