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/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="")