mailinfra 0.1.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.
- mailinfra/__init__.py +124 -0
- mailinfra/_errors.py +343 -0
- mailinfra/_generated/__init__.py +1 -0
- mailinfra/_generated/models.py +288 -0
- mailinfra/_transport.py +194 -0
- mailinfra/_version.py +11 -0
- mailinfra/client.py +657 -0
- mailinfra/models.py +146 -0
- mailinfra/py.typed +0 -0
- mailinfra/resources/__init__.py +13 -0
- mailinfra/resources/emails.py +882 -0
- mailinfra/webhooks.py +307 -0
- mailinfra-0.1.0.dist-info/METADATA +307 -0
- mailinfra-0.1.0.dist-info/RECORD +16 -0
- mailinfra-0.1.0.dist-info/WHEEL +4 -0
- mailinfra-0.1.0.dist-info/licenses/LICENSE +21 -0
mailinfra/__init__.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Официальный Python SDK MailInfra.
|
|
2
|
+
|
|
3
|
+
Быстрый старт::
|
|
4
|
+
|
|
5
|
+
from mailinfra import MailInfra
|
|
6
|
+
|
|
7
|
+
client = MailInfra() # ключ из MAILINFRA_API_KEY
|
|
8
|
+
result = client.emails.send(
|
|
9
|
+
from_email="hello@mail.example.ru",
|
|
10
|
+
to="user@example.com",
|
|
11
|
+
subject="Добро пожаловать!",
|
|
12
|
+
html="<p>Привет!</p>",
|
|
13
|
+
)
|
|
14
|
+
print(result.id, result.status)
|
|
15
|
+
|
|
16
|
+
Что клиент делает за вас:
|
|
17
|
+
|
|
18
|
+
* ставит `Idempotency-Key` на каждую отправку и переиспользует его при
|
|
19
|
+
повторах — сбой сети не превращается в два письма;
|
|
20
|
+
* повторяет `429`/`5xx`/таймауты с экспоненциальным backoff и уважает
|
|
21
|
+
`Retry-After`;
|
|
22
|
+
* поднимает ошибки API типизированными исключениями
|
|
23
|
+
(`RateLimitError`, `DomainNotVerifiedError`, …);
|
|
24
|
+
* валидирует письмо локально по OpenAPI-спеке до сетевого вызова;
|
|
25
|
+
* проверяет подписи вебхуков (`mailinfra.webhooks`).
|
|
26
|
+
|
|
27
|
+
Покрытие v1 — публичный API писем (`/v1/emails*`). Управление доменами,
|
|
28
|
+
шаблонами, вебхуками и ключами — Management API на сессионном токене, оно в
|
|
29
|
+
SDK пока не входит.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from . import webhooks
|
|
35
|
+
from ._errors import (
|
|
36
|
+
AllRecipientsSuppressedError,
|
|
37
|
+
APIConnectionError,
|
|
38
|
+
APITimeoutError,
|
|
39
|
+
AuthenticationError,
|
|
40
|
+
BadRequestError,
|
|
41
|
+
ConflictError,
|
|
42
|
+
DomainNotAttachedError,
|
|
43
|
+
DomainNotVerifiedError,
|
|
44
|
+
IdempotencyInFlightError,
|
|
45
|
+
LegalDocumentsNotAcceptedError,
|
|
46
|
+
MailInfraError,
|
|
47
|
+
NotFoundError,
|
|
48
|
+
PermissionDeniedError,
|
|
49
|
+
RateLimitError,
|
|
50
|
+
ReservedEmailHeaderError,
|
|
51
|
+
SandboxRecipientNotAllowedError,
|
|
52
|
+
SandboxTrialExhaustedError,
|
|
53
|
+
ServerError,
|
|
54
|
+
TemplateRenderError,
|
|
55
|
+
TemplateVariableMissingError,
|
|
56
|
+
UnprocessableError,
|
|
57
|
+
)
|
|
58
|
+
from ._transport import RetryPolicy
|
|
59
|
+
from ._version import __version__
|
|
60
|
+
from .client import AsyncMailInfra, MailInfra
|
|
61
|
+
from .models import (
|
|
62
|
+
Attachment,
|
|
63
|
+
BatchSendError,
|
|
64
|
+
BatchSendResponseData,
|
|
65
|
+
BatchSendResult,
|
|
66
|
+
BounceCategory,
|
|
67
|
+
BounceType,
|
|
68
|
+
DkimSigningStatus,
|
|
69
|
+
EmailDetail,
|
|
70
|
+
EmailEventPublic,
|
|
71
|
+
EmailEventType,
|
|
72
|
+
EmailListItem,
|
|
73
|
+
EmailSource,
|
|
74
|
+
EmailStatus,
|
|
75
|
+
SendEmailResult,
|
|
76
|
+
SuppressionReason,
|
|
77
|
+
)
|
|
78
|
+
from .webhooks import WebhookEvent, WebhookSignatureError
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"AllRecipientsSuppressedError",
|
|
82
|
+
"APIConnectionError",
|
|
83
|
+
"APITimeoutError",
|
|
84
|
+
"AsyncMailInfra",
|
|
85
|
+
"Attachment",
|
|
86
|
+
"AuthenticationError",
|
|
87
|
+
"BadRequestError",
|
|
88
|
+
"BatchSendError",
|
|
89
|
+
"BatchSendResponseData",
|
|
90
|
+
"BatchSendResult",
|
|
91
|
+
"BounceCategory",
|
|
92
|
+
"BounceType",
|
|
93
|
+
"ConflictError",
|
|
94
|
+
"DkimSigningStatus",
|
|
95
|
+
"DomainNotAttachedError",
|
|
96
|
+
"DomainNotVerifiedError",
|
|
97
|
+
"EmailDetail",
|
|
98
|
+
"EmailEventPublic",
|
|
99
|
+
"EmailEventType",
|
|
100
|
+
"EmailListItem",
|
|
101
|
+
"EmailSource",
|
|
102
|
+
"EmailStatus",
|
|
103
|
+
"IdempotencyInFlightError",
|
|
104
|
+
"LegalDocumentsNotAcceptedError",
|
|
105
|
+
"MailInfra",
|
|
106
|
+
"MailInfraError",
|
|
107
|
+
"NotFoundError",
|
|
108
|
+
"PermissionDeniedError",
|
|
109
|
+
"RateLimitError",
|
|
110
|
+
"ReservedEmailHeaderError",
|
|
111
|
+
"RetryPolicy",
|
|
112
|
+
"SandboxRecipientNotAllowedError",
|
|
113
|
+
"SandboxTrialExhaustedError",
|
|
114
|
+
"SendEmailResult",
|
|
115
|
+
"ServerError",
|
|
116
|
+
"SuppressionReason",
|
|
117
|
+
"TemplateRenderError",
|
|
118
|
+
"TemplateVariableMissingError",
|
|
119
|
+
"UnprocessableError",
|
|
120
|
+
"WebhookEvent",
|
|
121
|
+
"WebhookSignatureError",
|
|
122
|
+
"__version__",
|
|
123
|
+
"webhooks",
|
|
124
|
+
]
|
mailinfra/_errors.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Типизированные исключения поверх конверта ошибок MailInfra.
|
|
2
|
+
|
|
3
|
+
API отдаёт любую ошибку одной формой (см. https://docs.mailinfra.ru/errors)::
|
|
4
|
+
|
|
5
|
+
{"error": {"code": "domain_not_verified", "message": "…", "details": {}},
|
|
6
|
+
"request_id": "3d6f0dce…"}
|
|
7
|
+
|
|
8
|
+
`code` — стабильный машинный код, его и надо ветвить в коде. Классы ниже
|
|
9
|
+
подобраны так, чтобы ловить можно было на трёх уровнях: всё
|
|
10
|
+
(`MailInfraError`), категория HTTP-статуса (`NotFoundError`) или конкретная
|
|
11
|
+
причина (`DomainNotVerifiedError`). Неизвестный код деградирует до класса по
|
|
12
|
+
статусу: новый код на бэкенде не роняет старый SDK.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AllRecipientsSuppressedError",
|
|
21
|
+
"APIConnectionError",
|
|
22
|
+
"APITimeoutError",
|
|
23
|
+
"AuthenticationError",
|
|
24
|
+
"BadRequestError",
|
|
25
|
+
"ConflictError",
|
|
26
|
+
"DomainNotAttachedError",
|
|
27
|
+
"DomainNotVerifiedError",
|
|
28
|
+
"IdempotencyInFlightError",
|
|
29
|
+
"LegalDocumentsNotAcceptedError",
|
|
30
|
+
"MailInfraError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"PermissionDeniedError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"ReservedEmailHeaderError",
|
|
35
|
+
"SandboxRecipientNotAllowedError",
|
|
36
|
+
"SandboxTrialExhaustedError",
|
|
37
|
+
"ServerError",
|
|
38
|
+
"TemplateRenderError",
|
|
39
|
+
"TemplateVariableMissingError",
|
|
40
|
+
"UnprocessableError",
|
|
41
|
+
"error_from_response",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MailInfraError(Exception):
|
|
46
|
+
"""Базовая ошибка SDK. Ловите её, если детали не важны.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
message: Человеко-читаемое сообщение от API (на русском).
|
|
50
|
+
status_code: HTTP-статус ответа; `None` для сетевых ошибок.
|
|
51
|
+
code: Машинный код ошибки (`error.code`).
|
|
52
|
+
details: Безопасные структурированные детали (`error.details`).
|
|
53
|
+
request_id: Идентификатор запроса для поддержки (`request_id`,
|
|
54
|
+
он же заголовок `X-Request-ID`).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: str,
|
|
60
|
+
*,
|
|
61
|
+
status_code: int | None = None,
|
|
62
|
+
code: str = "unknown_error",
|
|
63
|
+
details: dict[str, Any] | None = None,
|
|
64
|
+
request_id: str | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
super().__init__(message)
|
|
67
|
+
self.message = message
|
|
68
|
+
self.status_code = status_code
|
|
69
|
+
self.code = code
|
|
70
|
+
self.details: dict[str, Any] = details or {}
|
|
71
|
+
self.request_id = request_id
|
|
72
|
+
|
|
73
|
+
def __str__(self) -> str:
|
|
74
|
+
parts = [self.message]
|
|
75
|
+
if self.code and self.code != "unknown_error":
|
|
76
|
+
parts.append(f"code={self.code}")
|
|
77
|
+
if self.status_code is not None:
|
|
78
|
+
parts.append(f"status={self.status_code}")
|
|
79
|
+
if self.request_id:
|
|
80
|
+
parts.append(f"request_id={self.request_id}")
|
|
81
|
+
return " ".join(parts) if len(parts) == 1 else f"{parts[0]} ({', '.join(parts[1:])})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------- Транспортные ошибки (ответа от API не было) ----------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class APIConnectionError(MailInfraError):
|
|
88
|
+
"""Не удалось получить ответ: DNS, TCP, TLS, обрыв соединения."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, message: str = "Не удалось соединиться с API MailInfra") -> None:
|
|
91
|
+
super().__init__(message, code="api_connection_error")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class APITimeoutError(APIConnectionError):
|
|
95
|
+
"""Запрос не успел уложиться в таймаут."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, message: str = "Истёк таймаут запроса к API MailInfra") -> None:
|
|
98
|
+
MailInfraError.__init__(self, message, code="api_timeout")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------- Ошибки по HTTP-статусу ----------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class BadRequestError(MailInfraError):
|
|
105
|
+
"""`400` — запрос некорректен."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AuthenticationError(MailInfraError):
|
|
109
|
+
"""`401` — API-ключ отсутствует, невалиден или отозван."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PermissionDeniedError(MailInfraError):
|
|
113
|
+
"""`403` — у ключа нет прав на операцию.
|
|
114
|
+
|
|
115
|
+
Типичная причина: ключ выпущен с правами `READ_ONLY` (тогда отправка
|
|
116
|
+
запрещена) или `SENDING_ONLY` (тогда запрещено чтение).
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class NotFoundError(MailInfraError):
|
|
121
|
+
"""`404` — ресурс не найден или принадлежит другому проекту."""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ConflictError(MailInfraError):
|
|
125
|
+
"""`409` — конфликт состояния."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class UnprocessableError(MailInfraError):
|
|
129
|
+
"""`422` — не прошла валидация полей.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
fields: Список `{"field", "message", "code"}` из `details.fields`,
|
|
133
|
+
если бэкенд его прислал (код `request_validation_failed`).
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def fields(self) -> list[dict[str, Any]]:
|
|
138
|
+
"""Пер-полевые причины отказа.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Список описаний полей; пустой, если детализации нет.
|
|
142
|
+
"""
|
|
143
|
+
raw = self.details.get("fields")
|
|
144
|
+
return [item for item in raw if isinstance(item, dict)] if isinstance(raw, list) else []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class RateLimitError(MailInfraError):
|
|
148
|
+
"""`429` — исчерпан лимит отправки: месячный объём или дневной потолок.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
retry_after: Секунды до безопасного повтора из заголовка
|
|
152
|
+
`Retry-After`; `None`, если заголовка не было.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
message: str,
|
|
158
|
+
*,
|
|
159
|
+
status_code: int | None = 429,
|
|
160
|
+
code: str = "rate_limit_exceeded",
|
|
161
|
+
details: dict[str, Any] | None = None,
|
|
162
|
+
request_id: str | None = None,
|
|
163
|
+
retry_after: int | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
super().__init__(
|
|
166
|
+
message,
|
|
167
|
+
status_code=status_code,
|
|
168
|
+
code=code,
|
|
169
|
+
details=details,
|
|
170
|
+
request_id=request_id,
|
|
171
|
+
)
|
|
172
|
+
self.retry_after = retry_after
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ServerError(MailInfraError):
|
|
176
|
+
"""`5xx` — ошибка на стороне MailInfra. Такие запросы SDK ретраит сам."""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------- Ошибки по конкретному `error.code` ----------
|
|
180
|
+
#
|
|
181
|
+
# Базовый класс каждой — класс того HTTP-статуса, с которым бэкенд реально
|
|
182
|
+
# отдаёт код (см. `app.services.email_service`): `except UnprocessableError`
|
|
183
|
+
# обязан ловить и `DomainNotVerifiedError`, иначе документированный приём
|
|
184
|
+
# «ловить по категории» молча пропускает половину ошибок отправки.
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class DomainNotVerifiedError(UnprocessableError):
|
|
188
|
+
"""`422` — домен отправителя ещё не подтверждён (DNS-записи не сошлись)."""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class DomainNotAttachedError(UnprocessableError):
|
|
192
|
+
"""`422` — домен не привязан к проекту, которому принадлежит ключ."""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class AllRecipientsSuppressedError(UnprocessableError):
|
|
196
|
+
"""`422` — все получатели в списке подавления, отправлять некому."""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class SandboxRecipientNotAllowedError(UnprocessableError):
|
|
200
|
+
"""`422` — пробное письмо с sandbox-домена (LIVE-ключ) уходит только владельцу ключа."""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class SandboxTrialExhaustedError(PermissionDeniedError):
|
|
204
|
+
"""`403` — пробная sandbox-отправка уже израсходована; нужен свой домен."""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TemplateRenderError(UnprocessableError):
|
|
208
|
+
"""`422` — шаблон не отрендерился."""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TemplateVariableMissingError(UnprocessableError):
|
|
212
|
+
"""`422` — в `variables` не хватает переменной, которую требует шаблон."""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class ReservedEmailHeaderError(UnprocessableError):
|
|
216
|
+
"""`422` — попытка переопределить системный заголовок письма."""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class IdempotencyInFlightError(ConflictError):
|
|
220
|
+
"""`409` — запрос с таким `Idempotency-Key` ещё выполняется, повторите позже."""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class LegalDocumentsNotAcceptedError(UnprocessableError):
|
|
224
|
+
"""`422` — не приняты обязательные юридические документы.
|
|
225
|
+
|
|
226
|
+
Политика, оферта или DPA ждут согласия владельца организации, до этого
|
|
227
|
+
отправка заблокирована. В `details["pending"]` — список документов,
|
|
228
|
+
которые нужно принять.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
_STATUS_ERRORS: dict[int, type[MailInfraError]] = {
|
|
233
|
+
400: BadRequestError,
|
|
234
|
+
401: AuthenticationError,
|
|
235
|
+
403: PermissionDeniedError,
|
|
236
|
+
404: NotFoundError,
|
|
237
|
+
409: ConflictError,
|
|
238
|
+
422: UnprocessableError,
|
|
239
|
+
429: RateLimitError,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Коды, которые бэкенд кладёт в `details.code` при отказе отправки.
|
|
243
|
+
# Источник истины — места `raise` в `app.services.{email_service,
|
|
244
|
+
# template_service, idempotency, rate_limit}`: только они говорят, какой код
|
|
245
|
+
# реально выбрасывается и с каким статусом.
|
|
246
|
+
_CODE_ERRORS: dict[str, type[MailInfraError]] = {
|
|
247
|
+
"all_recipients_suppressed": AllRecipientsSuppressedError,
|
|
248
|
+
"domain_not_attached": DomainNotAttachedError,
|
|
249
|
+
"domain_not_verified": DomainNotVerifiedError,
|
|
250
|
+
"idempotency_in_flight": IdempotencyInFlightError,
|
|
251
|
+
"legal_documents_not_accepted": LegalDocumentsNotAcceptedError,
|
|
252
|
+
"reserved_email_header": ReservedEmailHeaderError,
|
|
253
|
+
"rps_exceeded": RateLimitError,
|
|
254
|
+
"sandbox_recipient_not_allowed": SandboxRecipientNotAllowedError,
|
|
255
|
+
"sandbox_trial_exhausted": SandboxTrialExhaustedError,
|
|
256
|
+
"template_render_error": TemplateRenderError,
|
|
257
|
+
"template_variable_missing": TemplateVariableMissingError,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _status_error_class(status_code: int) -> type[MailInfraError]:
|
|
262
|
+
"""Возвращает класс ошибки по HTTP-статусу.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
status_code: HTTP-статус ответа.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Класс исключения; `ServerError` для `5xx`, `MailInfraError` для
|
|
269
|
+
неожиданных статусов.
|
|
270
|
+
"""
|
|
271
|
+
if status_code in _STATUS_ERRORS:
|
|
272
|
+
return _STATUS_ERRORS[status_code]
|
|
273
|
+
if status_code >= 500:
|
|
274
|
+
return ServerError
|
|
275
|
+
return MailInfraError
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def error_from_response(
|
|
279
|
+
*,
|
|
280
|
+
status_code: int,
|
|
281
|
+
body: Any,
|
|
282
|
+
request_id: str | None,
|
|
283
|
+
retry_after: int | None = None,
|
|
284
|
+
) -> MailInfraError:
|
|
285
|
+
"""Собирает исключение из ответа API.
|
|
286
|
+
|
|
287
|
+
Устойчиво к не-JSON телу и к ответу не по контракту: в этом случае берётся
|
|
288
|
+
класс по статусу и общее сообщение — сеть и прокси умеют возвращать HTML.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
status_code: HTTP-статус ответа.
|
|
292
|
+
body: Разобранное тело ответа (обычно `dict`) или сырая строка.
|
|
293
|
+
request_id: Значение заголовка `X-Request-ID`, если есть.
|
|
294
|
+
retry_after: Разобранный `Retry-After` в секундах, если есть.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Экземпляр подходящего подкласса `MailInfraError`.
|
|
298
|
+
"""
|
|
299
|
+
code = "unknown_error"
|
|
300
|
+
message = f"Запрос завершился со статусом {status_code}"
|
|
301
|
+
details: dict[str, Any] = {}
|
|
302
|
+
|
|
303
|
+
if isinstance(body, dict):
|
|
304
|
+
envelope = body.get("error")
|
|
305
|
+
if isinstance(envelope, dict):
|
|
306
|
+
raw_code = envelope.get("code")
|
|
307
|
+
raw_message = envelope.get("message")
|
|
308
|
+
raw_details = envelope.get("details")
|
|
309
|
+
if isinstance(raw_code, str) and raw_code:
|
|
310
|
+
code = raw_code
|
|
311
|
+
if isinstance(raw_message, str) and raw_message:
|
|
312
|
+
message = raw_message
|
|
313
|
+
if isinstance(raw_details, dict):
|
|
314
|
+
details = raw_details
|
|
315
|
+
raw_request_id = body.get("request_id")
|
|
316
|
+
if request_id is None and isinstance(raw_request_id, str):
|
|
317
|
+
request_id = raw_request_id
|
|
318
|
+
|
|
319
|
+
# `details.code` уточняет причину внутри статуса (например, 429 →
|
|
320
|
+
# `rps_exceeded`), поэтому он приоритетнее верхнеуровневого кода.
|
|
321
|
+
detail_code = details.get("code")
|
|
322
|
+
if isinstance(detail_code, str) and detail_code in _CODE_ERRORS:
|
|
323
|
+
code = detail_code
|
|
324
|
+
|
|
325
|
+
error_class = _CODE_ERRORS.get(code) or _status_error_class(status_code)
|
|
326
|
+
|
|
327
|
+
if issubclass(error_class, RateLimitError):
|
|
328
|
+
return error_class(
|
|
329
|
+
message,
|
|
330
|
+
status_code=status_code,
|
|
331
|
+
code=code,
|
|
332
|
+
details=details,
|
|
333
|
+
request_id=request_id,
|
|
334
|
+
retry_after=retry_after,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return error_class(
|
|
338
|
+
message,
|
|
339
|
+
status_code=status_code,
|
|
340
|
+
code=code,
|
|
341
|
+
details=details,
|
|
342
|
+
request_id=request_id,
|
|
343
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Сгенерированные из OpenAPI модели. Не редактировать руками."""
|