sendstack 0.1.0__tar.gz → 0.1.2__tar.gz

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.
Files changed (30) hide show
  1. sendstack-0.1.2/PKG-INFO +585 -0
  2. sendstack-0.1.2/README.md +558 -0
  3. {sendstack-0.1.0 → sendstack-0.1.2}/pyproject.toml +3 -3
  4. sendstack-0.1.2/src/sendstack/__init__.py +135 -0
  5. sendstack-0.1.2/src/sendstack/client.py +1080 -0
  6. sendstack-0.1.2/src/sendstack/errors.py +112 -0
  7. sendstack-0.1.2/src/sendstack/files.py +83 -0
  8. sendstack-0.1.2/src/sendstack/types.py +364 -0
  9. {sendstack-0.1.0 → sendstack-0.1.2}/src/sendstack/utils.py +2 -2
  10. sendstack-0.1.2/src/sendstack.egg-info/PKG-INFO +585 -0
  11. {sendstack-0.1.0 → sendstack-0.1.2}/src/sendstack.egg-info/SOURCES.txt +3 -1
  12. sendstack-0.1.2/tests/test_conformance.py +108 -0
  13. sendstack-0.1.2/tests/test_files.py +86 -0
  14. sendstack-0.1.2/tests/test_sendstack.py +1014 -0
  15. sendstack-0.1.0/PKG-INFO +0 -725
  16. sendstack-0.1.0/README.md +0 -698
  17. sendstack-0.1.0/src/sendstack/__init__.py +0 -32
  18. sendstack-0.1.0/src/sendstack/client.py +0 -1790
  19. sendstack-0.1.0/src/sendstack/errors.py +0 -43
  20. sendstack-0.1.0/src/sendstack/types.py +0 -101
  21. sendstack-0.1.0/src/sendstack.egg-info/PKG-INFO +0 -725
  22. sendstack-0.1.0/tests/test_internal.py +0 -466
  23. sendstack-0.1.0/tests/test_sendstack.py +0 -1348
  24. {sendstack-0.1.0 → sendstack-0.1.2}/LICENSE +0 -0
  25. {sendstack-0.1.0 → sendstack-0.1.2}/setup.cfg +0 -0
  26. {sendstack-0.1.0 → sendstack-0.1.2}/src/sendstack/py.typed +0 -0
  27. {sendstack-0.1.0 → sendstack-0.1.2}/src/sendstack.egg-info/dependency_links.txt +0 -0
  28. {sendstack-0.1.0 → sendstack-0.1.2}/src/sendstack.egg-info/requires.txt +0 -0
  29. {sendstack-0.1.0 → sendstack-0.1.2}/src/sendstack.egg-info/top_level.txt +0 -0
  30. {sendstack-0.1.0 → sendstack-0.1.2}/tests/test_distribution_identity.py +0 -0
