sendstack 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.
- sendstack/__init__.py +32 -0
- sendstack/client.py +1790 -0
- sendstack/errors.py +43 -0
- sendstack/py.typed +1 -0
- sendstack/types.py +101 -0
- sendstack/utils.py +141 -0
- sendstack-0.1.0.dist-info/METADATA +725 -0
- sendstack-0.1.0.dist-info/RECORD +11 -0
- sendstack-0.1.0.dist-info/WHEEL +5 -0
- sendstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- sendstack-0.1.0.dist-info/top_level.txt +1 -0
sendstack/client.py
ADDED
|
@@ -0,0 +1,1790 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from .errors import MailerError, error_envelope_message, is_success_envelope
|
|
15
|
+
from .types import (
|
|
16
|
+
UNSET,
|
|
17
|
+
BearerAuthStrategy,
|
|
18
|
+
HeadersAuthStrategy,
|
|
19
|
+
MailerAuthStrategy,
|
|
20
|
+
MailerMiddleware,
|
|
21
|
+
MailerRequestContext,
|
|
22
|
+
MailerResponseContext,
|
|
23
|
+
MailerRetryContext,
|
|
24
|
+
RequestOptions,
|
|
25
|
+
ResponseParser,
|
|
26
|
+
ResponseTransformer,
|
|
27
|
+
RetryOptions,
|
|
28
|
+
)
|
|
29
|
+
from .utils import (
|
|
30
|
+
append_query_params,
|
|
31
|
+
as_mapping,
|
|
32
|
+
build_request_url,
|
|
33
|
+
merge_headers,
|
|
34
|
+
merge_query_params,
|
|
35
|
+
normalize_base_url,
|
|
36
|
+
parse_response_body,
|
|
37
|
+
prepare_request_body,
|
|
38
|
+
serialize_datetime,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
42
|
+
RETRYABLE_STATUS_CODES = {408, 425, 429, 500, 502, 503, 504}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _MailerBase:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str | None = None,
|
|
49
|
+
*,
|
|
50
|
+
base_url: str,
|
|
51
|
+
timeout_seconds: float | None = DEFAULT_TIMEOUT_SECONDS,
|
|
52
|
+
headers: Mapping[str, str] | httpx.Headers | None = None,
|
|
53
|
+
query: Mapping[str, object] | None = None,
|
|
54
|
+
auth: MailerAuthStrategy | bool | None = None,
|
|
55
|
+
retry: RetryOptions | int | bool | None = None,
|
|
56
|
+
middleware: Sequence[MailerMiddleware] | None = None,
|
|
57
|
+
parse_response: ResponseParser | None = None,
|
|
58
|
+
transform_response: ResponseTransformer | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
normalized_api_key = (api_key or "").strip()
|
|
61
|
+
self.api_key = normalized_api_key
|
|
62
|
+
self.base_url = normalize_base_url(base_url)
|
|
63
|
+
self.timeout_seconds = timeout_seconds
|
|
64
|
+
self._default_headers = headers
|
|
65
|
+
self._default_query = query
|
|
66
|
+
self._default_auth = (
|
|
67
|
+
auth
|
|
68
|
+
if auth is not None
|
|
69
|
+
else (
|
|
70
|
+
False
|
|
71
|
+
if normalized_api_key == ""
|
|
72
|
+
else HeadersAuthStrategy(headers={"x-api-key": normalized_api_key})
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
self._default_retry = retry
|
|
76
|
+
self._default_middleware = tuple(middleware or ())
|
|
77
|
+
self._default_parse_response = parse_response
|
|
78
|
+
self._default_transform_response = transform_response
|
|
79
|
+
|
|
80
|
+
def _effective_options(self, options: RequestOptions | None) -> RequestOptions:
|
|
81
|
+
return options if options is not None else RequestOptions()
|
|
82
|
+
|
|
83
|
+
def _build_request_context(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
attempt: int,
|
|
87
|
+
method: str,
|
|
88
|
+
path: str,
|
|
89
|
+
options: RequestOptions,
|
|
90
|
+
query: Mapping[str, object] | None,
|
|
91
|
+
body: object,
|
|
92
|
+
headers: httpx.Headers,
|
|
93
|
+
timeout_seconds: float | None,
|
|
94
|
+
) -> MailerRequestContext:
|
|
95
|
+
url = append_query_params(build_request_url(self.base_url, path), query)
|
|
96
|
+
return MailerRequestContext(
|
|
97
|
+
method=method.upper(),
|
|
98
|
+
path=path,
|
|
99
|
+
url=url,
|
|
100
|
+
headers=headers,
|
|
101
|
+
body=body,
|
|
102
|
+
timeout_seconds=timeout_seconds,
|
|
103
|
+
attempt=attempt,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _select_timeout(self, options: RequestOptions) -> float | None:
|
|
107
|
+
if options.timeout_seconds is not None:
|
|
108
|
+
return options.timeout_seconds
|
|
109
|
+
return self.timeout_seconds
|
|
110
|
+
|
|
111
|
+
def _select_parse_response(self, options: RequestOptions) -> ResponseParser | None:
|
|
112
|
+
return (
|
|
113
|
+
options.parse_response
|
|
114
|
+
if options.parse_response is not None
|
|
115
|
+
else self._default_parse_response
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _select_transform_response(self, options: RequestOptions) -> ResponseTransformer | None:
|
|
119
|
+
return (
|
|
120
|
+
options.transform_response
|
|
121
|
+
if options.transform_response is not None
|
|
122
|
+
else self._default_transform_response
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _select_retry(self, options: RequestOptions) -> RetryOptions | int | bool | None:
|
|
126
|
+
if options.retry is not None:
|
|
127
|
+
return options.retry
|
|
128
|
+
return self._default_retry
|
|
129
|
+
|
|
130
|
+
def _select_middleware(self, options: RequestOptions) -> tuple[MailerMiddleware, ...]:
|
|
131
|
+
return (*self._default_middleware, *(options.middleware or ()))
|
|
132
|
+
|
|
133
|
+
def _select_query(self, options: RequestOptions) -> dict[str, object] | None:
|
|
134
|
+
return merge_query_params(self._default_query, options.query)
|
|
135
|
+
|
|
136
|
+
def _fallback_context(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
attempt: int,
|
|
140
|
+
method: str,
|
|
141
|
+
path: str,
|
|
142
|
+
options: RequestOptions,
|
|
143
|
+
timeout_seconds: float | None,
|
|
144
|
+
) -> MailerRequestContext:
|
|
145
|
+
query = self._select_query(options)
|
|
146
|
+
headers = merge_headers(self._default_headers, options.headers)
|
|
147
|
+
return self._build_request_context(
|
|
148
|
+
attempt=attempt,
|
|
149
|
+
method=method,
|
|
150
|
+
path=path,
|
|
151
|
+
options=options,
|
|
152
|
+
query=query,
|
|
153
|
+
body=UNSET,
|
|
154
|
+
headers=headers,
|
|
155
|
+
timeout_seconds=timeout_seconds,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Mailer(_MailerBase):
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
api_key: str | None = None,
|
|
163
|
+
*,
|
|
164
|
+
base_url: str,
|
|
165
|
+
client: httpx.Client | Any | None = None,
|
|
166
|
+
timeout_seconds: float | None = DEFAULT_TIMEOUT_SECONDS,
|
|
167
|
+
headers: Mapping[str, str] | httpx.Headers | None = None,
|
|
168
|
+
query: Mapping[str, object] | None = None,
|
|
169
|
+
auth: MailerAuthStrategy | bool | None = None,
|
|
170
|
+
retry: RetryOptions | int | bool | None = None,
|
|
171
|
+
middleware: Sequence[MailerMiddleware] | None = None,
|
|
172
|
+
parse_response: ResponseParser | None = None,
|
|
173
|
+
transform_response: ResponseTransformer | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
super().__init__(
|
|
176
|
+
api_key,
|
|
177
|
+
base_url=base_url,
|
|
178
|
+
timeout_seconds=timeout_seconds,
|
|
179
|
+
headers=headers,
|
|
180
|
+
query=query,
|
|
181
|
+
auth=auth,
|
|
182
|
+
retry=retry,
|
|
183
|
+
middleware=middleware,
|
|
184
|
+
parse_response=parse_response,
|
|
185
|
+
transform_response=transform_response,
|
|
186
|
+
)
|
|
187
|
+
self._client = client or httpx.Client()
|
|
188
|
+
self._owns_client = client is None
|
|
189
|
+
self.emails = EmailOperations(self)
|
|
190
|
+
self.sms = SmsOperations(self)
|
|
191
|
+
self.whatsapp = WhatsAppOperations(self)
|
|
192
|
+
self.merchant = MerchantOperations(self)
|
|
193
|
+
self.domains = DomainOperations(self)
|
|
194
|
+
self.api_keys = ApiKeyOperations(self)
|
|
195
|
+
self.apiKeys = self.api_keys
|
|
196
|
+
self.webhooks = WebhookOperations(self)
|
|
197
|
+
self.health = HealthOperations(self)
|
|
198
|
+
|
|
199
|
+
def request(
|
|
200
|
+
self,
|
|
201
|
+
method: str,
|
|
202
|
+
path: str,
|
|
203
|
+
options: RequestOptions | None = None,
|
|
204
|
+
) -> object:
|
|
205
|
+
request_options = self._effective_options(options)
|
|
206
|
+
timeout_seconds = self._select_timeout(request_options)
|
|
207
|
+
parse_response = self._select_parse_response(request_options)
|
|
208
|
+
transform_response = self._select_transform_response(request_options)
|
|
209
|
+
retry_policy = _normalize_retry_policy(self._select_retry(request_options))
|
|
210
|
+
middleware = self._select_middleware(request_options)
|
|
211
|
+
query = self._select_query(request_options)
|
|
212
|
+
|
|
213
|
+
for attempt in range(1, retry_policy.max_attempts + 1):
|
|
214
|
+
client = request_options.client or self._client
|
|
215
|
+
fallback_context = self._fallback_context(
|
|
216
|
+
attempt=attempt,
|
|
217
|
+
method=method,
|
|
218
|
+
path=path,
|
|
219
|
+
options=request_options,
|
|
220
|
+
timeout_seconds=timeout_seconds,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
context = self._build_sync_request_context(
|
|
225
|
+
attempt=attempt,
|
|
226
|
+
method=method,
|
|
227
|
+
path=path,
|
|
228
|
+
options=request_options,
|
|
229
|
+
query=query,
|
|
230
|
+
timeout_seconds=timeout_seconds,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def terminal(
|
|
234
|
+
request_context: MailerRequestContext,
|
|
235
|
+
resolved_client: Any = client,
|
|
236
|
+
resolved_parser: ResponseParser | None = parse_response,
|
|
237
|
+
) -> MailerResponseContext:
|
|
238
|
+
return _sync_transport(
|
|
239
|
+
request_context,
|
|
240
|
+
client=resolved_client,
|
|
241
|
+
parse_response=resolved_parser,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
response_context = _run_sync_middleware_stack(
|
|
245
|
+
middleware,
|
|
246
|
+
context,
|
|
247
|
+
terminal,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
not response_context.response.is_success
|
|
252
|
+
and attempt < retry_policy.max_attempts
|
|
253
|
+
and _sync_should_retry(
|
|
254
|
+
retry_policy,
|
|
255
|
+
MailerRetryContext(
|
|
256
|
+
request=response_context.request,
|
|
257
|
+
attempt=attempt,
|
|
258
|
+
response=response_context.response,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
):
|
|
262
|
+
_sleep_seconds(
|
|
263
|
+
_sync_retry_delay(
|
|
264
|
+
retry_policy,
|
|
265
|
+
MailerRetryContext(
|
|
266
|
+
request=response_context.request,
|
|
267
|
+
attempt=attempt,
|
|
268
|
+
response=response_context.response,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
return _sync_transform_response(
|
|
275
|
+
response_context,
|
|
276
|
+
transform_response,
|
|
277
|
+
unwrap_data=request_options.unwrap_data,
|
|
278
|
+
)
|
|
279
|
+
except Exception as error:
|
|
280
|
+
if (
|
|
281
|
+
attempt < retry_policy.max_attempts
|
|
282
|
+
and _sync_should_retry(
|
|
283
|
+
retry_policy,
|
|
284
|
+
MailerRetryContext(
|
|
285
|
+
request=fallback_context,
|
|
286
|
+
attempt=attempt,
|
|
287
|
+
error=error,
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
):
|
|
291
|
+
_sleep_seconds(
|
|
292
|
+
_sync_retry_delay(
|
|
293
|
+
retry_policy,
|
|
294
|
+
MailerRetryContext(
|
|
295
|
+
request=fallback_context,
|
|
296
|
+
attempt=attempt,
|
|
297
|
+
error=error,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
continue
|
|
302
|
+
raise
|
|
303
|
+
|
|
304
|
+
raise MailerError("Mailer request exhausted all retry attempts.", status_code=0)
|
|
305
|
+
|
|
306
|
+
def _build_sync_request_context(
|
|
307
|
+
self,
|
|
308
|
+
*,
|
|
309
|
+
attempt: int,
|
|
310
|
+
method: str,
|
|
311
|
+
path: str,
|
|
312
|
+
options: RequestOptions,
|
|
313
|
+
query: Mapping[str, object] | None,
|
|
314
|
+
timeout_seconds: float | None,
|
|
315
|
+
) -> MailerRequestContext:
|
|
316
|
+
headers = merge_headers(self._default_headers, options.headers)
|
|
317
|
+
authenticated = True if options.authenticated is None else options.authenticated
|
|
318
|
+
auth = self._default_auth if options.auth is None else options.auth
|
|
319
|
+
|
|
320
|
+
if not authenticated:
|
|
321
|
+
if "authorization" in headers:
|
|
322
|
+
del headers["authorization"]
|
|
323
|
+
else:
|
|
324
|
+
if not auth and not _has_explicit_auth_headers(headers):
|
|
325
|
+
raise TypeError("Mailer auth is required for authenticated requests.")
|
|
326
|
+
if auth:
|
|
327
|
+
auth_headers = _resolve_sync_auth_headers(
|
|
328
|
+
auth,
|
|
329
|
+
self._build_request_context(
|
|
330
|
+
attempt=attempt,
|
|
331
|
+
method=method,
|
|
332
|
+
path=path,
|
|
333
|
+
options=options,
|
|
334
|
+
query=query,
|
|
335
|
+
body=UNSET,
|
|
336
|
+
headers=headers,
|
|
337
|
+
timeout_seconds=timeout_seconds,
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
headers.update(auth_headers)
|
|
341
|
+
|
|
342
|
+
if "accept" not in headers:
|
|
343
|
+
headers["accept"] = "application/json"
|
|
344
|
+
if options.idempotency_key:
|
|
345
|
+
headers["idempotency-key"] = options.idempotency_key
|
|
346
|
+
|
|
347
|
+
body = prepare_request_body(options.body, headers)
|
|
348
|
+
return self._build_request_context(
|
|
349
|
+
attempt=attempt,
|
|
350
|
+
method=method,
|
|
351
|
+
path=path,
|
|
352
|
+
options=options,
|
|
353
|
+
query=query,
|
|
354
|
+
body=body,
|
|
355
|
+
headers=headers,
|
|
356
|
+
timeout_seconds=timeout_seconds,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def close(self) -> None:
|
|
360
|
+
if self._owns_client:
|
|
361
|
+
self._client.close()
|
|
362
|
+
|
|
363
|
+
def __enter__(self) -> Mailer:
|
|
364
|
+
return self
|
|
365
|
+
|
|
366
|
+
def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
|
|
367
|
+
self.close()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class AsyncMailer(_MailerBase):
|
|
371
|
+
def __init__(
|
|
372
|
+
self,
|
|
373
|
+
api_key: str | None = None,
|
|
374
|
+
*,
|
|
375
|
+
base_url: str,
|
|
376
|
+
client: httpx.AsyncClient | Any | None = None,
|
|
377
|
+
timeout_seconds: float | None = DEFAULT_TIMEOUT_SECONDS,
|
|
378
|
+
headers: Mapping[str, str] | httpx.Headers | None = None,
|
|
379
|
+
query: Mapping[str, object] | None = None,
|
|
380
|
+
auth: MailerAuthStrategy | bool | None = None,
|
|
381
|
+
retry: RetryOptions | int | bool | None = None,
|
|
382
|
+
middleware: Sequence[MailerMiddleware] | None = None,
|
|
383
|
+
parse_response: ResponseParser | None = None,
|
|
384
|
+
transform_response: ResponseTransformer | None = None,
|
|
385
|
+
) -> None:
|
|
386
|
+
super().__init__(
|
|
387
|
+
api_key,
|
|
388
|
+
base_url=base_url,
|
|
389
|
+
timeout_seconds=timeout_seconds,
|
|
390
|
+
headers=headers,
|
|
391
|
+
query=query,
|
|
392
|
+
auth=auth,
|
|
393
|
+
retry=retry,
|
|
394
|
+
middleware=middleware,
|
|
395
|
+
parse_response=parse_response,
|
|
396
|
+
transform_response=transform_response,
|
|
397
|
+
)
|
|
398
|
+
self._client = client or httpx.AsyncClient()
|
|
399
|
+
self._owns_client = client is None
|
|
400
|
+
self.emails = AsyncEmailOperations(self)
|
|
401
|
+
self.sms = AsyncSmsOperations(self)
|
|
402
|
+
self.whatsapp = AsyncWhatsAppOperations(self)
|
|
403
|
+
self.merchant = AsyncMerchantOperations(self)
|
|
404
|
+
self.domains = AsyncDomainOperations(self)
|
|
405
|
+
self.api_keys = AsyncApiKeyOperations(self)
|
|
406
|
+
self.apiKeys = self.api_keys
|
|
407
|
+
self.webhooks = AsyncWebhookOperations(self)
|
|
408
|
+
self.health = AsyncHealthOperations(self)
|
|
409
|
+
|
|
410
|
+
async def request(
|
|
411
|
+
self,
|
|
412
|
+
method: str,
|
|
413
|
+
path: str,
|
|
414
|
+
options: RequestOptions | None = None,
|
|
415
|
+
) -> object:
|
|
416
|
+
request_options = self._effective_options(options)
|
|
417
|
+
timeout_seconds = self._select_timeout(request_options)
|
|
418
|
+
parse_response = self._select_parse_response(request_options)
|
|
419
|
+
transform_response = self._select_transform_response(request_options)
|
|
420
|
+
retry_policy = _normalize_retry_policy(self._select_retry(request_options))
|
|
421
|
+
middleware = self._select_middleware(request_options)
|
|
422
|
+
query = self._select_query(request_options)
|
|
423
|
+
|
|
424
|
+
for attempt in range(1, retry_policy.max_attempts + 1):
|
|
425
|
+
client = request_options.client or self._client
|
|
426
|
+
fallback_context = self._fallback_context(
|
|
427
|
+
attempt=attempt,
|
|
428
|
+
method=method,
|
|
429
|
+
path=path,
|
|
430
|
+
options=request_options,
|
|
431
|
+
timeout_seconds=timeout_seconds,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
context = await self._build_async_request_context(
|
|
436
|
+
attempt=attempt,
|
|
437
|
+
method=method,
|
|
438
|
+
path=path,
|
|
439
|
+
options=request_options,
|
|
440
|
+
query=query,
|
|
441
|
+
timeout_seconds=timeout_seconds,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
async def terminal(
|
|
445
|
+
request_context: MailerRequestContext,
|
|
446
|
+
resolved_client: Any = client,
|
|
447
|
+
resolved_parser: ResponseParser | None = parse_response,
|
|
448
|
+
) -> MailerResponseContext:
|
|
449
|
+
return await _async_transport(
|
|
450
|
+
request_context,
|
|
451
|
+
client=resolved_client,
|
|
452
|
+
parse_response=resolved_parser,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
response_context = await _run_async_middleware_stack(
|
|
456
|
+
middleware,
|
|
457
|
+
context,
|
|
458
|
+
terminal,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if (
|
|
462
|
+
not response_context.response.is_success
|
|
463
|
+
and attempt < retry_policy.max_attempts
|
|
464
|
+
and await _async_should_retry(
|
|
465
|
+
retry_policy,
|
|
466
|
+
MailerRetryContext(
|
|
467
|
+
request=response_context.request,
|
|
468
|
+
attempt=attempt,
|
|
469
|
+
response=response_context.response,
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
):
|
|
473
|
+
await _async_sleep_seconds(
|
|
474
|
+
await _async_retry_delay(
|
|
475
|
+
retry_policy,
|
|
476
|
+
MailerRetryContext(
|
|
477
|
+
request=response_context.request,
|
|
478
|
+
attempt=attempt,
|
|
479
|
+
response=response_context.response,
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
return await _async_transform_response(
|
|
486
|
+
response_context,
|
|
487
|
+
transform_response,
|
|
488
|
+
unwrap_data=request_options.unwrap_data,
|
|
489
|
+
)
|
|
490
|
+
except Exception as error:
|
|
491
|
+
if (
|
|
492
|
+
attempt < retry_policy.max_attempts
|
|
493
|
+
and await _async_should_retry(
|
|
494
|
+
retry_policy,
|
|
495
|
+
MailerRetryContext(
|
|
496
|
+
request=fallback_context,
|
|
497
|
+
attempt=attempt,
|
|
498
|
+
error=error,
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
):
|
|
502
|
+
await _async_sleep_seconds(
|
|
503
|
+
await _async_retry_delay(
|
|
504
|
+
retry_policy,
|
|
505
|
+
MailerRetryContext(
|
|
506
|
+
request=fallback_context,
|
|
507
|
+
attempt=attempt,
|
|
508
|
+
error=error,
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
continue
|
|
513
|
+
raise
|
|
514
|
+
|
|
515
|
+
raise MailerError("Mailer request exhausted all retry attempts.", status_code=0)
|
|
516
|
+
|
|
517
|
+
async def _build_async_request_context(
|
|
518
|
+
self,
|
|
519
|
+
*,
|
|
520
|
+
attempt: int,
|
|
521
|
+
method: str,
|
|
522
|
+
path: str,
|
|
523
|
+
options: RequestOptions,
|
|
524
|
+
query: Mapping[str, object] | None,
|
|
525
|
+
timeout_seconds: float | None,
|
|
526
|
+
) -> MailerRequestContext:
|
|
527
|
+
headers = merge_headers(self._default_headers, options.headers)
|
|
528
|
+
authenticated = True if options.authenticated is None else options.authenticated
|
|
529
|
+
auth = self._default_auth if options.auth is None else options.auth
|
|
530
|
+
|
|
531
|
+
if not authenticated:
|
|
532
|
+
if "authorization" in headers:
|
|
533
|
+
del headers["authorization"]
|
|
534
|
+
else:
|
|
535
|
+
if not auth and not _has_explicit_auth_headers(headers):
|
|
536
|
+
raise TypeError("Mailer auth is required for authenticated requests.")
|
|
537
|
+
if auth:
|
|
538
|
+
auth_headers = await _resolve_async_auth_headers(
|
|
539
|
+
auth,
|
|
540
|
+
self._build_request_context(
|
|
541
|
+
attempt=attempt,
|
|
542
|
+
method=method,
|
|
543
|
+
path=path,
|
|
544
|
+
options=options,
|
|
545
|
+
query=query,
|
|
546
|
+
body=UNSET,
|
|
547
|
+
headers=headers,
|
|
548
|
+
timeout_seconds=timeout_seconds,
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
headers.update(auth_headers)
|
|
552
|
+
|
|
553
|
+
if "accept" not in headers:
|
|
554
|
+
headers["accept"] = "application/json"
|
|
555
|
+
if options.idempotency_key:
|
|
556
|
+
headers["idempotency-key"] = options.idempotency_key
|
|
557
|
+
|
|
558
|
+
body = prepare_request_body(options.body, headers)
|
|
559
|
+
return self._build_request_context(
|
|
560
|
+
attempt=attempt,
|
|
561
|
+
method=method,
|
|
562
|
+
path=path,
|
|
563
|
+
options=options,
|
|
564
|
+
query=query,
|
|
565
|
+
body=body,
|
|
566
|
+
headers=headers,
|
|
567
|
+
timeout_seconds=timeout_seconds,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
async def aclose(self) -> None:
|
|
571
|
+
if self._owns_client:
|
|
572
|
+
await self._client.aclose()
|
|
573
|
+
|
|
574
|
+
async def __aenter__(self) -> AsyncMailer:
|
|
575
|
+
return self
|
|
576
|
+
|
|
577
|
+
async def __aexit__(self, exc_type: object, exc: object, traceback: object) -> None:
|
|
578
|
+
await self.aclose()
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class EmailOperations:
|
|
582
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
583
|
+
self._mailer = mailer
|
|
584
|
+
self.sendBatch = self.send_batch
|
|
585
|
+
|
|
586
|
+
def quote(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
587
|
+
return self._mailer.request(
|
|
588
|
+
"POST",
|
|
589
|
+
"/emails/quote",
|
|
590
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
def send(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
594
|
+
return self._mailer.request(
|
|
595
|
+
"POST",
|
|
596
|
+
"/emails",
|
|
597
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
def send_batch(
|
|
601
|
+
self,
|
|
602
|
+
requests: Sequence[Mapping[str, Any]],
|
|
603
|
+
options: RequestOptions | None = None,
|
|
604
|
+
) -> object:
|
|
605
|
+
return self._mailer.request(
|
|
606
|
+
"POST",
|
|
607
|
+
"/emails/batch",
|
|
608
|
+
_replace_request_options(
|
|
609
|
+
options,
|
|
610
|
+
body=[_normalize_send_email_request(request) for request in requests],
|
|
611
|
+
transform_response=(
|
|
612
|
+
options.transform_response
|
|
613
|
+
if options and options.transform_response
|
|
614
|
+
else _extract_data_array_response
|
|
615
|
+
),
|
|
616
|
+
),
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
def get(self, email_id: str, options: RequestOptions | None = None) -> object:
|
|
620
|
+
return self._mailer.request("GET", f"/emails/{_quote(email_id)}", options)
|
|
621
|
+
|
|
622
|
+
def list(
|
|
623
|
+
self,
|
|
624
|
+
options: RequestOptions | None = None,
|
|
625
|
+
*,
|
|
626
|
+
limit: int | None = None,
|
|
627
|
+
cursor: str | None = None,
|
|
628
|
+
per_page: int | None = None,
|
|
629
|
+
status: str | None = None,
|
|
630
|
+
) -> object:
|
|
631
|
+
query = merge_query_params(
|
|
632
|
+
{
|
|
633
|
+
"limit": limit,
|
|
634
|
+
"cursor": cursor,
|
|
635
|
+
"per_page": per_page,
|
|
636
|
+
"status": status,
|
|
637
|
+
},
|
|
638
|
+
options.query if options else None,
|
|
639
|
+
)
|
|
640
|
+
return self._mailer.request(
|
|
641
|
+
"GET",
|
|
642
|
+
"/emails",
|
|
643
|
+
_replace_request_options(options, query=query),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class AsyncEmailOperations:
|
|
648
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
649
|
+
self._mailer = mailer
|
|
650
|
+
self.sendBatch = self.send_batch
|
|
651
|
+
|
|
652
|
+
async def quote(
|
|
653
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
654
|
+
) -> object:
|
|
655
|
+
return await self._mailer.request(
|
|
656
|
+
"POST",
|
|
657
|
+
"/emails/quote",
|
|
658
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
async def send(
|
|
662
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
663
|
+
) -> object:
|
|
664
|
+
return await self._mailer.request(
|
|
665
|
+
"POST",
|
|
666
|
+
"/emails",
|
|
667
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
async def send_batch(
|
|
671
|
+
self,
|
|
672
|
+
requests: Sequence[Mapping[str, Any]],
|
|
673
|
+
options: RequestOptions | None = None,
|
|
674
|
+
) -> object:
|
|
675
|
+
return await self._mailer.request(
|
|
676
|
+
"POST",
|
|
677
|
+
"/emails/batch",
|
|
678
|
+
_replace_request_options(
|
|
679
|
+
options,
|
|
680
|
+
body=[_normalize_send_email_request(request) for request in requests],
|
|
681
|
+
transform_response=(
|
|
682
|
+
options.transform_response
|
|
683
|
+
if options and options.transform_response
|
|
684
|
+
else _extract_data_array_response
|
|
685
|
+
),
|
|
686
|
+
),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
async def get(self, email_id: str, options: RequestOptions | None = None) -> object:
|
|
690
|
+
return await self._mailer.request("GET", f"/emails/{_quote(email_id)}", options)
|
|
691
|
+
|
|
692
|
+
async def list(
|
|
693
|
+
self,
|
|
694
|
+
options: RequestOptions | None = None,
|
|
695
|
+
*,
|
|
696
|
+
limit: int | None = None,
|
|
697
|
+
cursor: str | None = None,
|
|
698
|
+
per_page: int | None = None,
|
|
699
|
+
status: str | None = None,
|
|
700
|
+
) -> object:
|
|
701
|
+
query = merge_query_params(
|
|
702
|
+
{
|
|
703
|
+
"limit": limit,
|
|
704
|
+
"cursor": cursor,
|
|
705
|
+
"per_page": per_page,
|
|
706
|
+
"status": status,
|
|
707
|
+
},
|
|
708
|
+
options.query if options else None,
|
|
709
|
+
)
|
|
710
|
+
return await self._mailer.request(
|
|
711
|
+
"GET", "/emails", _replace_request_options(options, query=query)
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class SmsOperations:
|
|
716
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
717
|
+
self._mailer = mailer
|
|
718
|
+
|
|
719
|
+
def quote(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
720
|
+
return self._mailer.request(
|
|
721
|
+
"POST",
|
|
722
|
+
"/sms/quote",
|
|
723
|
+
_replace_request_options(options, body=dict(request)),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
def send(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
727
|
+
return self._mailer.request(
|
|
728
|
+
"POST",
|
|
729
|
+
"/sms",
|
|
730
|
+
_replace_request_options(options, body=dict(request)),
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def get(self, message_id: str, options: RequestOptions | None = None) -> object:
|
|
734
|
+
return self._mailer.request("GET", f"/sms/{_quote(message_id)}", options)
|
|
735
|
+
|
|
736
|
+
def list(
|
|
737
|
+
self,
|
|
738
|
+
options: RequestOptions | None = None,
|
|
739
|
+
*,
|
|
740
|
+
limit: int | None = None,
|
|
741
|
+
cursor: str | None = None,
|
|
742
|
+
per_page: int | None = None,
|
|
743
|
+
status: str | None = None,
|
|
744
|
+
) -> object:
|
|
745
|
+
query = merge_query_params(
|
|
746
|
+
{
|
|
747
|
+
"limit": limit,
|
|
748
|
+
"cursor": cursor,
|
|
749
|
+
"per_page": per_page,
|
|
750
|
+
"status": status,
|
|
751
|
+
},
|
|
752
|
+
options.query if options else None,
|
|
753
|
+
)
|
|
754
|
+
return self._mailer.request(
|
|
755
|
+
"GET",
|
|
756
|
+
"/sms",
|
|
757
|
+
_replace_request_options(options, query=query),
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class AsyncSmsOperations:
|
|
762
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
763
|
+
self._mailer = mailer
|
|
764
|
+
|
|
765
|
+
async def quote(
|
|
766
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
767
|
+
) -> object:
|
|
768
|
+
return await self._mailer.request(
|
|
769
|
+
"POST",
|
|
770
|
+
"/sms/quote",
|
|
771
|
+
_replace_request_options(options, body=dict(request)),
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
async def send(
|
|
775
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
776
|
+
) -> object:
|
|
777
|
+
return await self._mailer.request(
|
|
778
|
+
"POST",
|
|
779
|
+
"/sms",
|
|
780
|
+
_replace_request_options(options, body=dict(request)),
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
async def get(self, message_id: str, options: RequestOptions | None = None) -> object:
|
|
784
|
+
return await self._mailer.request("GET", f"/sms/{_quote(message_id)}", options)
|
|
785
|
+
|
|
786
|
+
async def list(
|
|
787
|
+
self,
|
|
788
|
+
options: RequestOptions | None = None,
|
|
789
|
+
*,
|
|
790
|
+
limit: int | None = None,
|
|
791
|
+
cursor: str | None = None,
|
|
792
|
+
per_page: int | None = None,
|
|
793
|
+
status: str | None = None,
|
|
794
|
+
) -> object:
|
|
795
|
+
query = merge_query_params(
|
|
796
|
+
{
|
|
797
|
+
"limit": limit,
|
|
798
|
+
"cursor": cursor,
|
|
799
|
+
"per_page": per_page,
|
|
800
|
+
"status": status,
|
|
801
|
+
},
|
|
802
|
+
options.query if options else None,
|
|
803
|
+
)
|
|
804
|
+
return await self._mailer.request(
|
|
805
|
+
"GET", "/sms", _replace_request_options(options, query=query)
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
class WhatsAppOperations:
|
|
810
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
811
|
+
self._mailer = mailer
|
|
812
|
+
|
|
813
|
+
def quote(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
814
|
+
return self._mailer.request(
|
|
815
|
+
"POST",
|
|
816
|
+
"/whatsapp/messages/quote",
|
|
817
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
def send(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
821
|
+
return self._mailer.request(
|
|
822
|
+
"POST",
|
|
823
|
+
"/whatsapp/messages",
|
|
824
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
def get(self, message_id: str, options: RequestOptions | None = None) -> object:
|
|
828
|
+
return self._mailer.request("GET", f"/whatsapp/messages/{_quote(message_id)}", options)
|
|
829
|
+
|
|
830
|
+
def list(
|
|
831
|
+
self,
|
|
832
|
+
options: RequestOptions | None = None,
|
|
833
|
+
*,
|
|
834
|
+
limit: int | None = None,
|
|
835
|
+
cursor: str | None = None,
|
|
836
|
+
per_page: int | None = None,
|
|
837
|
+
status: str | None = None,
|
|
838
|
+
) -> object:
|
|
839
|
+
query = merge_query_params(
|
|
840
|
+
{
|
|
841
|
+
"limit": limit,
|
|
842
|
+
"cursor": cursor,
|
|
843
|
+
"per_page": per_page,
|
|
844
|
+
"status": status,
|
|
845
|
+
},
|
|
846
|
+
options.query if options else None,
|
|
847
|
+
)
|
|
848
|
+
return self._mailer.request(
|
|
849
|
+
"GET",
|
|
850
|
+
"/whatsapp/messages",
|
|
851
|
+
_replace_request_options(options, query=query),
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
class AsyncWhatsAppOperations:
|
|
856
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
857
|
+
self._mailer = mailer
|
|
858
|
+
|
|
859
|
+
async def quote(
|
|
860
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
861
|
+
) -> object:
|
|
862
|
+
return await self._mailer.request(
|
|
863
|
+
"POST",
|
|
864
|
+
"/whatsapp/messages/quote",
|
|
865
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
async def send(
|
|
869
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
870
|
+
) -> object:
|
|
871
|
+
return await self._mailer.request(
|
|
872
|
+
"POST",
|
|
873
|
+
"/whatsapp/messages",
|
|
874
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
async def get(self, message_id: str, options: RequestOptions | None = None) -> object:
|
|
878
|
+
return await self._mailer.request(
|
|
879
|
+
"GET",
|
|
880
|
+
f"/whatsapp/messages/{_quote(message_id)}",
|
|
881
|
+
options,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
async def list(
|
|
885
|
+
self,
|
|
886
|
+
options: RequestOptions | None = None,
|
|
887
|
+
*,
|
|
888
|
+
limit: int | None = None,
|
|
889
|
+
cursor: str | None = None,
|
|
890
|
+
per_page: int | None = None,
|
|
891
|
+
status: str | None = None,
|
|
892
|
+
) -> object:
|
|
893
|
+
query = merge_query_params(
|
|
894
|
+
{
|
|
895
|
+
"limit": limit,
|
|
896
|
+
"cursor": cursor,
|
|
897
|
+
"per_page": per_page,
|
|
898
|
+
"status": status,
|
|
899
|
+
},
|
|
900
|
+
options.query if options else None,
|
|
901
|
+
)
|
|
902
|
+
return await self._mailer.request(
|
|
903
|
+
"GET",
|
|
904
|
+
"/whatsapp/messages",
|
|
905
|
+
_replace_request_options(options, query=query),
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
class MerchantOperations:
|
|
910
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
911
|
+
self.messages = MerchantMessageOperations(mailer)
|
|
912
|
+
self.emails = MerchantEmailOperations(mailer)
|
|
913
|
+
self.sms = MerchantSmsOperations(mailer)
|
|
914
|
+
self.whatsapp = MerchantWhatsAppOperations(mailer)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
class AsyncMerchantOperations:
|
|
918
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
919
|
+
self.messages = AsyncMerchantMessageOperations(mailer)
|
|
920
|
+
self.emails = AsyncMerchantEmailOperations(mailer)
|
|
921
|
+
self.sms = AsyncMerchantSmsOperations(mailer)
|
|
922
|
+
self.whatsapp = AsyncMerchantWhatsAppOperations(mailer)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
class MerchantMessageOperations:
|
|
926
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
927
|
+
self._mailer = mailer
|
|
928
|
+
|
|
929
|
+
def list(
|
|
930
|
+
self,
|
|
931
|
+
merchant_id: str,
|
|
932
|
+
options: RequestOptions | None = None,
|
|
933
|
+
*,
|
|
934
|
+
limit: int | None = None,
|
|
935
|
+
cursor: str | None = None,
|
|
936
|
+
per_page: int | None = None,
|
|
937
|
+
channel: str | None = None,
|
|
938
|
+
status: str | None = None,
|
|
939
|
+
) -> object:
|
|
940
|
+
query = merge_query_params(
|
|
941
|
+
{
|
|
942
|
+
"limit": limit,
|
|
943
|
+
"cursor": cursor,
|
|
944
|
+
"per_page": per_page,
|
|
945
|
+
"channel": channel,
|
|
946
|
+
"status": status,
|
|
947
|
+
},
|
|
948
|
+
options.query if options else None,
|
|
949
|
+
)
|
|
950
|
+
return self._mailer.request(
|
|
951
|
+
"GET",
|
|
952
|
+
_merchant_messages_path(merchant_id),
|
|
953
|
+
_replace_request_options(options, query=query),
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
def get(
|
|
957
|
+
self,
|
|
958
|
+
merchant_id: str,
|
|
959
|
+
message_id: str,
|
|
960
|
+
options: RequestOptions | None = None,
|
|
961
|
+
) -> object:
|
|
962
|
+
return self._mailer.request(
|
|
963
|
+
"GET",
|
|
964
|
+
_merchant_messages_path(merchant_id, f"/{_quote(message_id)}"),
|
|
965
|
+
options,
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class AsyncMerchantMessageOperations:
|
|
970
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
971
|
+
self._mailer = mailer
|
|
972
|
+
|
|
973
|
+
async def list(
|
|
974
|
+
self,
|
|
975
|
+
merchant_id: str,
|
|
976
|
+
options: RequestOptions | None = None,
|
|
977
|
+
*,
|
|
978
|
+
limit: int | None = None,
|
|
979
|
+
cursor: str | None = None,
|
|
980
|
+
per_page: int | None = None,
|
|
981
|
+
channel: str | None = None,
|
|
982
|
+
status: str | None = None,
|
|
983
|
+
) -> object:
|
|
984
|
+
query = merge_query_params(
|
|
985
|
+
{
|
|
986
|
+
"limit": limit,
|
|
987
|
+
"cursor": cursor,
|
|
988
|
+
"per_page": per_page,
|
|
989
|
+
"channel": channel,
|
|
990
|
+
"status": status,
|
|
991
|
+
},
|
|
992
|
+
options.query if options else None,
|
|
993
|
+
)
|
|
994
|
+
return await self._mailer.request(
|
|
995
|
+
"GET",
|
|
996
|
+
_merchant_messages_path(merchant_id),
|
|
997
|
+
_replace_request_options(options, query=query),
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
async def get(
|
|
1001
|
+
self, merchant_id: str, message_id: str, options: RequestOptions | None = None
|
|
1002
|
+
) -> object:
|
|
1003
|
+
return await self._mailer.request(
|
|
1004
|
+
"GET",
|
|
1005
|
+
_merchant_messages_path(merchant_id, f"/{_quote(message_id)}"),
|
|
1006
|
+
options,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
class MerchantEmailOperations:
|
|
1011
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1012
|
+
self._mailer = mailer
|
|
1013
|
+
self.quoteGroup = self.quote_group
|
|
1014
|
+
self.sendGroup = self.send_group
|
|
1015
|
+
|
|
1016
|
+
def quote(
|
|
1017
|
+
self,
|
|
1018
|
+
merchant_id: str,
|
|
1019
|
+
request: Mapping[str, Any],
|
|
1020
|
+
options: RequestOptions | None = None,
|
|
1021
|
+
) -> object:
|
|
1022
|
+
return self._mailer.request(
|
|
1023
|
+
"POST",
|
|
1024
|
+
_merchant_messages_path(merchant_id, "/email/quote"),
|
|
1025
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
def quote_group(
|
|
1029
|
+
self,
|
|
1030
|
+
merchant_id: str,
|
|
1031
|
+
request: Mapping[str, Any],
|
|
1032
|
+
options: RequestOptions | None = None,
|
|
1033
|
+
) -> object:
|
|
1034
|
+
return self._mailer.request(
|
|
1035
|
+
"POST",
|
|
1036
|
+
_merchant_messages_path(merchant_id, "/email/group/quote"),
|
|
1037
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
def send(
|
|
1041
|
+
self,
|
|
1042
|
+
merchant_id: str,
|
|
1043
|
+
request: Mapping[str, Any],
|
|
1044
|
+
options: RequestOptions | None = None,
|
|
1045
|
+
) -> object:
|
|
1046
|
+
return self._mailer.request(
|
|
1047
|
+
"POST",
|
|
1048
|
+
_merchant_messages_path(merchant_id, "/email"),
|
|
1049
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
def send_group(
|
|
1053
|
+
self,
|
|
1054
|
+
merchant_id: str,
|
|
1055
|
+
request: Mapping[str, Any],
|
|
1056
|
+
options: RequestOptions | None = None,
|
|
1057
|
+
) -> object:
|
|
1058
|
+
return self._mailer.request(
|
|
1059
|
+
"POST",
|
|
1060
|
+
_merchant_messages_path(merchant_id, "/email/group"),
|
|
1061
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
class AsyncMerchantEmailOperations:
|
|
1066
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1067
|
+
self._mailer = mailer
|
|
1068
|
+
self.quoteGroup = self.quote_group
|
|
1069
|
+
self.sendGroup = self.send_group
|
|
1070
|
+
|
|
1071
|
+
async def quote(
|
|
1072
|
+
self,
|
|
1073
|
+
merchant_id: str,
|
|
1074
|
+
request: Mapping[str, Any],
|
|
1075
|
+
options: RequestOptions | None = None,
|
|
1076
|
+
) -> object:
|
|
1077
|
+
return await self._mailer.request(
|
|
1078
|
+
"POST",
|
|
1079
|
+
_merchant_messages_path(merchant_id, "/email/quote"),
|
|
1080
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
async def quote_group(
|
|
1084
|
+
self,
|
|
1085
|
+
merchant_id: str,
|
|
1086
|
+
request: Mapping[str, Any],
|
|
1087
|
+
options: RequestOptions | None = None,
|
|
1088
|
+
) -> object:
|
|
1089
|
+
return await self._mailer.request(
|
|
1090
|
+
"POST",
|
|
1091
|
+
_merchant_messages_path(merchant_id, "/email/group/quote"),
|
|
1092
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
async def send(
|
|
1096
|
+
self,
|
|
1097
|
+
merchant_id: str,
|
|
1098
|
+
request: Mapping[str, Any],
|
|
1099
|
+
options: RequestOptions | None = None,
|
|
1100
|
+
) -> object:
|
|
1101
|
+
return await self._mailer.request(
|
|
1102
|
+
"POST",
|
|
1103
|
+
_merchant_messages_path(merchant_id, "/email"),
|
|
1104
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
async def send_group(
|
|
1108
|
+
self,
|
|
1109
|
+
merchant_id: str,
|
|
1110
|
+
request: Mapping[str, Any],
|
|
1111
|
+
options: RequestOptions | None = None,
|
|
1112
|
+
) -> object:
|
|
1113
|
+
return await self._mailer.request(
|
|
1114
|
+
"POST",
|
|
1115
|
+
_merchant_messages_path(merchant_id, "/email/group"),
|
|
1116
|
+
_replace_request_options(options, body=_normalize_send_email_request(request)),
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
class MerchantSmsOperations:
|
|
1121
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1122
|
+
self._mailer = mailer
|
|
1123
|
+
|
|
1124
|
+
def quote(
|
|
1125
|
+
self,
|
|
1126
|
+
merchant_id: str,
|
|
1127
|
+
request: Mapping[str, Any],
|
|
1128
|
+
options: RequestOptions | None = None,
|
|
1129
|
+
) -> object:
|
|
1130
|
+
return self._mailer.request(
|
|
1131
|
+
"POST",
|
|
1132
|
+
_merchant_messages_path(merchant_id, "/sms/quote"),
|
|
1133
|
+
_replace_request_options(options, body=dict(request)),
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
def send(
|
|
1137
|
+
self,
|
|
1138
|
+
merchant_id: str,
|
|
1139
|
+
request: Mapping[str, Any],
|
|
1140
|
+
options: RequestOptions | None = None,
|
|
1141
|
+
) -> object:
|
|
1142
|
+
return self._mailer.request(
|
|
1143
|
+
"POST",
|
|
1144
|
+
_merchant_messages_path(merchant_id, "/sms"),
|
|
1145
|
+
_replace_request_options(options, body=dict(request)),
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
class AsyncMerchantSmsOperations:
|
|
1150
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1151
|
+
self._mailer = mailer
|
|
1152
|
+
|
|
1153
|
+
async def quote(
|
|
1154
|
+
self,
|
|
1155
|
+
merchant_id: str,
|
|
1156
|
+
request: Mapping[str, Any],
|
|
1157
|
+
options: RequestOptions | None = None,
|
|
1158
|
+
) -> object:
|
|
1159
|
+
return await self._mailer.request(
|
|
1160
|
+
"POST",
|
|
1161
|
+
_merchant_messages_path(merchant_id, "/sms/quote"),
|
|
1162
|
+
_replace_request_options(options, body=dict(request)),
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
async def send(
|
|
1166
|
+
self,
|
|
1167
|
+
merchant_id: str,
|
|
1168
|
+
request: Mapping[str, Any],
|
|
1169
|
+
options: RequestOptions | None = None,
|
|
1170
|
+
) -> object:
|
|
1171
|
+
return await self._mailer.request(
|
|
1172
|
+
"POST",
|
|
1173
|
+
_merchant_messages_path(merchant_id, "/sms"),
|
|
1174
|
+
_replace_request_options(options, body=dict(request)),
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
class MerchantWhatsAppOperations:
|
|
1179
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1180
|
+
self._mailer = mailer
|
|
1181
|
+
|
|
1182
|
+
def quote(
|
|
1183
|
+
self,
|
|
1184
|
+
merchant_id: str,
|
|
1185
|
+
request: Mapping[str, Any],
|
|
1186
|
+
options: RequestOptions | None = None,
|
|
1187
|
+
) -> object:
|
|
1188
|
+
return self._mailer.request(
|
|
1189
|
+
"POST",
|
|
1190
|
+
_merchant_messages_path(merchant_id, "/whatsapp/quote"),
|
|
1191
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
def send(
|
|
1195
|
+
self,
|
|
1196
|
+
merchant_id: str,
|
|
1197
|
+
request: Mapping[str, Any],
|
|
1198
|
+
options: RequestOptions | None = None,
|
|
1199
|
+
) -> object:
|
|
1200
|
+
return self._mailer.request(
|
|
1201
|
+
"POST",
|
|
1202
|
+
_merchant_messages_path(merchant_id, "/whatsapp"),
|
|
1203
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
class AsyncMerchantWhatsAppOperations:
|
|
1208
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1209
|
+
self._mailer = mailer
|
|
1210
|
+
|
|
1211
|
+
async def quote(
|
|
1212
|
+
self,
|
|
1213
|
+
merchant_id: str,
|
|
1214
|
+
request: Mapping[str, Any],
|
|
1215
|
+
options: RequestOptions | None = None,
|
|
1216
|
+
) -> object:
|
|
1217
|
+
return await self._mailer.request(
|
|
1218
|
+
"POST",
|
|
1219
|
+
_merchant_messages_path(merchant_id, "/whatsapp/quote"),
|
|
1220
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
async def send(
|
|
1224
|
+
self,
|
|
1225
|
+
merchant_id: str,
|
|
1226
|
+
request: Mapping[str, Any],
|
|
1227
|
+
options: RequestOptions | None = None,
|
|
1228
|
+
) -> object:
|
|
1229
|
+
return await self._mailer.request(
|
|
1230
|
+
"POST",
|
|
1231
|
+
_merchant_messages_path(merchant_id, "/whatsapp"),
|
|
1232
|
+
_replace_request_options(options, body=_normalize_whatsapp_request(request)),
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
class DomainOperations:
|
|
1237
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1238
|
+
self._mailer = mailer
|
|
1239
|
+
|
|
1240
|
+
def create(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
1241
|
+
return self._mailer.request(
|
|
1242
|
+
"POST",
|
|
1243
|
+
"/domains",
|
|
1244
|
+
_replace_request_options(options, body=dict(request)),
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
def list(self, options: RequestOptions | None = None) -> object:
|
|
1248
|
+
return self._mailer.request("GET", "/domains", options)
|
|
1249
|
+
|
|
1250
|
+
def get(self, domain_id: str, options: RequestOptions | None = None) -> object:
|
|
1251
|
+
return self._mailer.request("GET", f"/domains/{_quote(domain_id)}", options)
|
|
1252
|
+
|
|
1253
|
+
def verify(self, domain_id: str, options: RequestOptions | None = None) -> object:
|
|
1254
|
+
return self._mailer.request("POST", f"/domains/{_quote(domain_id)}/verify", options)
|
|
1255
|
+
|
|
1256
|
+
def remove(self, domain_id: str, options: RequestOptions | None = None) -> object:
|
|
1257
|
+
return self._mailer.request("DELETE", f"/domains/{_quote(domain_id)}", options)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
class AsyncDomainOperations:
|
|
1261
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1262
|
+
self._mailer = mailer
|
|
1263
|
+
|
|
1264
|
+
async def create(
|
|
1265
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
1266
|
+
) -> object:
|
|
1267
|
+
return await self._mailer.request(
|
|
1268
|
+
"POST",
|
|
1269
|
+
"/domains",
|
|
1270
|
+
_replace_request_options(options, body=dict(request)),
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
async def list(self, options: RequestOptions | None = None) -> object:
|
|
1274
|
+
return await self._mailer.request("GET", "/domains", options)
|
|
1275
|
+
|
|
1276
|
+
async def get(self, domain_id: str, options: RequestOptions | None = None) -> object:
|
|
1277
|
+
return await self._mailer.request("GET", f"/domains/{_quote(domain_id)}", options)
|
|
1278
|
+
|
|
1279
|
+
async def verify(self, domain_id: str, options: RequestOptions | None = None) -> object:
|
|
1280
|
+
return await self._mailer.request("POST", f"/domains/{_quote(domain_id)}/verify", options)
|
|
1281
|
+
|
|
1282
|
+
async def remove(self, domain_id: str, options: RequestOptions | None = None) -> object:
|
|
1283
|
+
return await self._mailer.request("DELETE", f"/domains/{_quote(domain_id)}", options)
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
class ApiKeyOperations:
|
|
1287
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1288
|
+
self._mailer = mailer
|
|
1289
|
+
|
|
1290
|
+
def create(
|
|
1291
|
+
self,
|
|
1292
|
+
request: Mapping[str, Any] | None = None,
|
|
1293
|
+
options: RequestOptions | None = None,
|
|
1294
|
+
) -> object:
|
|
1295
|
+
return self._mailer.request(
|
|
1296
|
+
"POST",
|
|
1297
|
+
"/api-keys",
|
|
1298
|
+
_replace_request_options(options, body=_serialize_create_api_key_request(request)),
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
def list(self, options: RequestOptions | None = None) -> object:
|
|
1302
|
+
return self._mailer.request("GET", "/api-keys", options)
|
|
1303
|
+
|
|
1304
|
+
def get(self, api_key_id: str, options: RequestOptions | None = None) -> object:
|
|
1305
|
+
return self._mailer.request("GET", f"/api-keys/{_quote(api_key_id)}", options)
|
|
1306
|
+
|
|
1307
|
+
def remove(self, api_key_id: str, options: RequestOptions | None = None) -> object:
|
|
1308
|
+
return self._mailer.request("DELETE", f"/api-keys/{_quote(api_key_id)}", options)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
class AsyncApiKeyOperations:
|
|
1312
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1313
|
+
self._mailer = mailer
|
|
1314
|
+
|
|
1315
|
+
async def create(
|
|
1316
|
+
self,
|
|
1317
|
+
request: Mapping[str, Any] | None = None,
|
|
1318
|
+
options: RequestOptions | None = None,
|
|
1319
|
+
) -> object:
|
|
1320
|
+
return await self._mailer.request(
|
|
1321
|
+
"POST",
|
|
1322
|
+
"/api-keys",
|
|
1323
|
+
_replace_request_options(options, body=_serialize_create_api_key_request(request)),
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
async def list(self, options: RequestOptions | None = None) -> object:
|
|
1327
|
+
return await self._mailer.request("GET", "/api-keys", options)
|
|
1328
|
+
|
|
1329
|
+
async def get(self, api_key_id: str, options: RequestOptions | None = None) -> object:
|
|
1330
|
+
return await self._mailer.request("GET", f"/api-keys/{_quote(api_key_id)}", options)
|
|
1331
|
+
|
|
1332
|
+
async def remove(self, api_key_id: str, options: RequestOptions | None = None) -> object:
|
|
1333
|
+
return await self._mailer.request("DELETE", f"/api-keys/{_quote(api_key_id)}", options)
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
class WebhookOperations:
|
|
1337
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1338
|
+
self._mailer = mailer
|
|
1339
|
+
|
|
1340
|
+
def create(self, request: Mapping[str, Any], options: RequestOptions | None = None) -> object:
|
|
1341
|
+
return self._mailer.request(
|
|
1342
|
+
"POST",
|
|
1343
|
+
"/webhooks",
|
|
1344
|
+
_replace_request_options(options, body=dict(request)),
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
def list(self, options: RequestOptions | None = None) -> object:
|
|
1348
|
+
return self._mailer.request("GET", "/webhooks", options)
|
|
1349
|
+
|
|
1350
|
+
def remove(self, webhook_id: str, options: RequestOptions | None = None) -> object:
|
|
1351
|
+
return self._mailer.request("DELETE", f"/webhooks/{_quote(webhook_id)}", options)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
class AsyncWebhookOperations:
|
|
1355
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1356
|
+
self._mailer = mailer
|
|
1357
|
+
|
|
1358
|
+
async def create(
|
|
1359
|
+
self, request: Mapping[str, Any], options: RequestOptions | None = None
|
|
1360
|
+
) -> object:
|
|
1361
|
+
return await self._mailer.request(
|
|
1362
|
+
"POST",
|
|
1363
|
+
"/webhooks",
|
|
1364
|
+
_replace_request_options(options, body=dict(request)),
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
async def list(self, options: RequestOptions | None = None) -> object:
|
|
1368
|
+
return await self._mailer.request("GET", "/webhooks", options)
|
|
1369
|
+
|
|
1370
|
+
async def remove(self, webhook_id: str, options: RequestOptions | None = None) -> object:
|
|
1371
|
+
return await self._mailer.request("DELETE", f"/webhooks/{_quote(webhook_id)}", options)
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
class HealthOperations:
|
|
1375
|
+
def __init__(self, mailer: Mailer) -> None:
|
|
1376
|
+
self._mailer = mailer
|
|
1377
|
+
|
|
1378
|
+
def live(self, options: RequestOptions | None = None) -> object:
|
|
1379
|
+
return self._mailer.request(
|
|
1380
|
+
"GET",
|
|
1381
|
+
_root_url_path(self._mailer.base_url, "/livez"),
|
|
1382
|
+
_with_default_unauthenticated(options),
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
def check(self, options: RequestOptions | None = None) -> object:
|
|
1386
|
+
return self._mailer.request(
|
|
1387
|
+
"GET",
|
|
1388
|
+
_root_url_path(self._mailer.base_url, "/healthz"),
|
|
1389
|
+
_with_default_unauthenticated(options),
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
def ready(self, options: RequestOptions | None = None) -> object:
|
|
1393
|
+
return self._mailer.request(
|
|
1394
|
+
"GET",
|
|
1395
|
+
_root_url_path(self._mailer.base_url, "/readyz"),
|
|
1396
|
+
_with_default_unauthenticated(options),
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
class AsyncHealthOperations:
|
|
1401
|
+
def __init__(self, mailer: AsyncMailer) -> None:
|
|
1402
|
+
self._mailer = mailer
|
|
1403
|
+
|
|
1404
|
+
async def live(self, options: RequestOptions | None = None) -> object:
|
|
1405
|
+
return await self._mailer.request(
|
|
1406
|
+
"GET",
|
|
1407
|
+
_root_url_path(self._mailer.base_url, "/livez"),
|
|
1408
|
+
_with_default_unauthenticated(options),
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
async def check(self, options: RequestOptions | None = None) -> object:
|
|
1412
|
+
return await self._mailer.request(
|
|
1413
|
+
"GET",
|
|
1414
|
+
_root_url_path(self._mailer.base_url, "/healthz"),
|
|
1415
|
+
_with_default_unauthenticated(options),
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
async def ready(self, options: RequestOptions | None = None) -> object:
|
|
1419
|
+
return await self._mailer.request(
|
|
1420
|
+
"GET",
|
|
1421
|
+
_root_url_path(self._mailer.base_url, "/readyz"),
|
|
1422
|
+
_with_default_unauthenticated(options),
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def _with_default_unauthenticated(options: RequestOptions | None) -> RequestOptions:
|
|
1427
|
+
authenticated = (
|
|
1428
|
+
False
|
|
1429
|
+
if options is None or options.authenticated is None
|
|
1430
|
+
else options.authenticated
|
|
1431
|
+
)
|
|
1432
|
+
return _replace_request_options(options, authenticated=authenticated)
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def _replace_request_options(options: RequestOptions | None, **changes: object) -> RequestOptions:
|
|
1436
|
+
base = options if options is not None else RequestOptions()
|
|
1437
|
+
return replace(base, **changes)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _normalize_send_email_request(request: Mapping[str, Any]) -> dict[str, Any]:
|
|
1441
|
+
payload = dict(request)
|
|
1442
|
+
_rename_alias(payload, "reply_to", "replyTo")
|
|
1443
|
+
_rename_alias(payload, "scheduled_at", "scheduledAt")
|
|
1444
|
+
_rename_alias(payload, "configuration_set_name", "configurationSetName")
|
|
1445
|
+
_rename_alias(payload, "tenant_name", "tenantName")
|
|
1446
|
+
_rename_alias(payload, "endpoint_id", "endpointId")
|
|
1447
|
+
_rename_alias(
|
|
1448
|
+
payload,
|
|
1449
|
+
"feedback_forwarding_email_address",
|
|
1450
|
+
"feedbackForwardingEmailAddress",
|
|
1451
|
+
)
|
|
1452
|
+
_rename_alias(
|
|
1453
|
+
payload,
|
|
1454
|
+
"feedback_forwarding_email_address_identity_arn",
|
|
1455
|
+
"feedbackForwardingEmailAddressIdentityArn",
|
|
1456
|
+
)
|
|
1457
|
+
_rename_alias(
|
|
1458
|
+
payload,
|
|
1459
|
+
"from_email_address_identity_arn",
|
|
1460
|
+
"fromEmailAddressIdentityArn",
|
|
1461
|
+
)
|
|
1462
|
+
_rename_alias(payload, "list_management_options", "listManagementOptions")
|
|
1463
|
+
|
|
1464
|
+
list_management_options = payload.get("listManagementOptions")
|
|
1465
|
+
if isinstance(list_management_options, Mapping):
|
|
1466
|
+
payload["listManagementOptions"] = _normalize_list_management_options(
|
|
1467
|
+
list_management_options
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
attachments = payload.get("attachments")
|
|
1471
|
+
if isinstance(attachments, Sequence) and not isinstance(attachments, (str, bytes, bytearray)):
|
|
1472
|
+
payload["attachments"] = [
|
|
1473
|
+
_normalize_email_attachment(attachment)
|
|
1474
|
+
if isinstance(attachment, Mapping)
|
|
1475
|
+
else attachment
|
|
1476
|
+
for attachment in attachments
|
|
1477
|
+
]
|
|
1478
|
+
return payload
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
def _serialize_create_api_key_request(request: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
1482
|
+
payload = dict(request or {})
|
|
1483
|
+
if "expires_at" in payload and "expiresAt" not in payload:
|
|
1484
|
+
payload["expiresAt"] = payload.pop("expires_at")
|
|
1485
|
+
value = payload.get("expiresAt")
|
|
1486
|
+
if isinstance(value, datetime):
|
|
1487
|
+
payload["expiresAt"] = serialize_datetime(value)
|
|
1488
|
+
return payload
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _normalize_whatsapp_request(request: Mapping[str, Any]) -> dict[str, Any]:
|
|
1492
|
+
payload = dict(request)
|
|
1493
|
+
_rename_alias(payload, "template_variables", "variables")
|
|
1494
|
+
return payload
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _normalize_list_management_options(options: Mapping[str, Any]) -> dict[str, Any]:
|
|
1498
|
+
payload = dict(options)
|
|
1499
|
+
_rename_alias(payload, "contact_list_name", "contactListName")
|
|
1500
|
+
_rename_alias(payload, "topic_name", "topicName")
|
|
1501
|
+
return payload
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def _normalize_email_attachment(attachment: Mapping[str, Any]) -> dict[str, Any]:
|
|
1505
|
+
payload = dict(attachment)
|
|
1506
|
+
_rename_alias(payload, "content_type", "contentType")
|
|
1507
|
+
_rename_alias(payload, "content_id", "contentId")
|
|
1508
|
+
_rename_alias(payload, "content_disposition", "disposition")
|
|
1509
|
+
return payload
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def _rename_alias(payload: dict[str, Any], snake_name: str, camel_name: str) -> None:
|
|
1513
|
+
if snake_name in payload and camel_name not in payload:
|
|
1514
|
+
payload[camel_name] = payload.pop(snake_name)
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def _has_explicit_auth_headers(headers: httpx.Headers) -> bool:
|
|
1518
|
+
return "authorization" in headers or "x-api-key" in headers
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _merchant_messages_path(merchant_id: str, suffix: str = "") -> str:
|
|
1522
|
+
path = f"/merchants/{_quote(merchant_id)}/messages"
|
|
1523
|
+
return path if suffix == "" else f"{path}{suffix}"
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def _root_url_path(base_url: str, path: str) -> str:
|
|
1527
|
+
parts = urlsplit(base_url)
|
|
1528
|
+
return urlunsplit((parts.scheme, parts.netloc, path, "", ""))
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def _default_transform_response(context: MailerResponseContext, unwrap_data: bool = True) -> object:
|
|
1532
|
+
if not context.response.is_success:
|
|
1533
|
+
raise _to_mailer_error(context.response.status_code, context.payload)
|
|
1534
|
+
if unwrap_data and is_success_envelope(context.payload):
|
|
1535
|
+
payload = as_mapping(context.payload)
|
|
1536
|
+
return payload["data"]
|
|
1537
|
+
return context.payload
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _extract_data_array_response(context: MailerResponseContext) -> object:
|
|
1541
|
+
payload = _default_transform_response(context, unwrap_data=False)
|
|
1542
|
+
if isinstance(payload, list):
|
|
1543
|
+
return payload
|
|
1544
|
+
payload_mapping = as_mapping(payload)
|
|
1545
|
+
if isinstance(payload_mapping.get("data"), list):
|
|
1546
|
+
return payload_mapping["data"]
|
|
1547
|
+
return payload
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def _to_mailer_error(status_code: int, payload: object) -> MailerError:
|
|
1551
|
+
message, code, details = error_envelope_message(payload)
|
|
1552
|
+
if message is not None:
|
|
1553
|
+
return MailerError(
|
|
1554
|
+
message,
|
|
1555
|
+
status_code=status_code,
|
|
1556
|
+
code=code,
|
|
1557
|
+
details=details,
|
|
1558
|
+
response_body=payload,
|
|
1559
|
+
)
|
|
1560
|
+
if isinstance(payload, str) and payload.strip() != "":
|
|
1561
|
+
return MailerError(payload, status_code=status_code, response_body=payload)
|
|
1562
|
+
return MailerError(
|
|
1563
|
+
f"Mailer request failed with status {status_code}.",
|
|
1564
|
+
status_code=status_code,
|
|
1565
|
+
response_body=payload,
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _normalize_retry_policy(
|
|
1570
|
+
retry: RetryOptions | int | bool | None,
|
|
1571
|
+
) -> RetryOptions:
|
|
1572
|
+
if retry in (None, False):
|
|
1573
|
+
return RetryOptions(max_attempts=1)
|
|
1574
|
+
if retry is True:
|
|
1575
|
+
return RetryOptions()
|
|
1576
|
+
if isinstance(retry, int):
|
|
1577
|
+
return RetryOptions(max_attempts=max(1, retry))
|
|
1578
|
+
return RetryOptions(
|
|
1579
|
+
max_attempts=max(1, int(retry.max_attempts)),
|
|
1580
|
+
delay_seconds=retry.delay_seconds,
|
|
1581
|
+
should_retry=retry.should_retry,
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _default_should_retry(context: MailerRetryContext) -> bool:
|
|
1586
|
+
if context.error is not None:
|
|
1587
|
+
return not isinstance(context.error, MailerError)
|
|
1588
|
+
if context.response is None:
|
|
1589
|
+
return False
|
|
1590
|
+
return context.response.status_code in RETRYABLE_STATUS_CODES
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _default_retry_delay(attempt: int) -> float:
|
|
1594
|
+
return min(1.0, 0.1 * (2 ** max(0, attempt - 1)))
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def _sync_should_retry(policy: RetryOptions, context: MailerRetryContext) -> bool:
|
|
1598
|
+
if policy.should_retry is None:
|
|
1599
|
+
return _default_should_retry(context)
|
|
1600
|
+
return _resolve_sync_value(policy.should_retry(context), "Retry predicate")
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def _sync_retry_delay(policy: RetryOptions, context: MailerRetryContext) -> float:
|
|
1604
|
+
if policy.delay_seconds is None:
|
|
1605
|
+
return _default_retry_delay(context.attempt)
|
|
1606
|
+
if callable(policy.delay_seconds):
|
|
1607
|
+
return float(_resolve_sync_value(policy.delay_seconds(context), "Retry delay"))
|
|
1608
|
+
return float(policy.delay_seconds)
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
async def _async_should_retry(policy: RetryOptions, context: MailerRetryContext) -> bool:
|
|
1612
|
+
if policy.should_retry is None:
|
|
1613
|
+
return _default_should_retry(context)
|
|
1614
|
+
return bool(await _resolve_async_value(policy.should_retry(context)))
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
async def _async_retry_delay(policy: RetryOptions, context: MailerRetryContext) -> float:
|
|
1618
|
+
if policy.delay_seconds is None:
|
|
1619
|
+
return _default_retry_delay(context.attempt)
|
|
1620
|
+
if callable(policy.delay_seconds):
|
|
1621
|
+
return float(await _resolve_async_value(policy.delay_seconds(context)))
|
|
1622
|
+
return float(policy.delay_seconds)
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
def _sleep_seconds(delay: float) -> None:
|
|
1626
|
+
if delay > 0:
|
|
1627
|
+
time.sleep(delay)
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
async def _async_sleep_seconds(delay: float) -> None:
|
|
1631
|
+
if delay > 0:
|
|
1632
|
+
await asyncio.sleep(delay)
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
def _sync_transport(
|
|
1636
|
+
context: MailerRequestContext,
|
|
1637
|
+
*,
|
|
1638
|
+
client: Any,
|
|
1639
|
+
parse_response: ResponseParser | None,
|
|
1640
|
+
) -> MailerResponseContext:
|
|
1641
|
+
request_kwargs = {
|
|
1642
|
+
"method": context.method,
|
|
1643
|
+
"url": context.url,
|
|
1644
|
+
"headers": context.headers,
|
|
1645
|
+
"timeout": context.timeout_seconds,
|
|
1646
|
+
}
|
|
1647
|
+
if context.body is not UNSET:
|
|
1648
|
+
request_kwargs["content"] = context.body
|
|
1649
|
+
response = client.request(**request_kwargs)
|
|
1650
|
+
parser = parse_response or parse_response_body
|
|
1651
|
+
payload = _resolve_sync_value(
|
|
1652
|
+
parser(response, context),
|
|
1653
|
+
"Response parser",
|
|
1654
|
+
)
|
|
1655
|
+
return MailerResponseContext(request=context, response=response, payload=payload)
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
async def _async_transport(
|
|
1659
|
+
context: MailerRequestContext,
|
|
1660
|
+
*,
|
|
1661
|
+
client: Any,
|
|
1662
|
+
parse_response: ResponseParser | None,
|
|
1663
|
+
) -> MailerResponseContext:
|
|
1664
|
+
request_kwargs = {
|
|
1665
|
+
"method": context.method,
|
|
1666
|
+
"url": context.url,
|
|
1667
|
+
"headers": context.headers,
|
|
1668
|
+
"timeout": context.timeout_seconds,
|
|
1669
|
+
}
|
|
1670
|
+
if context.body is not UNSET:
|
|
1671
|
+
request_kwargs["content"] = context.body
|
|
1672
|
+
response = await client.request(**request_kwargs)
|
|
1673
|
+
parser = parse_response or parse_response_body
|
|
1674
|
+
payload = await _resolve_async_value(parser(response, context))
|
|
1675
|
+
return MailerResponseContext(request=context, response=response, payload=payload)
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _sync_transform_response(
|
|
1679
|
+
context: MailerResponseContext,
|
|
1680
|
+
transform_response: ResponseTransformer | None,
|
|
1681
|
+
*,
|
|
1682
|
+
unwrap_data: bool | None,
|
|
1683
|
+
) -> object:
|
|
1684
|
+
if transform_response is None:
|
|
1685
|
+
return _default_transform_response(context, unwrap_data=unwrap_data is not False)
|
|
1686
|
+
return _resolve_sync_value(transform_response(context), "Response transformer")
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
async def _async_transform_response(
|
|
1690
|
+
context: MailerResponseContext,
|
|
1691
|
+
transform_response: ResponseTransformer | None,
|
|
1692
|
+
*,
|
|
1693
|
+
unwrap_data: bool | None,
|
|
1694
|
+
) -> object:
|
|
1695
|
+
if transform_response is None:
|
|
1696
|
+
return _default_transform_response(context, unwrap_data=unwrap_data is not False)
|
|
1697
|
+
return await _resolve_async_value(transform_response(context))
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def _resolve_sync_auth_headers(
|
|
1701
|
+
auth: MailerAuthStrategy | bool,
|
|
1702
|
+
context: MailerRequestContext,
|
|
1703
|
+
) -> httpx.Headers:
|
|
1704
|
+
if isinstance(auth, BearerAuthStrategy):
|
|
1705
|
+
token = auth.token(context) if callable(auth.token) else auth.token
|
|
1706
|
+
resolved_token = _resolve_sync_value(token, "Auth token")
|
|
1707
|
+
headers = httpx.Headers()
|
|
1708
|
+
headers[auth.header_name] = f"{auth.prefix} {resolved_token}"
|
|
1709
|
+
return headers
|
|
1710
|
+
if isinstance(auth, HeadersAuthStrategy):
|
|
1711
|
+
value = auth.headers(context) if callable(auth.headers) else auth.headers
|
|
1712
|
+
return httpx.Headers(_resolve_sync_value(value, "Auth headers"))
|
|
1713
|
+
return httpx.Headers()
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
async def _resolve_async_auth_headers(
|
|
1717
|
+
auth: MailerAuthStrategy | bool,
|
|
1718
|
+
context: MailerRequestContext,
|
|
1719
|
+
) -> httpx.Headers:
|
|
1720
|
+
if isinstance(auth, BearerAuthStrategy):
|
|
1721
|
+
token = auth.token(context) if callable(auth.token) else auth.token
|
|
1722
|
+
resolved_token = await _resolve_async_value(token)
|
|
1723
|
+
headers = httpx.Headers()
|
|
1724
|
+
headers[auth.header_name] = f"{auth.prefix} {resolved_token}"
|
|
1725
|
+
return headers
|
|
1726
|
+
if isinstance(auth, HeadersAuthStrategy):
|
|
1727
|
+
value = auth.headers(context) if callable(auth.headers) else auth.headers
|
|
1728
|
+
return httpx.Headers(await _resolve_async_value(value))
|
|
1729
|
+
return httpx.Headers()
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def _run_sync_middleware_stack(
|
|
1733
|
+
middleware: Sequence[MailerMiddleware],
|
|
1734
|
+
context: MailerRequestContext,
|
|
1735
|
+
terminal: Any,
|
|
1736
|
+
) -> MailerResponseContext:
|
|
1737
|
+
handler = terminal
|
|
1738
|
+
for current in reversed(middleware):
|
|
1739
|
+
next_handler = handler
|
|
1740
|
+
|
|
1741
|
+
def wrapper(
|
|
1742
|
+
request_context: MailerRequestContext,
|
|
1743
|
+
current_middleware: MailerMiddleware = current,
|
|
1744
|
+
downstream: Any = next_handler,
|
|
1745
|
+
) -> MailerResponseContext:
|
|
1746
|
+
return _resolve_sync_value(
|
|
1747
|
+
current_middleware(request_context, downstream),
|
|
1748
|
+
"Middleware",
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
handler = wrapper
|
|
1752
|
+
return handler(context)
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
async def _run_async_middleware_stack(
|
|
1756
|
+
middleware: Sequence[MailerMiddleware],
|
|
1757
|
+
context: MailerRequestContext,
|
|
1758
|
+
terminal: Any,
|
|
1759
|
+
) -> MailerResponseContext:
|
|
1760
|
+
handler = terminal
|
|
1761
|
+
for current in reversed(middleware):
|
|
1762
|
+
next_handler = handler
|
|
1763
|
+
|
|
1764
|
+
async def wrapper(
|
|
1765
|
+
request_context: MailerRequestContext,
|
|
1766
|
+
current_middleware: MailerMiddleware = current,
|
|
1767
|
+
downstream: Any = next_handler,
|
|
1768
|
+
) -> MailerResponseContext:
|
|
1769
|
+
return await _resolve_async_value(current_middleware(request_context, downstream))
|
|
1770
|
+
|
|
1771
|
+
handler = wrapper
|
|
1772
|
+
return await handler(context)
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _resolve_sync_value(value: object, label: str) -> Any:
|
|
1776
|
+
if inspect.isawaitable(value):
|
|
1777
|
+
raise TypeError(f"{label} returned an awaitable in the sync Mailer client.")
|
|
1778
|
+
return value
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
async def _resolve_async_value(value: object) -> Any:
|
|
1782
|
+
if inspect.isawaitable(value):
|
|
1783
|
+
return await value
|
|
1784
|
+
return value
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
def _quote(value: str) -> str:
|
|
1788
|
+
from urllib.parse import quote
|
|
1789
|
+
|
|
1790
|
+
return quote(value, safe="")
|