@@ -0,0 +1,585 @@
1
+ Metadata-Version: 2.4
2
+ Name: sendstack
3
+ Version: 0.1.2
4
+ Summary: Sync and async Python SDK for the SendStack email SaaS API.
5
+ Author: Noria Labs
6
+ License-Expression: MIT
7
+ Keywords: sendstack,email,transactional-email,webhooks,httpx,async,sdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Classifier: Topic :: Communications :: Email
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: httpx>=0.28.1
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
24
+ Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
25
+ Requires-Dist: ruff>=0.15.9; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # `sendstack`
29
+
30
+ Official Python SDK for the SendStack email SaaS API.
31
+
32
+ Built on `httpx`, it ships both a synchronous client (`Sendstack`) and an
33
+ asynchronous client (`AsyncSendstack`) covering the SendStack developer API.
34
+
35
+ Use it for:
36
+
37
+ - transactional and scheduled email
38
+ - batch email
39
+ - reusable attachment uploads
40
+ - sending domains
41
+ - email templates
42
+ - webhook endpoints and webhook event retries
43
+ - suppression lists
44
+
45
+ Python `>=3.11` is required.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install sendstack
51
+ ```
52
+
53
+ For local development:
54
+
55
+ ```bash
56
+ uv sync --extra dev
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ### Sync
62
+
63
+ ```python
64
+ import os
65
+
66
+ from sendstack import Sendstack, RequestOptions
67
+
68
+ token = os.environ["SENDSTACK_TOKEN"]
69
+
70
+ client = Sendstack(token)
71
+
72
+ message = client.emails.send(
73
+ {
74
+ "from": "Noria <hello@example.com>",
75
+ "to": "friend@example.com",
76
+ "subject": "Hello from SendStack",
77
+ "html": "<p>Your email pipeline is working.</p>",
78
+ "text": "Your email pipeline is working.",
79
+ },
80
+ RequestOptions(idempotency_key="welcome-email-1"),
81
+ )
82
+
83
+ print(message["id"], message["status"])
84
+ ```
85
+
86
+ `Sendstack` opens an `httpx.Client` for you. Close it when you are done, or use
87
+ it as a context manager:
88
+
89
+ ```python
90
+ with Sendstack(token) as client:
91
+ client.emails.send({"from": "hello@example.com", "to": "friend@example.com", "text": "Hi"})
92
+ ```
93
+
94
+ ### Async
95
+
96
+ ```python
97
+ import asyncio
98
+ import os
99
+
100
+ from sendstack import AsyncSendstack, RequestOptions
101
+
102
+
103
+ async def main() -> None:
104
+ async with AsyncSendstack(os.environ["SENDSTACK_TOKEN"]) as client:
105
+ message = await client.emails.send(
106
+ {
107
+ "from": "Noria <hello@example.com>",
108
+ "to": "friend@example.com",
109
+ "subject": "Hello from SendStack",
110
+ "text": "Your async pipeline is working.",
111
+ },
112
+ RequestOptions(idempotency_key="welcome-email-1"),
113
+ )
114
+ print(message["id"], message["status"])
115
+
116
+
117
+ asyncio.run(main())
118
+ ```
119
+
120
+ Every example below works on both clients. On `Sendstack` a method returns the
121
+ decoded payload; on `AsyncSendstack` the same call returns a coroutine you
122
+ `await`.
123
+
124
+ ### Base URL
125
+
126
+ The SDK defaults to `https://mailer.norialabs.com`, matching the current live
127
+ API. Override it when you move to the SendStack domain:
128
+
129
+ ```python
130
+ client = Sendstack(token, base_url="https://sendstack.norialabs.com")
131
+ ```
132
+
133
+ ## Docs Split
134
+
135
+ This README is the package guide: install, initialization, SDK methods, request
136
+ options, errors, and examples.
137
+
138
+ The SaaS docs remain the canonical source for product/API behavior: account
139
+ setup, API tokens, domain verification, DNS records, webhook event catalogs,
140
+ deliverability concepts, provider behavior, dashboard workflows, and the raw
141
+ HTTP API reference. Current live SaaS docs are at
142
+ `https://mailer.norialabs.com/api/docs`.
143
+
144
+ ## Auth
145
+
146
+ The API uses bearer auth:
147
+
148
+ ```http
149
+ Authorization: Bearer <token>
150
+ ```
151
+
152
+ Passing a token as the first argument configures that header automatically:
153
+
154
+ ```python
155
+ client = Sendstack("mlr_live_...")
156
+ ```
157
+
158
+ You can also pass a custom auth strategy. Tokens may be static or resolved per
159
+ request (sync or async callables both work):
160
+
161
+ ```python
162
+ from sendstack import BearerAuthStrategy, Sendstack
163
+
164
+ client = Sendstack(
165
+ base_url="https://mailer.norialabs.com",
166
+ auth=BearerAuthStrategy(token=lambda context: get_fresh_token()),
167
+ )
168
+ ```
169
+
170
+ Use `HeadersAuthStrategy` when the target environment expects custom headers,
171
+ and `RequestOptions(authenticated=False)` to strip auth for a single call.
172
+
173
+ ## Method Reference
174
+
175
+ | SDK method | HTTP route | Returns |
176
+ | --- | --- | --- |
177
+ | `attachments.upload(payload, options=None)` | `POST /attachments` | `UploadedAttachment` |
178
+ | `emails.send(payload, options=None)` | `POST /emails` | `SendEmailResult` |
179
+ | `emails.send_batch(payload, options=None)` | `POST /emails/batch` | `SendEmailBatchResult` |
180
+ | `emails.list(options=None, *, limit=None, cursor=None, status=None)` | `GET /emails` | `CursorPage` |
181
+ | `emails.get(message_id, options=None)` | `GET /emails/{id}` | `EmailMessage` |
182
+ | `emails.events(message_id, options=None)` | `GET /emails/{id}/events` | `CursorPage` |
183
+ | `emails.cancel(message_id, options=None)` | `POST /emails/{id}/cancel` | `EmailMessage` |
184
+ | `emails.requeue(message_id, options=None)` | `POST /emails/{id}/requeue` | `EmailMessage` |
185
+ | `domains.create(payload, options=None)` | `POST /domains` | `Domain` |
186
+ | `domains.list(options=None)` | `GET /domains` | `CursorPage` |
187
+ | `domains.get(domain_id, options=None)` | `GET /domains/{id}` | `Domain` |
188
+ | `domains.verify(domain_id, options=None)` | `POST /domains/{id}/verify` | `Domain` |
189
+ | `templates.create(payload, options=None)` | `POST /templates` | `EmailTemplate` |
190
+ | `templates.list(options=None)` | `GET /templates` | `CursorPage` |
191
+ | `templates.get(template_id, options=None)` | `GET /templates/{id}` | `EmailTemplate` |
192
+ | `templates.update(template_id, payload, options=None)` | `PATCH /templates/{id}` | `EmailTemplate` |
193
+ | `templates.remove(template_id, options=None)` | `DELETE /templates/{id}` | `None` |
194
+ | `webhooks.create(payload, options=None)` | `POST /webhook-endpoints` | `WebhookEndpoint` |
195
+ | `webhooks.list(options=None)` | `GET /webhook-endpoints` | `CursorPage` |
196
+ | `webhooks.update(webhook_id, payload, options=None)` | `PATCH /webhook-endpoints/{id}` | `WebhookEndpoint` |
197
+ | `webhooks.remove(webhook_id, options=None)` | `DELETE /webhook-endpoints/{id}` | `None` |
198
+ | `webhook_events.retry(event_id, options=None)` | `POST /events/{id}/retry` | `RetryWebhookEventResult` |
199
+ | `suppressions.add(payload, options=None)` | `POST /suppressions` | `CreateSuppressionResult` |
200
+ | `suppressions.list(options=None)` | `GET /suppressions` | `CursorPage` |
201
+ | `suppressions.remove(recipient, options=None)` | `DELETE /suppressions/{recipient}` | `None` |
202
+
203
+ `webhook_events` is also available as `webhookEvents`, and `emails.send_batch`
204
+ as `emails.sendBatch`, as convenience camelCase aliases.
205
+
206
+ Methods take a plain `dict` payload and return plain `dict` responses (the
207
+ `{"ok": true, "data": ...}` envelope is unwrapped for you). The exported model
208
+ types — `EmailMessage`, `Domain`, `WebhookEndpoint`, and friends — are optional
209
+ typing aids you can annotate with.
210
+
211
+ ## Emails
212
+
213
+ ```python
214
+ client.emails.send(
215
+ {
216
+ "from": "hello@example.com",
217
+ "to": ["a@example.com", "b@example.com"],
218
+ "reply_to": "support@example.com",
219
+ "subject": "Welcome",
220
+ "html": "<p>Hello</p>",
221
+ "text": "Hello",
222
+ "tags": [{"name": "campaign", "value": "welcome"}],
223
+ "metadata": {"account": "acct_123"},
224
+ "track_opens": True,
225
+ "track_clicks": True,
226
+ }
227
+ )
228
+ ```
229
+
230
+ `to`, `cc`, `bcc`, and `reply_to` accept a single address or a list.
231
+
232
+ Batch sends accept either a list or `{"emails": [...]}`:
233
+
234
+ ```python
235
+ client.emails.send_batch(
236
+ [
237
+ {"from": "hello@example.com", "to": "a@example.com", "subject": "One", "text": "First"},
238
+ {"from": "hello@example.com", "to": "b@example.com", "subject": "Two", "text": "Second"},
239
+ ]
240
+ )
241
+ ```
242
+
243
+ List, fetch, inspect events, then cancel or requeue:
244
+
245
+ ```python
246
+ page = client.emails.list(limit=25, status="queued")
247
+ message = client.emails.get("msg_123")
248
+ events = client.emails.events("msg_123")
249
+ client.emails.cancel("msg_123")
250
+ client.emails.requeue("msg_123")
251
+ ```
252
+
253
+ ### Scheduled email
254
+
255
+ `scheduled_at` accepts an ISO-8601 string or a `datetime` (serialized to
256
+ ISO-8601 with a `Z` suffix):
257
+
258
+ ```python
259
+ from datetime import datetime, timezone
260
+
261
+ client.emails.send(
262
+ {
263
+ "from": "hello@example.com",
264
+ "to": "friend@example.com",
265
+ "subject": "Later",
266
+ "text": "Scheduled.",
267
+ "scheduled_at": datetime(2026, 3, 28, 9, 0, tzinfo=timezone.utc),
268
+ }
269
+ )
270
+ ```
271
+
272
+ ## Attachments
273
+
274
+ ```python
275
+ attachment = client.attachments.upload(
276
+ {
277
+ "filename": "invoice.pdf",
278
+ "content_base64": invoice_pdf_base64,
279
+ "content_type": "application/pdf",
280
+ }
281
+ )
282
+
283
+ client.emails.send(
284
+ {
285
+ "from": "billing@example.com",
286
+ "to": "customer@example.com",
287
+ "subject": "Invoice",
288
+ "text": "Attached.",
289
+ "attachments": [
290
+ {"filename": "invoice.pdf", "attachment_id": attachment["attachment_id"]},
291
+ ],
292
+ }
293
+ )
294
+ ```
295
+
296
+ ### Reading from files
297
+
298
+ The API accepts strings (`html`/`text`) and base64 (`attachments`). These
299
+ stdlib-only helpers do the read-and-encode step so you don't repeat it:
300
+
301
+ ```python
302
+ from sendstack import (
303
+ Sendstack,
304
+ html_from_file,
305
+ text_from_file,
306
+ attachment_from_file,
307
+ attachment_from_bytes,
308
+ )
309
+
310
+ client.emails.send(
311
+ {
312
+ "from": "billing@example.com",
313
+ "to": "customer@example.com",
314
+ "subject": "Your invoice",
315
+ "html": html_from_file("templates/invoice.html"),
316
+ "text": text_from_file("templates/invoice.txt"),
317
+ "attachments": [
318
+ # From a path: filename defaults to the basename, content is base64-encoded.
319
+ attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
320
+ # From in-memory bytes (e.g. a generated PDF): filename is required.
321
+ attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
322
+ ],
323
+ }
324
+ )
325
+ ```
326
+
327
+ - `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
328
+ file into a string.
329
+ - `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
330
+ — read a file into a base64 attachment `dict`. `filename` defaults to the basename.
331
+ `path` accepts a `str` or any `os.PathLike`.
332
+ - `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
333
+ — encode in-memory `bytes`; `filename` is required.
334
+
335
+ ## Domains
336
+
337
+ ```python
338
+ domain = client.domains.create(
339
+ {
340
+ "domain": "example.com",
341
+ "region": "af-south-1",
342
+ "tls": "enforced",
343
+ "capabilities": {"sending": "enabled"},
344
+ }
345
+ )
346
+
347
+ client.domains.verify(domain["id"])
348
+ ```
349
+
350
+ ## Templates
351
+
352
+ ```python
353
+ template = client.templates.create(
354
+ {
355
+ "name": "Welcome",
356
+ "slug": "welcome",
357
+ "subject": "Welcome, {{firstName}}",
358
+ "html": "<p>Hello {{firstName}}</p>",
359
+ "text": "Hello {{firstName}}",
360
+ }
361
+ )
362
+
363
+ client.emails.send(
364
+ {
365
+ "from": "hello@example.com",
366
+ "to": "friend@example.com",
367
+ "template": {"id": template["id"], "variables": {"firstName": "Amina"}},
368
+ }
369
+ )
370
+ ```
371
+
372
+ ## Webhooks
373
+
374
+ ```python
375
+ endpoint = client.webhooks.create(
376
+ {
377
+ "url": "https://example.com/webhooks/sendstack",
378
+ "event_types": ["email.sent", "email.failed"],
379
+ }
380
+ )
381
+
382
+ client.webhook_events.retry("event_123")
383
+ client.webhooks.update(endpoint["id"], {"enabled": False})
384
+ ```
385
+
386
+ ## Suppressions
387
+
388
+ ```python
389
+ client.suppressions.add({"recipient": "bad@example.com", "reason": "manual"})
390
+
391
+ suppressions = client.suppressions.list()
392
+ client.suppressions.remove("bad@example.com")
393
+ ```
394
+
395
+ ## Field Aliases
396
+
397
+ Python users write idiomatic snake_case, which is exactly the wire format for
398
+ email, attachment, webhook, and domain fields — `reply_to`, `track_opens`,
399
+ `track_clicks`, `provider_id`, `template_id`, `template_data`, `scheduled_at`,
400
+ `content_base64`, `content_type`, `attachment_id`, `content_id`, `event_types`,
401
+ `custom_return_path`. No translation needed.
402
+
403
+ The camelCase aliases (`replyTo`, `trackOpens`,
404
+ `trackClicks`, `providerId`, `templateId`, `templateData`, `scheduledAt`,
405
+ `contentBase64`, `attachmentId`, `eventTypes`, …) are also accepted and
406
+ converted to the wire field names. Everything else is passed through unchanged,
407
+ so the SDK stays forward-compatible with new API fields.
408
+
409
+ ## Request Options
410
+
411
+ All methods accept a `RequestOptions`. Mutating methods also honor
412
+ `idempotency_key`.
413
+
414
+ ```python
415
+ from sendstack import RequestOptions
416
+
417
+ client.emails.send(
418
+ {
419
+ "from": "hello@example.com",
420
+ "to": "friend@example.com",
421
+ "subject": "Hello",
422
+ "text": "Hello",
423
+ },
424
+ RequestOptions(
425
+ idempotency_key="email-123",
426
+ timeout_seconds=10.0,
427
+ query={"debug": True},
428
+ headers={"x-tenant-id": "tenant_123"},
429
+ ),
430
+ )
431
+ ```
432
+
433
+ Supported options:
434
+
435
+ - `headers`: extra headers (merged over constructor headers)
436
+ - `query`: per-request query params (merged over constructor query)
437
+ - `timeout_seconds`: request timeout, default `30.0`
438
+ - `authenticated`: set `False` to strip auth headers for a request
439
+ - `auth`: a `BearerAuthStrategy` / `HeadersAuthStrategy`, or `False`
440
+ - `retry`: a `RetryOptions`, an attempt count, or `False`
441
+ - `middleware`: request/response middleware (runs after constructor middleware)
442
+ - `parse_response`: custom response parser
443
+ - `transform_response`: custom response transformer
444
+ - `unwrap_data`: unwrap `{"ok": true, "data": ...}` envelopes, default `True`
445
+ - `client`: a per-request `httpx` client
446
+ - `idempotency_key`: sets the `Idempotency-Key` header
447
+ - `body`: raw body for `request(...)`
448
+
449
+ ## Retries
450
+
451
+ Retries are off by default (a single attempt). Enable them per request or on the
452
+ client:
453
+
454
+ ```python
455
+ from sendstack import RequestOptions, RetryOptions
456
+
457
+ client.emails.list(
458
+ RequestOptions(retry=RetryOptions(max_attempts=3, delay_seconds=0.5))
459
+ )
460
+
461
+ # Or just an attempt count:
462
+ client.emails.list(RequestOptions(retry=3))
463
+ ```
464
+
465
+ Default retry behavior:
466
+
467
+ - retries network exceptions, unless they are already a `SendstackError`
468
+ - retries responses only for `408`, `425`, `429`, `500`, `502`, `503`, `504`
469
+ - uses a short exponential backoff when no custom `delay_seconds` is given
470
+
471
+ `RetryOptions` accepts `delay_seconds` and `should_retry` as values or callables
472
+ (sync or async).
473
+
474
+ ## Custom `httpx` Clients
475
+
476
+ Inject your own `httpx.Client` or `httpx.AsyncClient`:
477
+
478
+ ```python
479
+ import httpx
480
+
481
+ from sendstack import Sendstack
482
+
483
+ client = Sendstack("mlr_live_...", client=httpx.Client(timeout=5.0))
484
+ ```
485
+
486
+ If you inject a client, the SDK uses it but does not close it for you.
487
+
488
+ ## Middleware
489
+
490
+ ```python
491
+ from sendstack import Sendstack
492
+
493
+
494
+ def add_sdk_header(context, next_call):
495
+ context.headers["x-sdk"] = "sendstack-python"
496
+ return next_call(context)
497
+
498
+
499
+ client = Sendstack("mlr_live_...", middleware=[add_sdk_header])
500
+ ```
501
+
502
+ Middleware can mutate headers, rewrite the final URL, or short-circuit a request
503
+ by returning a response context without calling `next_call`.
504
+
505
+ ## Lower-Level Request
506
+
507
+ Every resource method uses `request(...)` internally. Use it directly for new
508
+ API routes before the SDK grows a typed wrapper:
509
+
510
+ ```python
511
+ from sendstack import RequestOptions
512
+
513
+ result = client.request("GET", "/emails", RequestOptions(query={"limit": 25, "status": "queued"}))
514
+ ```
515
+
516
+ Pass `RequestOptions(unwrap_data=False)` if you need the raw `{"ok", "data"}`
517
+ envelope.
518
+
519
+ ## Errors
520
+
521
+ Failed responses raise `SendstackError`.
522
+
523
+ ```python
524
+ from sendstack import SendstackError
525
+
526
+ try:
527
+ client.emails.send({"from": "hello@example.com", "to": "bad", "subject": "Hi", "text": "Hi"})
528
+ except SendstackError as error:
529
+ print(error.status_code, error.code, error.message, error.details)
530
+ ```
531
+
532
+ `SendstackError` includes:
533
+
534
+ - `status_code`
535
+ - `code`
536
+ - `details`
537
+ - `response_body`
538
+ - `message`
539
+
540
+ It understands SendStack envelopes (`{"ok": false, "error": {...}}`) as well as
541
+ FastAPI-style `{"detail": "..."}` bodies.
542
+
543
+ ## Exports
544
+
545
+ Clients and errors:
546
+
547
+ - `Sendstack`, `AsyncSendstack`
548
+ - `SendstackClient`, `AsyncSendstackClient` (aliases)
549
+ - `SendstackError`
550
+ - `DEFAULT_BASE_URL`
551
+
552
+ Filesystem helpers:
553
+
554
+ - `html_from_file`, `text_from_file`
555
+ - `attachment_from_file`, `attachment_from_bytes`
556
+
557
+ Auth, options, and machinery:
558
+
559
+ - `BearerAuthStrategy`, `HeadersAuthStrategy`, `SendstackAuthStrategy`
560
+ - `RequestOptions`, `RetryOptions`
561
+ - `SendstackMiddleware`, `ResponseParser`, `ResponseTransformer`
562
+ - `SendstackRequestContext`, `SendstackResponseContext`, `SendstackRetryContext`
563
+
564
+ Model types (optional typing aids):
565
+
566
+ - `Recipient`, `SendstackTag`, `TemplateReference`
567
+ - `SendEmailRequest`, `SendEmailResult`, `SendEmailBatchResult`
568
+ - `EmailMessage`, `EmailEvent`, `EmailStatus`
569
+ - `UploadAttachmentRequest`, `UploadedAttachment`
570
+ - `CreateDomainRequest`, `Domain`, `DomainRegion`, `DomainTlsPolicy`, `DomainCapability`
571
+ - `CreateTemplateRequest`, `UpdateTemplateRequest`, `EmailTemplate`
572
+ - `CreateWebhookEndpointRequest`, `UpdateWebhookEndpointRequest`, `WebhookEndpoint`
573
+ - `WebhookEventType`, `KnownWebhookEvent`
574
+ - `RetryWebhookEventResult`
575
+ - `CreateSuppressionRequest`, `CreateSuppressionResult`, `Suppression`, `SuppressionReason`
576
+ - `CursorPage`
577
+
578
+ ## Development
579
+
580
+ ```bash
581
+ uv sync --extra dev
582
+ uv run ruff check .
583
+ uv run pytest
584
+ uv build
585
+ ```