From 1a45b6265f71b90c33b5314555d5ff28d162fe3f Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld Date: Tue, 29 Sep 2026 21:11:40 +0200 Subject: [PATCH 1/3] feat(api): align sandbox responses with OpenAPI --- src/lettermint/endpoints/api.py | 52 -- src/lettermint/endpoints/email.py | 97 +- src/lettermint/types.py | 1403 ++++++++++++++--------------- tests/test_api_surface.py | 72 +- tests/test_email.py | 128 +-- 5 files changed, 747 insertions(+), 1005 deletions(-) diff --git a/src/lettermint/endpoints/api.py b/src/lettermint/endpoints/api.py index 8d52cc3..d35a6b9 100644 --- a/src/lettermint/endpoints/api.py +++ b/src/lettermint/endpoints/api.py @@ -78,32 +78,6 @@ def retrieve(self, message_id: str, query: Query | None = None) -> lm_types.Mess ), ) - def reschedule( - self, message_id: str, data: lm_types.RescheduleMessageRequest - ) -> lm_types.RescheduleMessageResponse: - return cast( - lm_types.RescheduleMessageResponse, - self._client.patch( - self._path("/messages/{messageId}", messageId=message_id), data=data - ), - ) - - def cancel(self, message_id: str) -> lm_types.RescheduleMessageResponse: - return cast( - lm_types.RescheduleMessageResponse, - self._client.post( - self._path("/messages/{messageId}/cancel", messageId=message_id), data={} - ), - ) - - def process(self, message_id: str) -> lm_types.ProcessInboundMessageResponse: - return cast( - lm_types.ProcessInboundMessageResponse, - self._client.post( - self._path("/messages/{messageId}/process", messageId=message_id), data={} - ), - ) - def events(self, message_id: str, query: Query | None = None) -> lm_types.MessageEventsResponse: return cast( lm_types.MessageEventsResponse, @@ -364,32 +338,6 @@ async def retrieve( ), ) - async def reschedule( - self, message_id: str, data: lm_types.RescheduleMessageRequest - ) -> lm_types.RescheduleMessageResponse: - return cast( - lm_types.RescheduleMessageResponse, - await self._client.patch( - self._path("/messages/{messageId}", messageId=message_id), data=data - ), - ) - - async def cancel(self, message_id: str) -> lm_types.RescheduleMessageResponse: - return cast( - lm_types.RescheduleMessageResponse, - await self._client.post( - self._path("/messages/{messageId}/cancel", messageId=message_id), data={} - ), - ) - - async def process(self, message_id: str) -> lm_types.ProcessInboundMessageResponse: - return cast( - lm_types.ProcessInboundMessageResponse, - await self._client.post( - self._path("/messages/{messageId}/process", messageId=message_id), data={} - ), - ) - async def delete(self, domain_id: str) -> lm_types.DomainDestroyResponse: return cast( lm_types.DomainDestroyResponse, diff --git a/src/lettermint/endpoints/email.py b/src/lettermint/endpoints/email.py index d050ebe..31e4cae 100644 --- a/src/lettermint/endpoints/email.py +++ b/src/lettermint/endpoints/email.py @@ -12,8 +12,7 @@ else: from typing_extensions import Self -from ..message_tag import MessageTag, normalize_message_tags -from ..types import SendBatchEmailResponse, SendBatchMailRequest, SendEmailResponse, TlsPolicy +from ..types import SandboxResult, SendBatchEmailResponse, SendEmailResponse from .endpoint import AsyncEndpoint, Endpoint if TYPE_CHECKING: @@ -129,11 +128,6 @@ def subject(self, subject: str) -> Self: self._payload["subject"] = subject return self - def scheduled_at(self, scheduled_at: str) -> Self: - """Set the requested delivery time for the email.""" - self._payload["scheduled_at"] = scheduled_at - return self - def html(self, html: str | None) -> Self: """Set the HTML body of the email. @@ -213,7 +207,6 @@ def attach( filename: str, content: str, content_id: str | None = None, - content_type: str | None = None, ) -> Self: """Attach a file to the email. @@ -221,7 +214,6 @@ def attach( filename: The attachment filename. content: The base64-encoded file content. content_id: Optional Content-ID for inline attachments. - content_type: Optional MIME type for the attachment. Returns: The current instance for method chaining. @@ -241,17 +233,10 @@ def attach( } if content_id is not None: attachment["content_id"] = content_id - if content_type is not None: - attachment["content_type"] = content_type self._payload["attachments"].append(attachment) return self - def settings(self, settings: dict[str, bool | TlsPolicy]) -> Self: - """Set per-email settings that override the selected route.""" - self._payload["settings"] = settings - return self - def metadata(self, metadata: dict[str, str]) -> Self: """Set metadata for the email. @@ -279,24 +264,12 @@ def tag(self, tag: str) -> Self: Example: >>> client.email.tag("welcome-campaign") """ - if len(self._payload.get("tags", [])) >= 20: - raise ValueError("A legacy tag and no more than 19 message tags are permitted") self._payload["tag"] = tag return self - def tags(self, tags: list[MessageTag | dict[str, str]]) -> Self: - """Set reusable name-value tags for the email. - - Dictionaries remain supported for backward compatibility. - """ - maximum = 19 if self._payload.get("tag") is not None else 20 - if len(tags) > maximum: - raise ValueError(f"No more than {maximum} message tags are permitted") - normalized = [tag if isinstance(tag, MessageTag) else MessageTag(**tag) for tag in tags] - names = [tag.name for tag in normalized] - if len(names) != len(set(names)): - raise ValueError("Message tag names must be unique and case-sensitive") - self._payload["tags"] = [tag.to_dict() for tag in normalized] + def sandbox_result(self, result: SandboxResult) -> Self: + """Select the simulated result for a Sandbox project.""" + self._payload["sandbox_result"] = result return self def send(self) -> SendEmailResponse: @@ -329,21 +302,10 @@ def send(self) -> SendEmailResponse: finally: self._reset() - def send_batch(self, payload: SendBatchMailRequest) -> SendBatchEmailResponse: + def send_batch(self, payload: list[dict[str, Any]]) -> SendBatchEmailResponse: """Send multiple emails in one batch.""" - headers: dict[str, str] | None = None - if self._idempotency_key is not None: - headers = {"Idempotency-Key": self._idempotency_key} - - try: - response: SendBatchEmailResponse = self._client.post( - "/send/batch", - data=normalize_message_tags(payload), - headers=headers, - ) - return response - finally: - self._reset() + response: SendBatchEmailResponse = self._client.post("/send/batch", data=payload) + return response def ping(self) -> str: """Ping the Sending API.""" @@ -442,11 +404,6 @@ def subject(self, subject: str) -> Self: self._payload["subject"] = subject return self - def scheduled_at(self, scheduled_at: str) -> Self: - """Set the requested delivery time for the email.""" - self._payload["scheduled_at"] = scheduled_at - return self - def html(self, html: str | None) -> Self: """Set the HTML body of the email. @@ -526,7 +483,6 @@ def attach( filename: str, content: str, content_id: str | None = None, - content_type: str | None = None, ) -> Self: """Attach a file to the email. @@ -534,7 +490,6 @@ def attach( filename: The attachment filename. content: The base64-encoded file content. content_id: Optional Content-ID for inline attachments. - content_type: Optional MIME type for the attachment. Returns: The current instance for method chaining. @@ -548,17 +503,10 @@ def attach( } if content_id is not None: attachment["content_id"] = content_id - if content_type is not None: - attachment["content_type"] = content_type self._payload["attachments"].append(attachment) return self - def settings(self, settings: dict[str, bool | TlsPolicy]) -> Self: - """Set per-email settings that override the selected route.""" - self._payload["settings"] = settings - return self - def metadata(self, metadata: dict[str, str]) -> Self: """Set metadata for the email. @@ -580,24 +528,12 @@ def tag(self, tag: str) -> Self: Returns: The current instance for method chaining. """ - if len(self._payload.get("tags", [])) >= 20: - raise ValueError("A legacy tag and no more than 19 message tags are permitted") self._payload["tag"] = tag return self - def tags(self, tags: list[MessageTag | dict[str, str]]) -> Self: - """Set reusable name-value tags for the email. - - Dictionaries remain supported for backward compatibility. - """ - maximum = 19 if self._payload.get("tag") is not None else 20 - if len(tags) > maximum: - raise ValueError(f"No more than {maximum} message tags are permitted") - normalized = [tag if isinstance(tag, MessageTag) else MessageTag(**tag) for tag in tags] - names = [tag.name for tag in normalized] - if len(names) != len(set(names)): - raise ValueError("Message tag names must be unique and case-sensitive") - self._payload["tags"] = [tag.to_dict() for tag in normalized] + def sandbox_result(self, result: SandboxResult) -> Self: + """Select the simulated result for a Sandbox project.""" + self._payload["sandbox_result"] = result return self def send(self) -> Coroutine[Any, Any, SendEmailResponse]: @@ -628,18 +564,9 @@ async def _send() -> SendEmailResponse: return _send() - async def send_batch(self, payload: SendBatchMailRequest) -> SendBatchEmailResponse: + async def send_batch(self, payload: list[dict[str, Any]]) -> SendBatchEmailResponse: """Send multiple emails in one batch asynchronously.""" - headers: dict[str, str] | None = None - if self._idempotency_key is not None: - headers = {"Idempotency-Key": self._idempotency_key} - self._reset() - - response: SendBatchEmailResponse = await self._client.post( - "/send/batch", - data=normalize_message_tags(payload), - headers=headers, - ) + response: SendBatchEmailResponse = await self._client.post("/send/batch", data=payload) return response async def ping(self) -> str: diff --git a/src/lettermint/types.py b/src/lettermint/types.py index 4b012f5..7a3d8b2 100644 --- a/src/lettermint/types.py +++ b/src/lettermint/types.py @@ -2,836 +2,764 @@ from __future__ import annotations -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias -MessageStatus: TypeAlias = Literal[ - "scheduled", - "pending", - "queued", - "quarantined", - "suppressed", - "processed", - "delivered", - "opened", - "clicked", - "soft_bounced", - "hard_bounced", - "spam_complaint", - "failed", - "blocked", - "policy_rejected", - "unsubscribed", - "canceled", -] -TlsPolicy: TypeAlias = Literal["opportunistic", "enforced"] +MessageStatus: TypeAlias = Literal['scheduled', 'pending', 'queued', 'quarantined', 'suppressed', 'processed', 'delivered', 'opened', 'clicked', 'soft_bounced', 'hard_bounced', 'spam_complaint', 'failed', 'blocked', 'policy_rejected', 'unsubscribed', 'canceled'] +SandboxResult: TypeAlias = Literal['delivered', 'hard_bounced', 'soft_bounced', 'deferred', 'failed', 'suppressed', 'spam_complaint', 'auto_replied', 'opened', 'clicked', 'unsubscribed'] +TlsPolicy: TypeAlias = Literal['opportunistic', 'enforced'] SendMailRequest = TypedDict( - "SendMailRequest", - { - "route": "NotRequired[str]", - "from": "Required[str]", - "to": "Required[list[str]]", - "cc": "NotRequired[list[str]]", - "bcc": "NotRequired[list[str]]", - "reply_to": "NotRequired[list[str]]", - "subject": "Required[str]", - "scheduled_at": "NotRequired[str]", - "headers": "NotRequired[dict[str, str]]", - "metadata": "NotRequired[dict[str, str]]", - "tag": "NotRequired[str | None]", - "tags": "NotRequired[list[dict[str, Any]]]", - "settings": "NotRequired[dict[str, Any] | None]", - "html": "NotRequired[str | None]", - "text": "NotRequired[str | None]", - "attachments": "NotRequired[list[dict[str, Any]]]", - }, -) - -SendBatchMailRequest: TypeAlias = list[dict[str, Any]] -AttachmentDelivery: TypeAlias = Literal["inline", "url"] -BuiltInTeamRole: TypeAlias = Literal["owner", "admin", "member"] -CursorPaginator = TypedDict( - "CursorPaginator", - { - "data": "Required[list[str]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", - }, -) - -DkimMode: TypeAlias = Literal["legacy_txt", "managed_cname"] -DnsRecordPurpose: TypeAlias = Literal[ - "return_path", "dmarc", "dkim_legacy", "dkim_primary", "dkim_secondary" -] -DnsRecordStatus: TypeAlias = Literal["active", "failed", "pending"] -DnsVerificationScope: TypeAlias = Literal["required", "recommended", "migration", "deprecated"] -RecordType: TypeAlias = Literal["TXT", "CNAME", "MX"] + 'SendMailRequest', + { + 'route': 'NotRequired[str]', + 'from': 'Required[str]', + 'to': 'Required[list[str]]', + 'cc': 'NotRequired[list[str]]', + 'bcc': 'NotRequired[list[str]]', + 'reply_to': 'NotRequired[list[str]]', + 'subject': 'Required[str]', + 'scheduled_at': 'NotRequired[str]', + 'headers': 'NotRequired[dict[str, str]]', + 'metadata': 'NotRequired[dict[str, str]]', + 'tag': 'NotRequired[str | None]', + 'tags': 'NotRequired[list[dict[str, Any]]]', + 'settings': 'NotRequired[dict[str, Any] | None]', + 'html': 'NotRequired[str | None]', + 'text': 'NotRequired[str | None]', + 'attachments': 'NotRequired[list[dict[str, Any]]]', + 'sandbox_result': 'NotRequired[SandboxResult]', + }, +) + +SendBatchMailRequest: TypeAlias = list[SendMailRequest] +AttachmentDelivery: TypeAlias = Literal['inline', 'url'] +BuiltInTeamRole: TypeAlias = Literal['owner', 'admin', 'member'] +DkimMode: TypeAlias = Literal['legacy_txt', 'managed_cname'] +DnsRecordPurpose: TypeAlias = Literal['return_path', 'dmarc', 'dkim_legacy', 'dkim_primary', 'dkim_secondary'] +DnsRecordStatus: TypeAlias = Literal['active', 'failed', 'pending'] +DnsVerificationScope: TypeAlias = Literal['required', 'recommended', 'migration', 'deprecated'] +RecordType: TypeAlias = Literal['TXT', 'CNAME', 'MX'] DomainDnsRecordData = TypedDict( - "DomainDnsRecordData", + 'DomainDnsRecordData', { - "id": "Required[str]", - "type": "Required[RecordType]", - "hostname": "Required[str]", - "fqdn": "Required[str]", - "content": "Required[str]", - "status": "Required[DnsRecordStatus]", - "purpose": "Required[DnsRecordPurpose]", - "verification_scope": "Required[DnsVerificationScope]", - "required_for_verification": "Required[bool]", - "verified_at": "Required[str | None]", - "last_checked_at": "Required[str | None]", + 'id': 'Required[str]', + 'type': 'Required[RecordType]', + 'hostname': 'Required[str]', + 'fqdn': 'Required[str]', + 'content': 'Required[str]', + 'status': 'Required[DnsRecordStatus]', + 'purpose': 'Required[DnsRecordPurpose]', + 'verification_scope': 'Required[DnsVerificationScope]', + 'required_for_verification': 'Required[bool]', + 'verified_at': 'Required[str | None]', + 'last_checked_at': 'Required[str | None]', }, ) DomainData = TypedDict( - "DomainData", + 'DomainData', { - "id": "Required[str]", - "domain": "Required[str]", - "dkim_mode": "Required[DkimMode]", - "rotation_ready": "Required[bool]", - "status_changed_at": "Required[str | None]", - "dns_records": "NotRequired[list[DomainDnsRecordData]]", - "projects": "NotRequired[list[dict[str, Any]]]", - "created_at": "Required[str]", + 'id': 'Required[str]', + 'domain': 'Required[str]', + 'dkim_mode': 'Required[DkimMode]', + 'rotation_ready': 'Required[bool]', + 'status_changed_at': 'Required[str | None]', + 'dns_records': 'NotRequired[list[DomainDnsRecordData]]', + 'projects': 'NotRequired[list[dict[str, Any]]]', + 'created_at': 'Required[str]', }, ) -DomainStatus: TypeAlias = Literal[ - "verified", "partially_verified", "pending_verification", "failed_verification" -] +DomainStatus: TypeAlias = Literal['verified', 'partially_verified', 'pending_verification', 'failed_verification'] DomainListData = TypedDict( - "DomainListData", + 'DomainListData', { - "id": "Required[str]", - "domain": "Required[str]", - "status": "Required[DomainStatus]", - "dkim_mode": "Required[DkimMode]", - "status_changed_at": "Required[str | None]", - "created_at": "Required[str]", + 'id': 'Required[str]', + 'domain': 'Required[str]', + 'status': 'Required[DomainStatus]', + 'dkim_mode': 'Required[DkimMode]', + 'status_changed_at': 'Required[str | None]', + 'created_at': 'Required[str]', }, ) -InitialRoutes: TypeAlias = Literal["both", "transactional", "broadcast"] +InitialRoutes: TypeAlias = Literal['both', 'transactional', 'broadcast'] MessageAttachmentData = TypedDict( - "MessageAttachmentData", + 'MessageAttachmentData', { - "size": "Required[int]", - "filename": "Required[str]", - "content_id": "Required[str | None]", - "content_type": "Required[str]", + 'size': 'Required[int]', + 'filename': 'Required[str]', + 'content_id': 'Required[str | None]', + 'content_type': 'Required[str]', }, ) +DeliveryMode: TypeAlias = Literal['live', 'sandbox'] MessageRecipientData = TypedDict( - "MessageRecipientData", + 'MessageRecipientData', { - "email": "Required[str]", - "name": "Required[str | None]", + 'email': 'Required[str]', + 'name': 'Required[str | None]', + 'sandbox_result': 'Required[SandboxResult | None]', }, ) -MessageType: TypeAlias = Literal["inbound", "outbound"] SpamSymbol = TypedDict( - "SpamSymbol", + 'SpamSymbol', { - "name": "Required[str]", - "score": "Required[float]", - "options": "Required[list[str]]", - "description": "Required[str | None]", + 'name': 'Required[str]', + 'score': 'Required[float]', + 'options': 'Required[list[str]]', + 'description': 'Required[str | None]', }, ) +MessageType: TypeAlias = Literal['inbound', 'outbound'] MessageData = TypedDict( - "MessageData", - { - "id": "Required[str]", - "type": "Required[MessageType]", - "status": "Required[MessageStatus]", - "status_changed_at": "Required[str | None]", - "scheduled_at": "Required[str | None]", - "tag": "Required[str | None]", - "tags": "Required[list[dict[str, Any]]]", - "from_email": "Required[str]", - "from_name": "Required[str | None]", - "reply_to": "Required[list[str] | None]", - "subject": "Required[str | None]", - "to": "Required[list[MessageRecipientData] | None]", - "cc": "Required[list[MessageRecipientData] | None]", - "bcc": "Required[list[MessageRecipientData] | None]", - "attachments": "Required[list[MessageAttachmentData] | None]", - "metadata": "Required[dict[str, str] | None]", - "spam_score": "NotRequired[float | None]", - "spam_symbols": "NotRequired[list[SpamSymbol]]", - "route_id": "Required[str]", - "created_at": "Required[str]", - }, -) - -MessageEventType: TypeAlias = Literal[ - "scheduled", - "rescheduled", - "canceled", - "released", - "queued", - "processed", - "suppressed", - "delivered", - "auto_replied", - "soft_bounced", - "hard_bounced", - "spam_complaint", - "failed", - "blocked", - "policy_rejected", - "unsubscribed", - "opened", - "clicked", - "inbound_received", - "inbound_queued", - "inbound_spam_blocked", - "inbound_released", - "inbound_processed", - "inbound_retry", -] + 'MessageData', + { + 'id': 'Required[str]', + 'type': 'Required[MessageType]', + 'status': 'Required[MessageStatus]', + 'status_changed_at': 'Required[str | None]', + 'scheduled_at': 'Required[str | None]', + 'tag': 'Required[str | None]', + 'tags': 'Required[list[dict[str, Any]]]', + 'from_email': 'Required[str]', + 'from_name': 'Required[str | None]', + 'reply_to': 'Required[list[str] | None]', + 'subject': 'Required[str | None]', + 'to': 'Required[list[MessageRecipientData] | None]', + 'cc': 'Required[list[MessageRecipientData] | None]', + 'bcc': 'Required[list[MessageRecipientData] | None]', + 'attachments': 'Required[list[MessageAttachmentData] | None]', + 'metadata': 'Required[dict[str, str] | None]', + 'spam_score': 'NotRequired[float | None]', + 'spam_symbols': 'NotRequired[list[SpamSymbol]]', + 'route_id': 'Required[str]', + 'created_at': 'Required[str]', + 'delivery_mode': 'Required[DeliveryMode]', + 'sandbox_result': 'Required[SandboxResult | None]', + }, +) + +MessageEventType: TypeAlias = Literal['scheduled', 'rescheduled', 'canceled', 'released', 'queued', 'processed', 'suppressed', 'delivered', 'auto_replied', 'soft_bounced', 'hard_bounced', 'spam_complaint', 'failed', 'blocked', 'policy_rejected', 'unsubscribed', 'opened', 'clicked', 'inbound_received', 'inbound_queued', 'inbound_spam_blocked', 'inbound_released', 'inbound_processed', 'inbound_retry'] MessageEventData = TypedDict( - "MessageEventData", + 'MessageEventData', { - "message_id": "Required[str]", - "event": "Required[MessageEventType]", - "tag": "Required[str | None]", - "tags": "Required[list[dict[str, Any]]]", - "metadata": "Required[dict[str, Any] | None]", - "timestamp": "Required[str]", + 'message_id': 'Required[str]', + 'event': 'Required[MessageEventType]', + 'tag': 'Required[str | None]', + 'tags': 'Required[list[dict[str, Any]]]', + 'metadata': 'Required[dict[str, Any] | None]', + 'timestamp': 'Required[str]', }, ) MessageListData = TypedDict( - "MessageListData", - { - "id": "Required[str]", - "type": "Required[MessageType]", - "status": "Required[MessageStatus]", - "scheduled_at": "Required[str | None]", - "spam_score": "NotRequired[float | None]", - "from_email": "Required[str]", - "from_name": "Required[str | None]", - "subject": "Required[str | None]", - "to": "Required[list[MessageRecipientData] | None]", - "cc": "Required[list[MessageRecipientData] | None]", - "bcc": "Required[list[MessageRecipientData] | None]", - "reply_to": "Required[list[str] | None]", - "tag": "Required[str | None]", - "tags": "Required[list[dict[str, Any]]]", - "status_changed_at": "Required[str | None]", - "created_at": "Required[str]", + 'MessageListData', + { + 'id': 'Required[str]', + 'type': 'Required[MessageType]', + 'status': 'Required[MessageStatus]', + 'scheduled_at': 'Required[str | None]', + 'spam_score': 'NotRequired[float | None]', + 'from_email': 'Required[str]', + 'from_name': 'Required[str | None]', + 'subject': 'Required[str | None]', + 'to': 'Required[list[MessageRecipientData] | None]', + 'cc': 'Required[list[MessageRecipientData] | None]', + 'bcc': 'Required[list[MessageRecipientData] | None]', + 'reply_to': 'Required[list[str] | None]', + 'tag': 'Required[str | None]', + 'tags': 'Required[list[dict[str, Any]]]', + 'status_changed_at': 'Required[str | None]', + 'created_at': 'Required[str]', + 'delivery_mode': 'Required[DeliveryMode]', + 'sandbox_result': 'Required[SandboxResult | None]', }, ) MessageStatsData = TypedDict( - "MessageStatsData", + 'MessageStatsData', { - "messages_transactional": "Required[int]", - "messages_broadcast": "Required[int]", - "messages_inbound": "Required[int]", - "deliverability": "Required[float]", + 'messages_transactional': 'Required[int]', + 'messages_broadcast': 'Required[int]', + 'messages_inbound': 'Required[int]', + 'deliverability': 'Required[float]', }, ) -Plan: TypeAlias = Literal["free", "starter", "growth", "pro"] -ProjectAccessScope: TypeAlias = Literal["all", "selected"] +Plan: TypeAlias = Literal['free', 'starter', 'growth', 'pro'] +ProjectAccessScope: TypeAlias = Literal['all', 'selected'] RouteStatisticData = TypedDict( - "RouteStatisticData", - { - "date": "Required[str]", - "sent_count": "Required[int]", - "delivered_count": "Required[int]", - "opened_count": "Required[int]", - "clicked_count": "Required[int]", - "hard_bounce_count": "Required[int]", - "spam_complaint_count": "Required[int]", - "inbound_received_count": "Required[int]", - "observed_opened_count": "NotRequired[int | None]", - "human_opened_count": "NotRequired[int | None]", - "privacy_opened_count": "NotRequired[int | None]", - "effective_opened_count": "Required[int | None]", - "machine_opened_count": "Required[int | None]", - "machine_clicked_count": "Required[int | None]", - }, -) - -RouteType: TypeAlias = Literal["transactional", "broadcast", "inbound"] + 'RouteStatisticData', + { + 'date': 'Required[str]', + 'sent_count': 'Required[int]', + 'delivered_count': 'Required[int]', + 'opened_count': 'Required[int]', + 'clicked_count': 'Required[int]', + 'hard_bounce_count': 'Required[int]', + 'spam_complaint_count': 'Required[int]', + 'inbound_received_count': 'Required[int]', + 'observed_opened_count': 'NotRequired[int | None]', + 'human_opened_count': 'NotRequired[int | None]', + 'privacy_opened_count': 'NotRequired[int | None]', + 'effective_opened_count': 'Required[int | None]', + 'machine_opened_count': 'Required[int | None]', + 'machine_clicked_count': 'Required[int | None]', + }, +) + +RouteType: TypeAlias = Literal['transactional', 'broadcast', 'inbound'] RouteData = TypedDict( - "RouteData", - { - "id": "Required[str]", - "project_id": "Required[str]", - "slug": "Required[str]", - "name": "Required[str]", - "route_type": "Required[RouteType]", - "is_default": "Required[bool]", - "inbound_address": "NotRequired[str | None]", - "inbound_domain": "NotRequired[str | None]", - "inbound_domain_verified_at": "NotRequired[str | None]", - "inbound_spam_threshold": "NotRequired[float | None]", - "attachment_delivery": "NotRequired[AttachmentDelivery]", - "settings": "NotRequired[dict[str, Any] | None]", - "project": "NotRequired[ProjectData]", - "webhooks_count": "NotRequired[int]", - "suppressed_recipients_count": "NotRequired[int]", - "statistics": "NotRequired[list[RouteStatisticData]]", - "created_at": "Required[str]", - "updated_at": "Required[str]", + 'RouteData', + { + 'id': 'Required[str]', + 'project_id': 'Required[str]', + 'slug': 'Required[str]', + 'name': 'Required[str]', + 'route_type': 'Required[RouteType]', + 'is_default': 'Required[bool]', + 'inbound_address': 'NotRequired[str | None]', + 'inbound_mx_hostname': 'NotRequired[str]', + 'inbound_domain': 'NotRequired[str | None]', + 'inbound_domain_verified_at': 'NotRequired[str | None]', + 'inbound_spam_threshold': 'NotRequired[float | None]', + 'attachment_delivery': 'NotRequired[AttachmentDelivery]', + 'settings': 'NotRequired[dict[str, Any] | None]', + 'project': 'NotRequired[ProjectData]', + 'webhooks_count': 'NotRequired[int]', + 'suppressed_recipients_count': 'NotRequired[int]', + 'statistics': 'NotRequired[list[RouteStatisticData]]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', }, ) ProjectData = TypedDict( - "ProjectData", - { - "id": "Required[str]", - "name": "Required[str]", - "smtp_enabled": "Required[bool]", - "redact_email_content": "Required[bool]", - "default_route_id": "Required[str | None]", - "token_generated_at": "Required[str | None]", - "token_last_used_at": "Required[str | None]", - "token_last_used_ip": "Required[str | None]", - "routes": "NotRequired[list[RouteData]]", - "routes_count": "NotRequired[int]", - "domains": "NotRequired[list[DomainData]]", - "domains_count": "NotRequired[int]", - "last_28_days": "NotRequired[MessageStatsData | None]", - "created_at": "Required[str]", - "updated_at": "Required[str]", + 'ProjectData', + { + 'id': 'Required[str]', + 'name': 'Required[str]', + 'smtp_enabled': 'Required[bool]', + 'redact_email_content': 'Required[bool]', + 'default_route_id': 'Required[str | None]', + 'token_generated_at': 'Required[str | None]', + 'token_last_used_at': 'Required[str | None]', + 'token_last_used_ip': 'Required[str | None]', + 'routes': 'NotRequired[list[RouteData]]', + 'routes_count': 'NotRequired[int]', + 'domains': 'NotRequired[list[DomainData]]', + 'domains_count': 'NotRequired[int]', + 'last_28_days': 'NotRequired[MessageStatsData | None]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', + 'delivery_mode': 'Required[DeliveryMode]', }, ) ProjectListData = TypedDict( - "ProjectListData", - { - "id": "Required[str]", - "name": "Required[str]", - "smtp_enabled": "Required[bool]", - "routes_count": "Required[int]", - "domains_count": "Required[int]", - "last_28_days": "Required[MessageStatsData]", - "created_at": "Required[str]", - "updated_at": "Required[str]", - }, -) - -RbacConflictCode: TypeAlias = Literal[ - "stale_resource", - "owner_protected", - "last_owner", - "built_in_role_immutable", - "custom_role_requires_pro", -] -RbacPermission: TypeAlias = Literal[ - "team:manage", - "billing:manage", - "security:manage", - "audit:read", - "support:manage", - "members:read", - "members:manage", - "roles:manage", - "team_tokens:read", - "team_tokens:manage", - "team_tokens:rotate", - "team_tokens:revoke", - "projects:create", - "team_suppressions:read", - "team_suppressions:add", - "team_suppressions:remove", - "projects:read", - "projects:manage", - "projects:delete", - "routes:read", - "routes:manage", - "routes:delete", - "domains:read", - "domains:manage", - "domains:delete", - "project_tokens:read", - "project_tokens:manage", - "project_tokens:rotate", - "project_tokens:revoke", - "webhooks:read", - "webhooks:manage", - "webhooks:delete", - "webhooks:rotate_secret", - "stats:read", - "messages:read", - "messages:read_content", - "messages:send", - "suppressions:read", - "suppressions:add", - "suppressions:remove", -] + 'ProjectListData', + { + 'id': 'Required[str]', + 'name': 'Required[str]', + 'smtp_enabled': 'Required[bool]', + 'routes_count': 'Required[int]', + 'domains_count': 'Required[int]', + 'last_28_days': 'Required[MessageStatsData]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', + }, +) + +RbacConflictCode: TypeAlias = Literal['stale_resource', 'owner_protected', 'last_owner', 'built_in_role_immutable', 'custom_role_requires_pro'] +RbacPermission: TypeAlias = Literal['team:manage', 'billing:manage', 'security:manage', 'audit:read', 'support:manage', 'members:read', 'members:manage', 'roles:manage', 'team_tokens:read', 'team_tokens:manage', 'team_tokens:rotate', 'team_tokens:revoke', 'projects:create', 'team_suppressions:read', 'team_suppressions:add', 'team_suppressions:remove', 'projects:read', 'projects:manage', 'projects:delete', 'routes:read', 'routes:manage', 'routes:delete', 'domains:read', 'domains:manage', 'domains:delete', 'project_tokens:read', 'project_tokens:manage', 'project_tokens:rotate', 'project_tokens:revoke', 'webhooks:read', 'webhooks:manage', 'webhooks:delete', 'webhooks:rotate_secret', 'stats:read', 'messages:read', 'messages:read_content', 'messages:send', 'suppressions:read', 'suppressions:add', 'suppressions:remove'] +RescheduleMessageRequest = TypedDict( + 'RescheduleMessageRequest', + { + 'scheduled_at': 'Required[str]', + }, +) + RouteListData = TypedDict( - "RouteListData", + 'RouteListData', { - "id": "Required[str]", - "slug": "Required[str]", - "name": "Required[str]", - "route_type": "Required[RouteType]", - "is_default": "Required[bool]", - "webhooks_count": "Required[int]", - "suppressed_recipients_count": "Required[int]", - "created_at": "Required[str]", - "updated_at": "Required[str]", + 'id': 'Required[str]', + 'slug': 'Required[str]', + 'name': 'Required[str]', + 'route_type': 'Required[RouteType]', + 'is_default': 'Required[bool]', + 'webhooks_count': 'Required[int]', + 'suppressed_recipients_count': 'Required[int]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', }, ) StatsInboundData = TypedDict( - "StatsInboundData", + 'StatsInboundData', { - "received": "Required[int]", + 'received': 'Required[int]', }, ) StatsTypeData = TypedDict( - "StatsTypeData", + 'StatsTypeData', { - "sent": "Required[int]", - "hard_bounced": "Required[int]", - "spam_complaints": "Required[int]", + 'sent': 'Required[int]', + 'hard_bounced': 'Required[int]', + 'spam_complaints': 'Required[int]', }, ) StatsDailyData = TypedDict( - "StatsDailyData", - { - "date": "Required[str]", - "sent": "Required[int]", - "delivered": "Required[int]", - "hard_bounced": "Required[int]", - "spam_complaints": "Required[int]", - "opened": "Required[int | None]", - "clicked": "Required[int | None]", - "inbound": "Required[StatsInboundData]", - "transactional": "Required[StatsTypeData | None]", - "broadcast": "Required[StatsTypeData | None]", - "observed_opened": "NotRequired[int | None]", - "human_opened": "NotRequired[int | None]", - "privacy_opened": "NotRequired[int | None]", - "effective_opened": "Required[int | None]", - "machine_opened": "Required[int | None]", - "machine_clicked": "Required[int | None]", + 'StatsDailyData', + { + 'date': 'Required[str]', + 'sent': 'Required[int]', + 'delivered': 'Required[int]', + 'hard_bounced': 'Required[int]', + 'spam_complaints': 'Required[int]', + 'opened': 'Required[int | None]', + 'clicked': 'Required[int | None]', + 'inbound': 'Required[StatsInboundData]', + 'transactional': 'Required[StatsTypeData | None]', + 'broadcast': 'Required[StatsTypeData | None]', + 'observed_opened': 'NotRequired[int | None]', + 'human_opened': 'NotRequired[int | None]', + 'privacy_opened': 'NotRequired[int | None]', + 'effective_opened': 'Required[int | None]', + 'machine_opened': 'Required[int | None]', + 'machine_clicked': 'Required[int | None]', }, ) StatsTotalsData = TypedDict( - "StatsTotalsData", - { - "sent": "Required[int]", - "delivered": "Required[int]", - "hard_bounced": "Required[int]", - "spam_complaints": "Required[int]", - "opened": "Required[int | None]", - "clicked": "Required[int | None]", - "inbound": "Required[StatsInboundData]", - "transactional": "Required[StatsTypeData | None]", - "broadcast": "Required[StatsTypeData | None]", - "observed_opened": "NotRequired[int | None]", - "human_opened": "NotRequired[int | None]", - "privacy_opened": "NotRequired[int | None]", - "effective_opened": "Required[int | None]", - "machine_opened": "Required[int | None]", - "machine_clicked": "Required[int | None]", + 'StatsTotalsData', + { + 'sent': 'Required[int]', + 'delivered': 'Required[int]', + 'hard_bounced': 'Required[int]', + 'spam_complaints': 'Required[int]', + 'opened': 'Required[int | None]', + 'clicked': 'Required[int | None]', + 'inbound': 'Required[StatsInboundData]', + 'transactional': 'Required[StatsTypeData | None]', + 'broadcast': 'Required[StatsTypeData | None]', + 'observed_opened': 'NotRequired[int | None]', + 'human_opened': 'NotRequired[int | None]', + 'privacy_opened': 'NotRequired[int | None]', + 'effective_opened': 'Required[int | None]', + 'machine_opened': 'Required[int | None]', + 'machine_clicked': 'Required[int | None]', }, ) StatsData = TypedDict( - "StatsData", + 'StatsData', { - "from": "Required[str]", - "to": "Required[str]", - "totals": "Required[StatsTotalsData]", - "daily": "Required[list[StatsDailyData]]", + 'from': 'Required[str]', + 'to': 'Required[str]', + 'totals': 'Required[StatsTotalsData]', + 'daily': 'Required[list[StatsDailyData]]', }, ) StatsRequestData = TypedDict( - "StatsRequestData", + 'StatsRequestData', { - "from": "Required[str]", - "to": "Required[str]", - "project_id": "NotRequired[str | None]", - "include_machine": "NotRequired[bool]", + 'from': 'Required[str]', + 'to': 'Required[str]', + 'project_id': 'NotRequired[str | None]', + 'include_machine': 'NotRequired[bool]', }, ) StoreDomainData = TypedDict( - "StoreDomainData", + 'StoreDomainData', { - "domain": "Required[str]", + 'domain': 'Required[str]', }, ) StoreProjectData = TypedDict( - "StoreProjectData", + 'StoreProjectData', { - "name": "Required[str]", - "smtp_enabled": "NotRequired[bool]", - "initial_routes": "NotRequired[InitialRoutes]", - "short_token": "NotRequired[bool]", + 'name': 'Required[str]', + 'smtp_enabled': 'NotRequired[bool]', + 'initial_routes': 'NotRequired[InitialRoutes]', + 'short_token': 'NotRequired[bool]', + 'delivery_mode': 'NotRequired[DeliveryMode]', }, ) StoreRouteData = TypedDict( - "StoreRouteData", + 'StoreRouteData', { - "name": "Required[str]", - "route_type": "Required[RouteType]", - "slug": "NotRequired[str | None]", + 'name': 'Required[str]', + 'route_type': 'Required[RouteType]', + 'slug': 'NotRequired[str | None]', }, ) -SuppressionReason: TypeAlias = Literal["spam_complaint", "hard_bounce", "unsubscribe", "manual"] -SuppressionScope: TypeAlias = Literal["global", "team", "project", "route"] +SuppressionReason: TypeAlias = Literal['spam_complaint', 'hard_bounce', 'unsubscribe', 'manual'] +SuppressionScope: TypeAlias = Literal['team', 'project', 'route'] +SuppressionAppliesTo: TypeAlias = Literal['all', 'broadcast'] StoreSuppressionData = TypedDict( - "StoreSuppressionData", - { - "email": "NotRequired[str | None]", - "emails": "NotRequired[list[str] | None]", - "reason": "Required[SuppressionReason]", - "scope": "Required[SuppressionScope]", - "route_id": "NotRequired[str | None]", - "project_id": "NotRequired[str | None]", - }, -) - -WebhookEvent: TypeAlias = Literal[ - "message.created", - "message.sent", - "message.delivered", - "message.auto_replied", - "message.hard_bounced", - "message.soft_bounced", - "message.spam_complaint", - "message.failed", - "message.suppressed", - "message.unsubscribed", - "message.opened", - "message.clicked", - "message.inbound", - "message.policy_rejected", - "suppression.added", - "suppression.removed", - "webhook.test", -] + 'StoreSuppressionData', + { + 'email': 'NotRequired[str | None]', + 'emails': 'NotRequired[list[str] | None]', + 'reason': 'Required[SuppressionReason]', + 'scope': 'Required[SuppressionScope]', + 'route_id': 'NotRequired[str | None]', + 'project_id': 'NotRequired[str | None]', + 'applies_to': 'NotRequired[SuppressionAppliesTo | None]', + }, +) + +WebhookEvent: TypeAlias = Literal['message.created', 'message.sent', 'message.delivered', 'message.auto_replied', 'message.hard_bounced', 'message.soft_bounced', 'message.spam_complaint', 'message.failed', 'message.suppressed', 'message.unsubscribed', 'message.opened', 'message.clicked', 'message.inbound', 'message.policy_rejected', 'message.scheduled', 'message.rescheduled', 'message.canceled', 'message.released', 'suppression.added', 'suppression.removed', 'webhook.test'] +WebhookScope: TypeAlias = Literal['team', 'project', 'route'] +WebhookDeliveryModeFilter: TypeAlias = Literal['live', 'sandbox', 'both'] StoreWebhookData = TypedDict( - "StoreWebhookData", + 'StoreWebhookData', { - "route_id": "Required[str]", - "name": "Required[str]", - "url": "Required[str]", - "events": "Required[list[WebhookEvent]]", - "enabled": "NotRequired[bool | None]", - "include_machine_events": "NotRequired[bool | None]", + 'name': 'Required[str]', + 'url': 'Required[str]', + 'events': 'Required[list[WebhookEvent]]', + 'enabled': 'NotRequired[bool | None]', + 'include_machine_events': 'NotRequired[bool | None]', + 'scope': 'NotRequired[WebhookScope | None]', + 'project_ids': 'NotRequired[list[str]]', + 'route_ids': 'NotRequired[list[str]]', + 'route_id': 'NotRequired[str | None]', + 'delivery_mode_filter': 'NotRequired[WebhookDeliveryModeFilter | None]', }, ) -SuppressionType: TypeAlias = Literal["email", "domain", "extension"] SuppressionSourceMessageData = TypedDict( - "SuppressionSourceMessageData", + 'SuppressionSourceMessageData', { - "id": "Required[str]", - "available": "Required[bool]", - "subject": "Required[str | None]", - "created_at": "Required[str | None]", + 'id': 'Required[str]', + 'available': 'Required[bool]', + 'subject': 'Required[str | None]', + 'created_at': 'Required[str | None]', }, ) +SuppressionType: TypeAlias = Literal['email', 'domain', 'extension'] SuppressedRecipientData = TypedDict( - "SuppressedRecipientData", + 'SuppressedRecipientData', { - "id": "Required[str]", - "type": "Required[SuppressionType]", - "value": "Required[str]", - "reason": "Required[SuppressionReason]", - "scope": "Required[SuppressionScope]", - "project_id": "Required[str | None]", - "route_id": "Required[str | None]", - "source_message": "NotRequired[SuppressionSourceMessageData | None]", - "created_at": "Required[str]", - "updated_at": "Required[str]", + 'id': 'Required[str]', + 'type': 'Required[SuppressionType]', + 'value': 'Required[str]', + 'reason': 'Required[SuppressionReason]', + 'scope': 'Required[SuppressionScope]', + 'applies_to': 'Required[SuppressionAppliesTo]', + 'project_id': 'Required[str | None]', + 'route_id': 'Required[str | None]', + 'source_message': 'NotRequired[SuppressionSourceMessageData | None]', + 'created_at': 'Required[str]', }, ) TeamAddonData = TypedDict( - "TeamAddonData", + 'TeamAddonData', { - "type": "Required[str | None]", - "expires_at": "Required[str | None]", + 'type': 'Required[str | None]', + 'expires_at': 'Required[str | None]', }, ) -TeamType: TypeAlias = Literal["personal", "business"] +TeamType: TypeAlias = Literal['personal', 'business'] TeamData = TypedDict( - "TeamData", + 'TeamData', { - "id": "Required[str]", - "name": "Required[str]", - "type": "Required[TeamType]", - "plan": "Required[Plan]", - "included_volume": "Required[int]", - "tier": "Required[int]", - "verified_at": "Required[str | None]", - "features": "NotRequired[list[str]]", - "addons": "NotRequired[list[TeamAddonData]]", - "created_at": "Required[str]", - "domains_count": "NotRequired[int]", - "projects_count": "NotRequired[int]", - "members_count": "NotRequired[int]", + 'id': 'Required[str]', + 'name': 'Required[str]', + 'type': 'Required[TeamType]', + 'plan': 'Required[Plan]', + 'included_volume': 'Required[int]', + 'tier': 'Required[int]', + 'verified_at': 'Required[str | None]', + 'features': 'NotRequired[list[str]]', + 'addons': 'NotRequired[list[TeamAddonData]]', + 'created_at': 'Required[str]', + 'domains_count': 'NotRequired[int]', + 'projects_count': 'NotRequired[int]', + 'members_count': 'NotRequired[int]', }, ) TeamMemberProjectAccessData = TypedDict( - "TeamMemberProjectAccessData", + 'TeamMemberProjectAccessData', { - "scope": "Required[ProjectAccessScope]", - "projects": "Required[list[dict[str, Any]]]", + 'scope': 'Required[ProjectAccessScope]', + 'projects': 'Required[list[dict[str, Any]]]', }, ) TeamMemberData = TypedDict( - "TeamMemberData", + 'TeamMemberData', { - "id": "Required[str]", - "name": "Required[str]", - "email": "Required[str]", - "role": "Required[dict[str, Any]]", - "project_access": "Required[TeamMemberProjectAccessData]", - "joined_at": "Required[str | None]", + 'id': 'Required[str]', + 'name': 'Required[str]', + 'email': 'Required[str]', + 'role': 'Required[dict[str, Any]]', + 'project_access': 'Required[TeamMemberProjectAccessData]', + 'joined_at': 'Required[str | None]', }, ) TeamRoleData = TypedDict( - "TeamRoleData", + 'TeamRoleData', { - "id": "Required[str]", - "name": "Required[str]", - "system_key": "Required[BuiltInTeamRole | None]", - "permissions": "Required[list[RbacPermission]]", - "assignable": "Required[bool]", + 'id': 'Required[str]', + 'name': 'Required[str]', + 'system_key': 'Required[BuiltInTeamRole | None]', + 'permissions': 'Required[list[RbacPermission]]', + 'assignable': 'Required[bool]', }, ) TeamUsagePeriodData = TypedDict( - "TeamUsagePeriodData", + 'TeamUsagePeriodData', { - "usage": "Required[int]", - "last_incremented_at": "Required[str | None]", - "period_start": "Required[str]", - "period_end": "Required[str]", + 'usage': 'Required[int]', + 'last_incremented_at': 'Required[str | None]', + 'period_start': 'Required[str]', + 'period_end': 'Required[str]', }, ) TeamUsageDetailData = TypedDict( - "TeamUsageDetailData", + 'TeamUsageDetailData', { - "current_period": "Required[TeamUsagePeriodData]", - "historical_usage": "Required[list[TeamUsagePeriodData]]", + 'current_period': 'Required[TeamUsagePeriodData]', + 'historical_usage': 'Required[list[TeamUsagePeriodData]]', }, ) UpdateDomainProjectsData = TypedDict( - "UpdateDomainProjectsData", + 'UpdateDomainProjectsData', { - "project_ids": "Required[list[str]]", + 'project_ids': 'Required[list[str]]', }, ) UpdateProjectData = TypedDict( - "UpdateProjectData", + 'UpdateProjectData', { - "name": "NotRequired[str | None]", - "smtp_enabled": "NotRequired[bool | None]", - "redact_email_content": "NotRequired[bool | None]", - "default_route_id": "NotRequired[str | None]", + 'name': 'NotRequired[str | None]', + 'smtp_enabled': 'NotRequired[bool | None]', + 'redact_email_content': 'NotRequired[bool | None]', + 'default_route_id': 'NotRequired[str | None]', + 'delivery_mode': 'NotRequired[DeliveryMode | None]', }, ) UpdateRouteInboundSettingsData = TypedDict( - "UpdateRouteInboundSettingsData", + 'UpdateRouteInboundSettingsData', { - "inbound_domain": "NotRequired[str | None]", - "inbound_spam_threshold": "NotRequired[float | None]", - "attachment_delivery": "NotRequired[AttachmentDelivery | None]", + 'inbound_domain': 'NotRequired[str | None]', + 'inbound_spam_threshold': 'NotRequired[float | None]', + 'attachment_delivery': 'NotRequired[AttachmentDelivery | None]', }, ) UpdateRouteSettingsData = TypedDict( - "UpdateRouteSettingsData", + 'UpdateRouteSettingsData', { - "track_opens": "NotRequired[bool | None]", - "track_clicks": "NotRequired[bool | None]", - "generate_plaintext_fallback": "NotRequired[bool | None]", - "suppress_auto_responders": "NotRequired[bool | None]", - "suppress_disposable_recipients": "NotRequired[bool | None]", - "tls": "NotRequired[TlsPolicy | None]", - "disable_hosted_unsubscribe": "NotRequired[bool | None]", - "redact_email_content": "NotRequired[bool | None]", + 'track_opens': 'NotRequired[bool | None]', + 'track_clicks': 'NotRequired[bool | None]', + 'generate_plaintext_fallback': 'NotRequired[bool | None]', + 'suppress_auto_responders': 'NotRequired[bool | None]', + 'suppress_disposable_recipients': 'NotRequired[bool | None]', + 'tls': 'NotRequired[TlsPolicy | None]', + 'disable_hosted_unsubscribe': 'NotRequired[bool | None]', + 'redact_email_content': 'NotRequired[bool | None]', }, ) UpdateRouteData = TypedDict( - "UpdateRouteData", + 'UpdateRouteData', { - "name": "NotRequired[str | None]", - "settings": "NotRequired[UpdateRouteSettingsData | None]", - "inbound_settings": "NotRequired[UpdateRouteInboundSettingsData | None]", + 'name': 'NotRequired[str | None]', + 'settings': 'NotRequired[UpdateRouteSettingsData | None]', + 'inbound_settings': 'NotRequired[UpdateRouteInboundSettingsData | None]', }, ) UpdateTeamData = TypedDict( - "UpdateTeamData", + 'UpdateTeamData', { - "name": "NotRequired[str | None]", + 'name': 'NotRequired[str | None]', }, ) UpdateTeamMemberAssignmentData = TypedDict( - "UpdateTeamMemberAssignmentData", + 'UpdateTeamMemberAssignmentData', { - "role_id": "Required[str]", - "project_access": "Required[dict[str, Any]]", + 'role_id': 'Required[str]', + 'project_access': 'Required[dict[str, Any]]', }, ) UpdateWebhookData = TypedDict( - "UpdateWebhookData", + 'UpdateWebhookData', { - "name": "NotRequired[str]", - "url": "NotRequired[str]", - "events": "NotRequired[list[WebhookEvent]]", - "enabled": "NotRequired[bool]", - "include_machine_events": "NotRequired[bool]", + 'name': 'NotRequired[str]', + 'url': 'NotRequired[str]', + 'events': 'NotRequired[list[WebhookEvent]]', + 'enabled': 'NotRequired[bool]', + 'include_machine_events': 'NotRequired[bool]', + 'scope': 'NotRequired[WebhookScope]', + 'project_ids': 'NotRequired[list[str]]', + 'route_ids': 'NotRequired[list[str]]', + 'route_id': 'NotRequired[str | None]', + 'delivery_mode_filter': 'NotRequired[WebhookDeliveryModeFilter]', }, ) WebhookData = TypedDict( - "WebhookData", - { - "id": "Required[str]", - "route_id": "Required[str]", - "name": "Required[str]", - "url": "Required[str]", - "events": "Required[list[str]]", - "enabled": "Required[bool]", - "include_machine_events": "Required[bool]", - "secret": "NotRequired[str]", - "last_called_at": "Required[str | None]", - "created_at": "Required[str]", - "updated_at": "Required[str]", - }, -) - -WebhookDeliveryStatus: TypeAlias = Literal[ - "pending", "success", "failed", "client_error", "server_error", "timeout" -] + 'WebhookData', + { + 'id': 'Required[str]', + 'scope': 'Required[WebhookScope]', + 'project_ids': 'Required[list[str]]', + 'route_ids': 'Required[list[str]]', + 'route_id': 'Required[str | None]', + 'name': 'Required[str]', + 'url': 'Required[str]', + 'events': 'Required[list[str]]', + 'enabled': 'Required[bool]', + 'include_machine_events': 'Required[bool]', + 'last_called_at': 'Required[str | None]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', + 'delivery_mode_filter': 'Required[WebhookDeliveryModeFilter]', + }, +) + +WebhookDeliveryStatus: TypeAlias = Literal['pending', 'success', 'failed', 'client_error', 'server_error', 'timeout'] WebhookDeliveryData = TypedDict( - "WebhookDeliveryData", - { - "id": "Required[str]", - "webhook_id": "Required[str]", - "event_type": "Required[WebhookEvent]", - "status": "Required[WebhookDeliveryStatus]", - "attempt_number": "Required[int]", - "http_status_code": "Required[int | None]", - "duration_ms": "Required[int | None]", - "payload": "Required[list[str]]", - "response_body": "Required[str | None]", - "response_headers": "Required[list[str] | None]", - "error_message": "Required[str | None]", - "delivered_at": "Required[str | None]", - "timestamp": "Required[str]", + 'WebhookDeliveryData', + { + 'id': 'Required[str]', + 'webhook_id': 'Required[str]', + 'event_type': 'Required[WebhookEvent]', + 'source_scope': 'Required[str | None]', + 'source_project_id': 'Required[str | None]', + 'source_route_id': 'Required[str | None]', + 'status': 'Required[WebhookDeliveryStatus]', + 'attempt_number': 'Required[int]', + 'http_status_code': 'Required[int | None]', + 'duration_ms': 'Required[int | None]', + 'payload': 'Required[list[str]]', + 'response_body': 'Required[str | None]', + 'response_headers': 'Required[list[str] | None]', + 'error_message': 'Required[str | None]', + 'delivered_at': 'Required[str | None]', + 'timestamp': 'Required[str]', + 'sandbox': 'Required[bool]', }, ) WebhookDeliveryListData = TypedDict( - "WebhookDeliveryListData", + 'WebhookDeliveryListData', { - "id": "Required[str]", - "webhook_id": "Required[str]", - "event_type": "Required[WebhookEvent]", - "status": "Required[WebhookDeliveryStatus]", - "attempt_number": "Required[int]", - "http_status_code": "Required[int | None]", - "duration_ms": "Required[int | None]", - "delivered_at": "Required[str | None]", - "created_at": "Required[str]", + 'id': 'Required[str]', + 'webhook_id': 'Required[str]', + 'event_type': 'Required[WebhookEvent]', + 'source_scope': 'Required[str | None]', + 'source_project_id': 'Required[str | None]', + 'source_route_id': 'Required[str | None]', + 'status': 'Required[WebhookDeliveryStatus]', + 'attempt_number': 'Required[int]', + 'http_status_code': 'Required[int | None]', + 'duration_ms': 'Required[int | None]', + 'delivered_at': 'Required[str | None]', + 'created_at': 'Required[str]', }, ) WebhookListData = TypedDict( - "WebhookListData", - { - "id": "Required[str]", - "route_id": "Required[str]", - "name": "Required[str]", - "url": "Required[str]", - "events": "Required[list[WebhookEvent]]", - "enabled": "Required[bool]", - "last_called_at": "Required[str | None]", - "created_at": "Required[str]", - "updated_at": "Required[str]", + 'WebhookListData', + { + 'id': 'Required[str]', + 'scope': 'Required[WebhookScope]', + 'project_ids': 'Required[list[str]]', + 'route_ids': 'Required[list[str]]', + 'route_id': 'Required[str | None]', + 'name': 'Required[str]', + 'url': 'Required[str]', + 'events': 'Required[list[str]]', + 'enabled': 'Required[bool]', + 'last_called_at': 'Required[str | None]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', + }, +) + +WebhookSecretData = TypedDict( + 'WebhookSecretData', + { + 'id': 'Required[str]', + 'scope': 'Required[WebhookScope]', + 'project_ids': 'Required[list[str]]', + 'route_ids': 'Required[list[str]]', + 'route_id': 'Required[str | None]', + 'name': 'Required[str]', + 'url': 'Required[str]', + 'events': 'Required[list[str]]', + 'enabled': 'Required[bool]', + 'include_machine_events': 'Required[bool]', + 'secret': 'Required[str]', + 'last_called_at': 'Required[str | None]', + 'created_at': 'Required[str]', + 'updated_at': 'Required[str]', + 'delivery_mode_filter': 'Required[WebhookDeliveryModeFilter]', }, ) EmailAttachment = TypedDict( - "EmailAttachment", + 'EmailAttachment', { - "filename": "Required[str]", - "content": "Required[str]", - "content_type": "NotRequired[str | None]", - "content_id": "NotRequired[str | None]", + 'filename': 'Required[str]', + 'content': 'Required[str]', + 'content_type': 'NotRequired[str | None]', + 'content_id': 'NotRequired[str | None]', }, ) EmailPayload: TypeAlias = SendMailRequest EmailStatus: TypeAlias = MessageStatus SendMailResponse = TypedDict( - "SendMailResponse", - { - "message_id": "Required[str]", - "status": "Required[MessageStatus]", - "scheduled_at": "NotRequired[str]", - }, -) - -RescheduleMessageRequest = TypedDict( - "RescheduleMessageRequest", - {"scheduled_at": "Required[str]"}, -) -RescheduleMessageResponse = TypedDict( - "RescheduleMessageResponse", + 'SendMailResponse', { - "message_id": "Required[str]", - "status": "Required[MessageStatus | None]", - "scheduled_at": "Required[str | None]", + 'message_id': 'Required[str | None]', + 'status': 'Required[MessageStatus]', + 'scheduled_at': 'NotRequired[str]', + 'sandbox': 'NotRequired[Literal[True]]', + 'sandbox_result': 'NotRequired[SandboxResult]', }, ) -SendBatchMailResponse: TypeAlias = list[dict[str, Any]] +SendBatchMailResponse: TypeAlias = list[SendMailResponse] PingResponse: TypeAlias = Literal[200] DomainIndexResponse = TypedDict( - "DomainIndexResponse", + 'DomainIndexResponse', { - "data": "Required[list[DomainListData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'data': 'Required[list[DomainListData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) @@ -839,225 +767,244 @@ DomainStoreResponse: TypeAlias = DomainData DomainShowResponse: TypeAlias = DomainData DomainDestroyResponse = TypedDict( - "DomainDestroyResponse", + 'DomainDestroyResponse', { - "message": "Required[Literal['Domain deleted successfully.']]", + 'message': "Required[Literal['Domain deleted successfully.']]", }, ) DomainVerifyDnsRecordsResponse = TypedDict( - "DomainVerifyDnsRecordsResponse", + 'DomainVerifyDnsRecordsResponse', { - "message": "Required[str]", - "recommended_failed_records": "Required[list[dict[str, Any]]]", + 'message': 'Required[str]', + 'recommended_failed_records': 'Required[list[dict[str, Any]]]', }, ) DomainVerifySpecificDnsRecordResponse = TypedDict( - "DomainVerifySpecificDnsRecordResponse", + 'DomainVerifySpecificDnsRecordResponse', { - "message": "Required[Literal['DNS record verified successfully.']]", + 'message': "Required[Literal['DNS record verified successfully.']]", }, ) DomainUpdateProjectsRequest: TypeAlias = UpdateDomainProjectsData DomainUpdateProjectsResponse = TypedDict( - "DomainUpdateProjectsResponse", + 'DomainUpdateProjectsResponse', { - "data": "Required[DomainData]", - "message": "Required[Literal['Domain projects updated successfully.']]", + 'data': 'Required[DomainData]', + 'message': "Required[Literal['Domain projects updated successfully.']]", }, ) BlockedFileTypesResponse = TypedDict( - "BlockedFileTypesResponse", + 'BlockedFileTypesResponse', { - "extensions": "Required[list[str]]", - "mime_types": "Required[list[str]]", + 'extensions': 'Required[list[str]]', + 'mime_types': 'Required[list[str]]', }, ) -MessageIndexResponse = TypedDict( - "MessageIndexResponse", +RescheduleMessageResponse = TypedDict( + 'RescheduleMessageResponse', { - "data": "Required[list[MessageListData]]", - "links": "Required[list[str]]", - "meta": "Required[dict[str, Any]]", + 'message_id': 'Required[str]', + 'status': 'Required[MessageStatus | None]', + 'scheduled_at': 'Required[str | None]', }, ) MessageShowResponse: TypeAlias = MessageData -ProcessInboundMessageResponse = TypedDict( - "ProcessInboundMessageResponse", +CancelScheduledMessageResponse = TypedDict( + 'CancelScheduledMessageResponse', + { + 'message_id': 'Required[str]', + 'status': 'Required[MessageStatus | None]', + 'scheduled_at': 'Required[str | None]', + }, +) + +MessageIndexResponse = TypedDict( + 'MessageIndexResponse', { - "data": "Required[dict[str, str | int]]", + 'data': 'Required[list[MessageListData]]', + 'links': 'Required[list[str]]', + 'meta': 'Required[dict[str, Any]]', }, ) + MessageEventsResponse = TypedDict( - "MessageEventsResponse", + 'MessageEventsResponse', + { + 'data': 'Required[list[MessageEventData]]', + 'links': 'Required[list[str]]', + 'meta': 'Required[dict[str, Any]]', + }, +) + +ProcessInboundMessageResponse = TypedDict( + 'ProcessInboundMessageResponse', { - "data": "Required[list[MessageEventData]]", - "links": "Required[list[str]]", - "meta": "Required[dict[str, Any]]", + 'data': 'Required[dict[str, Any]]', }, ) ProjectIndexResponse = TypedDict( - "ProjectIndexResponse", + 'ProjectIndexResponse', { - "data": "Required[list[ProjectListData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'data': 'Required[list[ProjectListData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) ProjectStoreRequest: TypeAlias = StoreProjectData ProjectStoreResponse = TypedDict( - "ProjectStoreResponse", + 'ProjectStoreResponse', { - "data": "Required[ProjectData]", - "message": "Required[Literal['Project created successfully.']]", - "api_token": "Required[str]", + 'data': 'Required[ProjectData]', + 'message': "Required[Literal['Project created successfully.']]", + 'api_token': 'Required[str]', }, ) ProjectShowResponse: TypeAlias = ProjectData ProjectUpdateRequest: TypeAlias = UpdateProjectData ProjectUpdateResponse = TypedDict( - "ProjectUpdateResponse", + 'ProjectUpdateResponse', { - "data": "Required[ProjectData]", - "message": "Required[Literal['Project updated successfully.']]", + 'data': 'Required[ProjectData]', + 'message': "Required[Literal['Project updated successfully.']]", }, ) ProjectDestroyResponse = TypedDict( - "ProjectDestroyResponse", + 'ProjectDestroyResponse', { - "message": "Required[Literal['Project deleted successfully.']]", + 'message': "Required[Literal['Project deleted successfully.']]", }, ) ProjectRotateTokenResponse = TypedDict( - "ProjectRotateTokenResponse", + 'ProjectRotateTokenResponse', { - "data": "Required[ProjectData]", - "new_token": "Required[str]", - "message": "Required[Literal['API token rotated successfully. Please update your integrations.']]", + 'data': 'Required[ProjectData]', + 'new_token': 'Required[str]', + 'message': "Required[Literal['API token rotated successfully. Please update your integrations.']]", }, ) RouteIndexResponse = TypedDict( - "RouteIndexResponse", + 'RouteIndexResponse', { - "data": "Required[list[RouteListData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'data': 'Required[list[RouteListData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) RouteStoreRequest: TypeAlias = StoreRouteData RouteStoreResponse = TypedDict( - "RouteStoreResponse", + 'RouteStoreResponse', { - "data": "Required[RouteData]", - "message": "Required[Literal['Route created successfully.']]", + 'data': 'Required[RouteData]', + 'message': "Required[Literal['Route created successfully.']]", }, ) RouteShowResponse: TypeAlias = RouteData RouteUpdateRequest: TypeAlias = UpdateRouteData RouteUpdateResponse = TypedDict( - "RouteUpdateResponse", + 'RouteUpdateResponse', { - "data": "Required[RouteData]", - "message": "Required[Literal['Route updated successfully.']]", + 'data': 'Required[RouteData]', + 'message': "Required[Literal['Route updated successfully.']]", }, ) RouteDestroyResponse = TypedDict( - "RouteDestroyResponse", + 'RouteDestroyResponse', { - "message": "Required[Literal['Route deleted successfully.']]", + 'message': "Required[Literal['Route deleted successfully.']]", }, ) RouteVerifyInboundDomainResponse = TypedDict( - "RouteVerifyInboundDomainResponse", + 'RouteVerifyInboundDomainResponse', { - "data": "Required[dict[str, Any]]", + 'data': 'Required[dict[str, Any]]', }, ) StatsIndexResponse: TypeAlias = StatsData SuppressionIndexResponse = TypedDict( - "SuppressionIndexResponse", + 'SuppressionIndexResponse', { - "data": "Required[list[SuppressedRecipientData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'data': 'Required[list[SuppressedRecipientData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) SuppressionStoreRequest: TypeAlias = StoreSuppressionData SuppressionStoreResponse = TypedDict( - "SuppressionStoreResponse", + 'SuppressionStoreResponse', { - "message": "Required[str | Literal['No emails were added.']]", - "data": "Required[dict[str, Any]]", + 'message': "Required[str | Literal['No emails were added.']]", + 'data': 'Required[dict[str, Any]]', }, ) SuppressionDestroyResponse = TypedDict( - "SuppressionDestroyResponse", + 'SuppressionDestroyResponse', { - "success": "Required[bool]", - "status": "Required[Literal['removed']]", - "message": "Required[str]", - "confidence": "NotRequired[float]", + 'success': 'Required[bool]', + 'status': "Required[Literal['removed']]", + 'message': 'Required[str]', + 'confidence': 'NotRequired[float]', }, ) TeamShowResponse: TypeAlias = TeamData TeamUpdateRequest: TypeAlias = UpdateTeamData TeamUpdateResponse = TypedDict( - "TeamUpdateResponse", + 'TeamUpdateResponse', { - "data": "Required[TeamData]", - "message": "Required[Literal['Team settings updated successfully.']]", + 'data': 'Required[TeamData]', + 'message': "Required[Literal['Team settings updated successfully.']]", }, ) TeamUsageResponse: TypeAlias = TeamUsageDetailData TeamRolesResponse = TypedDict( - "TeamRolesResponse", + 'TeamRolesResponse', { - "data": "Required[list[TeamRoleData]]", + 'data': 'Required[list[TeamRoleData]]', }, ) TeamMembersResponse = TypedDict( - "TeamMembersResponse", + 'TeamMembersResponse', { - "data": "Required[list[TeamMemberData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'data': 'Required[list[TeamMemberData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) @@ -1065,70 +1012,70 @@ TeamMembersAssignmentUpdateRequest: TypeAlias = UpdateTeamMemberAssignmentData TeamMembersAssignmentUpdateResponse: TypeAlias = TeamMemberData WebhookIndexResponse = TypedDict( - "WebhookIndexResponse", + 'WebhookIndexResponse', { - "data": "Required[list[WebhookListData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'data': 'Required[list[WebhookListData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) WebhookStoreRequest: TypeAlias = StoreWebhookData WebhookStoreResponse = TypedDict( - "WebhookStoreResponse", + 'WebhookStoreResponse', { - "data": "Required[WebhookData]", - "message": "Required[Literal['Webhook created successfully. Please save the secret as it will not be shown again.']]", + 'data': 'Required[WebhookSecretData]', + 'message': "Required[Literal['Webhook created successfully. Please save the secret as it will not be shown again.']]", }, ) WebhookShowResponse: TypeAlias = WebhookData WebhookUpdateRequest: TypeAlias = UpdateWebhookData WebhookUpdateResponse = TypedDict( - "WebhookUpdateResponse", + 'WebhookUpdateResponse', { - "data": "Required[WebhookData]", - "message": "Required[Literal['Webhook updated successfully.']]", + 'data': 'Required[WebhookData]', + 'message': "Required[Literal['Webhook updated successfully.']]", }, ) WebhookDestroyResponse = TypedDict( - "WebhookDestroyResponse", + 'WebhookDestroyResponse', { - "message": "Required[Literal['Webhook deleted successfully.']]", + 'message': "Required[Literal['Webhook deleted successfully.']]", }, ) WebhookTestResponse = TypedDict( - "WebhookTestResponse", + 'WebhookTestResponse', { - "message": "Required[Literal['Test webhook dispatched successfully. Check the deliveries endpoint for results.']]", - "delivery_id": "Required[str]", + 'message': "Required[Literal['Test webhook dispatched successfully. Check the deliveries endpoint for results.']]", + 'delivery_id': 'Required[str]', }, ) WebhookRegenerateSecretResponse = TypedDict( - "WebhookRegenerateSecretResponse", + 'WebhookRegenerateSecretResponse', { - "data": "Required[WebhookData]", - "message": "Required[Literal['Webhook secret regenerated successfully. Please update your integration.']]", + 'data': 'Required[WebhookSecretData]', + 'message': "Required[Literal['Webhook secret regenerated successfully. Please update your integration.']]", }, ) WebhookDeliveriesResponse = TypedDict( - "WebhookDeliveriesResponse", - { - "data": "Required[list[WebhookDeliveryListData]]", - "path": "Required[str | None]", - "per_page": "Required[int]", - "next_cursor": "Required[str | None]", - "next_page_url": "Required[str | None]", - "prev_cursor": "Required[str | None]", - "prev_page_url": "Required[str | None]", + 'WebhookDeliveriesResponse', + { + 'data': 'Required[list[WebhookDeliveryListData]]', + 'path': 'Required[str | None]', + 'per_page': 'Required[int]', + 'next_cursor': 'Required[str | None]', + 'next_page_url': 'Required[str | None]', + 'prev_cursor': 'Required[str | None]', + 'prev_page_url': 'Required[str | None]', }, ) diff --git a/tests/test_api_surface.py b/tests/test_api_surface.py index 91746c5..05e8d29 100644 --- a/tests/test_api_surface.py +++ b/tests/test_api_surface.py @@ -106,7 +106,7 @@ def test_send_batch_posts_list_payload(self) -> None: ) with Lettermint.email("sending-token") as email: - response = email.idempotency_key("batch-key").send_batch( + response = email.send_batch( [ { "from": "sender@example.com", @@ -118,7 +118,6 @@ def test_send_batch_posts_list_payload(self) -> None: assert response[0]["message_id"] == "msg_123" assert json.loads(route.calls.last.request.content)[0]["subject"] == "Hello" - assert route.calls.last.request.headers["Idempotency-Key"] == "batch-key" class TestFullApiEndpoints: @@ -169,43 +168,6 @@ def test_message_raw_body_endpoints(self) -> None: assert html_route.called assert text_route.called - @respx.mock - def test_scheduled_message_endpoints(self) -> None: - reschedule_route = respx.patch("https://api.lettermint.co/v1/messages/message%2Fid").mock( - return_value=Response( - 200, - json={ - "message_id": "message/id", - "status": "scheduled", - "scheduled_at": "2026-08-27T09:00:00Z", - }, - ) - ) - cancel_route = respx.post("https://api.lettermint.co/v1/messages/message%2Fid/cancel").mock( - return_value=Response( - 200, json={"message_id": "message/id", "status": "canceled", "scheduled_at": None} - ) - ) - process_route = respx.post("https://api.lettermint.co/v1/messages/message%2Fid/process").mock( - return_value=Response(202, json={"data": {"message_id": "message/id", "status": "queued", "webhook_target_count": 1}}) - ) - - with Lettermint.api("api-token") as api: - assert ( - api.messages.reschedule("message/id", {"scheduled_at": "2026-08-27T09:00:00Z"})[ - "status" - ] - == "scheduled" - ) - assert api.messages.cancel("message/id")["status"] == "canceled" - assert api.messages.process("message/id")["data"]["status"] == "queued" - - assert json.loads(reschedule_route.calls.last.request.content) == { - "scheduled_at": "2026-08-27T09:00:00Z" - } - assert cancel_route.called - assert process_route.called - @respx.mock def test_team_role_and_member_assignment_endpoints(self) -> None: roles_route = respx.get("https://api.lettermint.co/v1/team/roles").mock( @@ -247,9 +209,6 @@ def test_documented_operations_are_exposed(self) -> None: (Lettermint.api("token").domains, "update_projects"), (Lettermint.api("token").messages, "list"), (Lettermint.api("token").messages, "retrieve"), - (Lettermint.api("token").messages, "reschedule"), - (Lettermint.api("token").messages, "cancel"), - (Lettermint.api("token").messages, "process"), (Lettermint.api("token").messages, "events"), (Lettermint.api("token").messages, "source"), (Lettermint.api("token").messages, "html"), @@ -293,12 +252,11 @@ def test_documented_operations_are_exposed(self) -> None: assert missing == [] def test_generated_types_match_current_team_schema(self) -> None: - assert "scheduled" in get_args(lm_types.MessageEventType) + assert "auto_replied" in get_args(lm_types.MessageEventType) assert "message.auto_replied" in get_args(lm_types.WebhookEvent) assert "admin" in get_args(lm_types.BuiltInTeamRole) assert "members:manage" in get_args(lm_types.RbacPermission) - assert "enforced" in get_args(lm_types.TlsPolicy) - assert "global" in get_args(lm_types.SuppressionScope) + assert "team" in get_args(lm_types.SuppressionScope) assert "short_token" in lm_types.StoreProjectData.__annotations__ assert "redact_email_content" in lm_types.ProjectData.__annotations__ @@ -310,13 +268,25 @@ def test_generated_types_match_current_team_schema(self) -> None: assert "mime_types" in lm_types.BlockedFileTypesResponse.__annotations__ assert "redact_email_content" in lm_types.UpdateRouteSettingsData.__annotations__ assert "generate_plaintext_fallback" in lm_types.UpdateRouteSettingsData.__annotations__ - assert "tls" in lm_types.UpdateRouteSettingsData.__annotations__ assert "inbound_spam_threshold" in lm_types.UpdateRouteInboundSettingsData.__annotations__ assert "included_volume" in lm_types.TeamData.__annotations__ assert "assignable" in lm_types.TeamRoleData.__annotations__ assert "role_id" in lm_types.UpdateTeamMemberAssignmentData.__annotations__ - assert "dkim_mode" in lm_types.DomainData.__annotations__ - assert "source_message" in lm_types.SuppressedRecipientData.__annotations__ - assert "spam_score" in lm_types.MessageListData.__annotations__ - assert "scheduled_at" in lm_types.SendMailRequest.__annotations__ - assert hasattr(lm_types, "RescheduleMessageRequest") + + def test_generated_types_include_sandbox_contracts(self) -> None: + assert set(get_args(lm_types.DeliveryMode)) == {"live", "sandbox"} + assert "clicked" in get_args(lm_types.SandboxResult) + assert set(get_args(lm_types.WebhookDeliveryModeFilter)) == {"live", "sandbox", "both"} + + assert "sandbox_result" in lm_types.SendMailRequest.__annotations__ + assert "sandbox" in lm_types.SendMailResponse.__annotations__ + assert "sandbox_result" in lm_types.SendMailResponse.__annotations__ + assert "delivery_mode" in lm_types.ProjectData.__annotations__ + assert "delivery_mode" in lm_types.StoreProjectData.__annotations__ + assert "delivery_mode" in lm_types.UpdateProjectData.__annotations__ + assert "delivery_mode" in lm_types.MessageData.__annotations__ + assert "sandbox_result" in lm_types.MessageData.__annotations__ + assert "delivery_mode_filter" in lm_types.StoreWebhookData.__annotations__ + assert "delivery_mode_filter" in lm_types.UpdateWebhookData.__annotations__ + assert "delivery_mode_filter" in lm_types.WebhookData.__annotations__ + assert "sandbox" in lm_types.WebhookDeliveryData.__annotations__ diff --git a/tests/test_email.py b/tests/test_email.py index 0fe8085..8b86a46 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -121,7 +121,7 @@ def test_send_with_attachments(self, api_token: str) -> None: client.email.from_("sender@example.com").to("recipient@example.com").subject( "Test" ).attach("document.pdf", "base64content").attach( - "logo.png", "base64image", "logo@example.com", "image/png" + "logo.png", "base64image", "logo@example.com" ).send() import json @@ -133,7 +133,6 @@ def test_send_with_attachments(self, api_token: str) -> None: "filename": "logo.png", "content": "base64image", "content_id": "logo@example.com", - "content_type": "image/png", } @respx.mock @@ -146,20 +145,12 @@ def test_send_with_custom_headers(self, api_token: str) -> None: with Lettermint(api_token=api_token) as client: client.email.from_("sender@example.com").to("recipient@example.com").subject( "Test" - ).headers( - { - "Message-ID": "", - "X-LM-Preserve-Message-ID": "true", - } - ).send() + ).headers({"X-Custom-Header": "value"}).send() import json body = json.loads(route.calls.last.request.content) - assert body["headers"] == { - "Message-ID": "", - "X-LM-Preserve-Message-ID": "true", - } + assert body["headers"] == {"X-Custom-Header": "value"} @respx.mock def test_send_with_idempotency_key(self, api_token: str) -> None: @@ -186,49 +177,13 @@ def test_send_with_metadata_and_tag(self, api_token: str) -> None: with Lettermint(api_token=api_token) as client: client.email.from_("sender@example.com").to("recipient@example.com").subject( "Test" - ).metadata({"campaign_id": "123"}).tag("welcome").tags( - [{"name": "campaign", "value": "welcome-v2"}] - ).settings({"track_opens": False, "track_clicks": True, "tls": "enforced"}).send() + ).metadata({"campaign_id": "123"}).tag("welcome").send() import json body = json.loads(route.calls.last.request.content) assert body["metadata"] == {"campaign_id": "123"} assert body["tag"] == "welcome" - assert body["tags"] == [{"name": "campaign", "value": "welcome-v2"}] - assert body["settings"] == { - "track_opens": False, - "track_clicks": True, - "tls": "enforced", - } - - def test_typed_message_tags_and_legacy_dictionary_support(self, api_token: str) -> None: - from lettermint import MessageTag - - with Lettermint(api_token=api_token) as client: - endpoint = client.email.tags( - [ - MessageTag(name="campaign", value="welcome"), - {"name": "customer", "value": "new"}, - ] - ) - assert endpoint._payload["tags"] == [ - {"name": "campaign", "value": "welcome"}, - {"name": "customer", "value": "new"}, - ] - - def test_rejects_invalid_message_tags(self, api_token: str) -> None: - with Lettermint(api_token=api_token) as client: - with pytest.raises(ValueError): - client.email.tags( - [{"name": "duplicate", "value": "one"}, {"name": "duplicate", "value": "two"}] - ) - with pytest.raises(ValueError): - client.email.tags([{"name": "__LETTERMINT_internal", "value": "one"}]) - with pytest.raises(ValueError): - client.email.tag("legacy").tags( - [{"name": f"tag_{index}", "value": "one"} for index in range(20)] - ) @respx.mock def test_send_with_route(self, api_token: str) -> None: @@ -247,6 +202,35 @@ def test_send_with_route(self, api_token: str) -> None: body = json.loads(route.calls.last.request.content) assert body["route"] == "my-route" + @respx.mock + def test_send_with_sandbox_result(self, api_token: str) -> None: + """Test selecting a simulated Sandbox result.""" + route = respx.post("https://api.lettermint.co/v1/send").mock( + return_value=Response( + 200, + json={ + "message_id": "msg_123", + "status": "delivered", + "sandbox": True, + "sandbox_result": "clicked", + }, + ) + ) + + with Lettermint(api_token=api_token) as client: + response = ( + client.email.from_("sender@example.com") + .to("recipient@example.com") + .subject("Test") + .sandbox_result("clicked") + .send() + ) + + body = json.loads(route.calls.last.request.content) + assert body["sandbox_result"] == "clicked" + assert response["sandbox"] is True + assert response["sandbox_result"] == "clicked" + @respx.mock def test_validation_error(self, api_token: str) -> None: """Test handling validation errors (422).""" @@ -366,17 +350,12 @@ async def test_send_with_all_options_async(self, api_token: str) -> None: .cc("cc@example.com") .bcc("bcc@example.com") .reply_to("reply@example.com") - .headers( - { - "Message-ID": "", - "X-LM-Preserve-Message-ID": "true", - } - ) - .attach("file.pdf", "base64content", None, "application/pdf") + .headers({"X-Custom": "value"}) + .attach("file.pdf", "base64content") .metadata({"key": "value"}) .tag("campaign") - .settings({"track_opens": False, "tls": "enforced"}) .route("my-route") + .sandbox_result("opened") .idempotency_key("unique-key") .send() ) @@ -392,45 +371,16 @@ async def test_send_with_all_options_async(self, api_token: str) -> None: assert body["cc"] == ["cc@example.com"] assert body["bcc"] == ["bcc@example.com"] assert body["reply_to"] == ["reply@example.com"] - assert body["headers"] == { - "Message-ID": "", - "X-LM-Preserve-Message-ID": "true", - } - assert body["attachments"] == [ - { - "filename": "file.pdf", - "content": "base64content", - "content_type": "application/pdf", - } - ] + assert body["headers"] == {"X-Custom": "value"} + assert body["attachments"] == [{"filename": "file.pdf", "content": "base64content"}] assert body["metadata"] == {"key": "value"} assert body["tag"] == "campaign" assert body["route"] == "my-route" - assert body["settings"] == {"track_opens": False, "tls": "enforced"} + assert body["sandbox_result"] == "opened" request = route.calls.last.request assert request.headers["Idempotency-Key"] == "unique-key" - @respx.mock - @pytest.mark.asyncio - async def test_send_batch_with_idempotency_key_async(self, api_token: str) -> None: - """Test an asynchronous batch idempotency key.""" - route = respx.post("https://api.lettermint.co/v1/send/batch").mock( - return_value=Response(200, json=[{"message_id": "msg_123", "status": "pending"}]) - ) - payload = [ - { - "from": "sender@example.com", - "to": ["recipient@example.com"], - "subject": "Test", - } - ] - - async with AsyncLettermint(api_token=api_token) as client: - await client.email.idempotency_key("batch-key").send_batch(payload) - - assert route.calls.last.request.headers["Idempotency-Key"] == "batch-key" - @respx.mock @pytest.mark.asyncio async def test_deferred_async_sends_use_payload_snapshots(self, api_token: str) -> None: From 13cc8a528277b3b8decf281f3b871b0412c1e3d2 Mon Sep 17 00:00:00 2001 From: bjarn <14638441+bjarn@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:12:04 +0000 Subject: [PATCH 2/3] Fix styling --- src/lettermint/types.py | 1424 +++++++++++++++++++++------------------ 1 file changed, 777 insertions(+), 647 deletions(-) diff --git a/src/lettermint/types.py b/src/lettermint/types.py index 7a3d8b2..f35f87d 100644 --- a/src/lettermint/types.py +++ b/src/lettermint/types.py @@ -2,764 +2,894 @@ from __future__ import annotations -from typing import Any, Literal, TypedDict, Union +from typing import Any, Literal, TypedDict from typing_extensions import NotRequired, Required, TypeAlias -MessageStatus: TypeAlias = Literal['scheduled', 'pending', 'queued', 'quarantined', 'suppressed', 'processed', 'delivered', 'opened', 'clicked', 'soft_bounced', 'hard_bounced', 'spam_complaint', 'failed', 'blocked', 'policy_rejected', 'unsubscribed', 'canceled'] -SandboxResult: TypeAlias = Literal['delivered', 'hard_bounced', 'soft_bounced', 'deferred', 'failed', 'suppressed', 'spam_complaint', 'auto_replied', 'opened', 'clicked', 'unsubscribed'] -TlsPolicy: TypeAlias = Literal['opportunistic', 'enforced'] +MessageStatus: TypeAlias = Literal[ + "scheduled", + "pending", + "queued", + "quarantined", + "suppressed", + "processed", + "delivered", + "opened", + "clicked", + "soft_bounced", + "hard_bounced", + "spam_complaint", + "failed", + "blocked", + "policy_rejected", + "unsubscribed", + "canceled", +] +SandboxResult: TypeAlias = Literal[ + "delivered", + "hard_bounced", + "soft_bounced", + "deferred", + "failed", + "suppressed", + "spam_complaint", + "auto_replied", + "opened", + "clicked", + "unsubscribed", +] +TlsPolicy: TypeAlias = Literal["opportunistic", "enforced"] SendMailRequest = TypedDict( - 'SendMailRequest', - { - 'route': 'NotRequired[str]', - 'from': 'Required[str]', - 'to': 'Required[list[str]]', - 'cc': 'NotRequired[list[str]]', - 'bcc': 'NotRequired[list[str]]', - 'reply_to': 'NotRequired[list[str]]', - 'subject': 'Required[str]', - 'scheduled_at': 'NotRequired[str]', - 'headers': 'NotRequired[dict[str, str]]', - 'metadata': 'NotRequired[dict[str, str]]', - 'tag': 'NotRequired[str | None]', - 'tags': 'NotRequired[list[dict[str, Any]]]', - 'settings': 'NotRequired[dict[str, Any] | None]', - 'html': 'NotRequired[str | None]', - 'text': 'NotRequired[str | None]', - 'attachments': 'NotRequired[list[dict[str, Any]]]', - 'sandbox_result': 'NotRequired[SandboxResult]', + "SendMailRequest", + { + "route": "NotRequired[str]", + "from": "Required[str]", + "to": "Required[list[str]]", + "cc": "NotRequired[list[str]]", + "bcc": "NotRequired[list[str]]", + "reply_to": "NotRequired[list[str]]", + "subject": "Required[str]", + "scheduled_at": "NotRequired[str]", + "headers": "NotRequired[dict[str, str]]", + "metadata": "NotRequired[dict[str, str]]", + "tag": "NotRequired[str | None]", + "tags": "NotRequired[list[dict[str, Any]]]", + "settings": "NotRequired[dict[str, Any] | None]", + "html": "NotRequired[str | None]", + "text": "NotRequired[str | None]", + "attachments": "NotRequired[list[dict[str, Any]]]", + "sandbox_result": "NotRequired[SandboxResult]", }, ) SendBatchMailRequest: TypeAlias = list[SendMailRequest] -AttachmentDelivery: TypeAlias = Literal['inline', 'url'] -BuiltInTeamRole: TypeAlias = Literal['owner', 'admin', 'member'] -DkimMode: TypeAlias = Literal['legacy_txt', 'managed_cname'] -DnsRecordPurpose: TypeAlias = Literal['return_path', 'dmarc', 'dkim_legacy', 'dkim_primary', 'dkim_secondary'] -DnsRecordStatus: TypeAlias = Literal['active', 'failed', 'pending'] -DnsVerificationScope: TypeAlias = Literal['required', 'recommended', 'migration', 'deprecated'] -RecordType: TypeAlias = Literal['TXT', 'CNAME', 'MX'] +AttachmentDelivery: TypeAlias = Literal["inline", "url"] +BuiltInTeamRole: TypeAlias = Literal["owner", "admin", "member"] +DkimMode: TypeAlias = Literal["legacy_txt", "managed_cname"] +DnsRecordPurpose: TypeAlias = Literal[ + "return_path", "dmarc", "dkim_legacy", "dkim_primary", "dkim_secondary" +] +DnsRecordStatus: TypeAlias = Literal["active", "failed", "pending"] +DnsVerificationScope: TypeAlias = Literal["required", "recommended", "migration", "deprecated"] +RecordType: TypeAlias = Literal["TXT", "CNAME", "MX"] DomainDnsRecordData = TypedDict( - 'DomainDnsRecordData', + "DomainDnsRecordData", { - 'id': 'Required[str]', - 'type': 'Required[RecordType]', - 'hostname': 'Required[str]', - 'fqdn': 'Required[str]', - 'content': 'Required[str]', - 'status': 'Required[DnsRecordStatus]', - 'purpose': 'Required[DnsRecordPurpose]', - 'verification_scope': 'Required[DnsVerificationScope]', - 'required_for_verification': 'Required[bool]', - 'verified_at': 'Required[str | None]', - 'last_checked_at': 'Required[str | None]', + "id": "Required[str]", + "type": "Required[RecordType]", + "hostname": "Required[str]", + "fqdn": "Required[str]", + "content": "Required[str]", + "status": "Required[DnsRecordStatus]", + "purpose": "Required[DnsRecordPurpose]", + "verification_scope": "Required[DnsVerificationScope]", + "required_for_verification": "Required[bool]", + "verified_at": "Required[str | None]", + "last_checked_at": "Required[str | None]", }, ) DomainData = TypedDict( - 'DomainData', + "DomainData", { - 'id': 'Required[str]', - 'domain': 'Required[str]', - 'dkim_mode': 'Required[DkimMode]', - 'rotation_ready': 'Required[bool]', - 'status_changed_at': 'Required[str | None]', - 'dns_records': 'NotRequired[list[DomainDnsRecordData]]', - 'projects': 'NotRequired[list[dict[str, Any]]]', - 'created_at': 'Required[str]', + "id": "Required[str]", + "domain": "Required[str]", + "dkim_mode": "Required[DkimMode]", + "rotation_ready": "Required[bool]", + "status_changed_at": "Required[str | None]", + "dns_records": "NotRequired[list[DomainDnsRecordData]]", + "projects": "NotRequired[list[dict[str, Any]]]", + "created_at": "Required[str]", }, ) -DomainStatus: TypeAlias = Literal['verified', 'partially_verified', 'pending_verification', 'failed_verification'] +DomainStatus: TypeAlias = Literal[ + "verified", "partially_verified", "pending_verification", "failed_verification" +] DomainListData = TypedDict( - 'DomainListData', + "DomainListData", { - 'id': 'Required[str]', - 'domain': 'Required[str]', - 'status': 'Required[DomainStatus]', - 'dkim_mode': 'Required[DkimMode]', - 'status_changed_at': 'Required[str | None]', - 'created_at': 'Required[str]', + "id": "Required[str]", + "domain": "Required[str]", + "status": "Required[DomainStatus]", + "dkim_mode": "Required[DkimMode]", + "status_changed_at": "Required[str | None]", + "created_at": "Required[str]", }, ) -InitialRoutes: TypeAlias = Literal['both', 'transactional', 'broadcast'] +InitialRoutes: TypeAlias = Literal["both", "transactional", "broadcast"] MessageAttachmentData = TypedDict( - 'MessageAttachmentData', + "MessageAttachmentData", { - 'size': 'Required[int]', - 'filename': 'Required[str]', - 'content_id': 'Required[str | None]', - 'content_type': 'Required[str]', + "size": "Required[int]", + "filename": "Required[str]", + "content_id": "Required[str | None]", + "content_type": "Required[str]", }, ) -DeliveryMode: TypeAlias = Literal['live', 'sandbox'] +DeliveryMode: TypeAlias = Literal["live", "sandbox"] MessageRecipientData = TypedDict( - 'MessageRecipientData', + "MessageRecipientData", { - 'email': 'Required[str]', - 'name': 'Required[str | None]', - 'sandbox_result': 'Required[SandboxResult | None]', + "email": "Required[str]", + "name": "Required[str | None]", + "sandbox_result": "Required[SandboxResult | None]", }, ) SpamSymbol = TypedDict( - 'SpamSymbol', + "SpamSymbol", { - 'name': 'Required[str]', - 'score': 'Required[float]', - 'options': 'Required[list[str]]', - 'description': 'Required[str | None]', + "name": "Required[str]", + "score": "Required[float]", + "options": "Required[list[str]]", + "description": "Required[str | None]", }, ) -MessageType: TypeAlias = Literal['inbound', 'outbound'] +MessageType: TypeAlias = Literal["inbound", "outbound"] MessageData = TypedDict( - 'MessageData', - { - 'id': 'Required[str]', - 'type': 'Required[MessageType]', - 'status': 'Required[MessageStatus]', - 'status_changed_at': 'Required[str | None]', - 'scheduled_at': 'Required[str | None]', - 'tag': 'Required[str | None]', - 'tags': 'Required[list[dict[str, Any]]]', - 'from_email': 'Required[str]', - 'from_name': 'Required[str | None]', - 'reply_to': 'Required[list[str] | None]', - 'subject': 'Required[str | None]', - 'to': 'Required[list[MessageRecipientData] | None]', - 'cc': 'Required[list[MessageRecipientData] | None]', - 'bcc': 'Required[list[MessageRecipientData] | None]', - 'attachments': 'Required[list[MessageAttachmentData] | None]', - 'metadata': 'Required[dict[str, str] | None]', - 'spam_score': 'NotRequired[float | None]', - 'spam_symbols': 'NotRequired[list[SpamSymbol]]', - 'route_id': 'Required[str]', - 'created_at': 'Required[str]', - 'delivery_mode': 'Required[DeliveryMode]', - 'sandbox_result': 'Required[SandboxResult | None]', - }, -) - -MessageEventType: TypeAlias = Literal['scheduled', 'rescheduled', 'canceled', 'released', 'queued', 'processed', 'suppressed', 'delivered', 'auto_replied', 'soft_bounced', 'hard_bounced', 'spam_complaint', 'failed', 'blocked', 'policy_rejected', 'unsubscribed', 'opened', 'clicked', 'inbound_received', 'inbound_queued', 'inbound_spam_blocked', 'inbound_released', 'inbound_processed', 'inbound_retry'] + "MessageData", + { + "id": "Required[str]", + "type": "Required[MessageType]", + "status": "Required[MessageStatus]", + "status_changed_at": "Required[str | None]", + "scheduled_at": "Required[str | None]", + "tag": "Required[str | None]", + "tags": "Required[list[dict[str, Any]]]", + "from_email": "Required[str]", + "from_name": "Required[str | None]", + "reply_to": "Required[list[str] | None]", + "subject": "Required[str | None]", + "to": "Required[list[MessageRecipientData] | None]", + "cc": "Required[list[MessageRecipientData] | None]", + "bcc": "Required[list[MessageRecipientData] | None]", + "attachments": "Required[list[MessageAttachmentData] | None]", + "metadata": "Required[dict[str, str] | None]", + "spam_score": "NotRequired[float | None]", + "spam_symbols": "NotRequired[list[SpamSymbol]]", + "route_id": "Required[str]", + "created_at": "Required[str]", + "delivery_mode": "Required[DeliveryMode]", + "sandbox_result": "Required[SandboxResult | None]", + }, +) + +MessageEventType: TypeAlias = Literal[ + "scheduled", + "rescheduled", + "canceled", + "released", + "queued", + "processed", + "suppressed", + "delivered", + "auto_replied", + "soft_bounced", + "hard_bounced", + "spam_complaint", + "failed", + "blocked", + "policy_rejected", + "unsubscribed", + "opened", + "clicked", + "inbound_received", + "inbound_queued", + "inbound_spam_blocked", + "inbound_released", + "inbound_processed", + "inbound_retry", +] MessageEventData = TypedDict( - 'MessageEventData', + "MessageEventData", { - 'message_id': 'Required[str]', - 'event': 'Required[MessageEventType]', - 'tag': 'Required[str | None]', - 'tags': 'Required[list[dict[str, Any]]]', - 'metadata': 'Required[dict[str, Any] | None]', - 'timestamp': 'Required[str]', + "message_id": "Required[str]", + "event": "Required[MessageEventType]", + "tag": "Required[str | None]", + "tags": "Required[list[dict[str, Any]]]", + "metadata": "Required[dict[str, Any] | None]", + "timestamp": "Required[str]", }, ) MessageListData = TypedDict( - 'MessageListData', - { - 'id': 'Required[str]', - 'type': 'Required[MessageType]', - 'status': 'Required[MessageStatus]', - 'scheduled_at': 'Required[str | None]', - 'spam_score': 'NotRequired[float | None]', - 'from_email': 'Required[str]', - 'from_name': 'Required[str | None]', - 'subject': 'Required[str | None]', - 'to': 'Required[list[MessageRecipientData] | None]', - 'cc': 'Required[list[MessageRecipientData] | None]', - 'bcc': 'Required[list[MessageRecipientData] | None]', - 'reply_to': 'Required[list[str] | None]', - 'tag': 'Required[str | None]', - 'tags': 'Required[list[dict[str, Any]]]', - 'status_changed_at': 'Required[str | None]', - 'created_at': 'Required[str]', - 'delivery_mode': 'Required[DeliveryMode]', - 'sandbox_result': 'Required[SandboxResult | None]', + "MessageListData", + { + "id": "Required[str]", + "type": "Required[MessageType]", + "status": "Required[MessageStatus]", + "scheduled_at": "Required[str | None]", + "spam_score": "NotRequired[float | None]", + "from_email": "Required[str]", + "from_name": "Required[str | None]", + "subject": "Required[str | None]", + "to": "Required[list[MessageRecipientData] | None]", + "cc": "Required[list[MessageRecipientData] | None]", + "bcc": "Required[list[MessageRecipientData] | None]", + "reply_to": "Required[list[str] | None]", + "tag": "Required[str | None]", + "tags": "Required[list[dict[str, Any]]]", + "status_changed_at": "Required[str | None]", + "created_at": "Required[str]", + "delivery_mode": "Required[DeliveryMode]", + "sandbox_result": "Required[SandboxResult | None]", }, ) MessageStatsData = TypedDict( - 'MessageStatsData', + "MessageStatsData", { - 'messages_transactional': 'Required[int]', - 'messages_broadcast': 'Required[int]', - 'messages_inbound': 'Required[int]', - 'deliverability': 'Required[float]', + "messages_transactional": "Required[int]", + "messages_broadcast": "Required[int]", + "messages_inbound": "Required[int]", + "deliverability": "Required[float]", }, ) -Plan: TypeAlias = Literal['free', 'starter', 'growth', 'pro'] -ProjectAccessScope: TypeAlias = Literal['all', 'selected'] +Plan: TypeAlias = Literal["free", "starter", "growth", "pro"] +ProjectAccessScope: TypeAlias = Literal["all", "selected"] RouteStatisticData = TypedDict( - 'RouteStatisticData', - { - 'date': 'Required[str]', - 'sent_count': 'Required[int]', - 'delivered_count': 'Required[int]', - 'opened_count': 'Required[int]', - 'clicked_count': 'Required[int]', - 'hard_bounce_count': 'Required[int]', - 'spam_complaint_count': 'Required[int]', - 'inbound_received_count': 'Required[int]', - 'observed_opened_count': 'NotRequired[int | None]', - 'human_opened_count': 'NotRequired[int | None]', - 'privacy_opened_count': 'NotRequired[int | None]', - 'effective_opened_count': 'Required[int | None]', - 'machine_opened_count': 'Required[int | None]', - 'machine_clicked_count': 'Required[int | None]', - }, -) - -RouteType: TypeAlias = Literal['transactional', 'broadcast', 'inbound'] + "RouteStatisticData", + { + "date": "Required[str]", + "sent_count": "Required[int]", + "delivered_count": "Required[int]", + "opened_count": "Required[int]", + "clicked_count": "Required[int]", + "hard_bounce_count": "Required[int]", + "spam_complaint_count": "Required[int]", + "inbound_received_count": "Required[int]", + "observed_opened_count": "NotRequired[int | None]", + "human_opened_count": "NotRequired[int | None]", + "privacy_opened_count": "NotRequired[int | None]", + "effective_opened_count": "Required[int | None]", + "machine_opened_count": "Required[int | None]", + "machine_clicked_count": "Required[int | None]", + }, +) + +RouteType: TypeAlias = Literal["transactional", "broadcast", "inbound"] RouteData = TypedDict( - 'RouteData', - { - 'id': 'Required[str]', - 'project_id': 'Required[str]', - 'slug': 'Required[str]', - 'name': 'Required[str]', - 'route_type': 'Required[RouteType]', - 'is_default': 'Required[bool]', - 'inbound_address': 'NotRequired[str | None]', - 'inbound_mx_hostname': 'NotRequired[str]', - 'inbound_domain': 'NotRequired[str | None]', - 'inbound_domain_verified_at': 'NotRequired[str | None]', - 'inbound_spam_threshold': 'NotRequired[float | None]', - 'attachment_delivery': 'NotRequired[AttachmentDelivery]', - 'settings': 'NotRequired[dict[str, Any] | None]', - 'project': 'NotRequired[ProjectData]', - 'webhooks_count': 'NotRequired[int]', - 'suppressed_recipients_count': 'NotRequired[int]', - 'statistics': 'NotRequired[list[RouteStatisticData]]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', + "RouteData", + { + "id": "Required[str]", + "project_id": "Required[str]", + "slug": "Required[str]", + "name": "Required[str]", + "route_type": "Required[RouteType]", + "is_default": "Required[bool]", + "inbound_address": "NotRequired[str | None]", + "inbound_mx_hostname": "NotRequired[str]", + "inbound_domain": "NotRequired[str | None]", + "inbound_domain_verified_at": "NotRequired[str | None]", + "inbound_spam_threshold": "NotRequired[float | None]", + "attachment_delivery": "NotRequired[AttachmentDelivery]", + "settings": "NotRequired[dict[str, Any] | None]", + "project": "NotRequired[ProjectData]", + "webhooks_count": "NotRequired[int]", + "suppressed_recipients_count": "NotRequired[int]", + "statistics": "NotRequired[list[RouteStatisticData]]", + "created_at": "Required[str]", + "updated_at": "Required[str]", }, ) ProjectData = TypedDict( - 'ProjectData', - { - 'id': 'Required[str]', - 'name': 'Required[str]', - 'smtp_enabled': 'Required[bool]', - 'redact_email_content': 'Required[bool]', - 'default_route_id': 'Required[str | None]', - 'token_generated_at': 'Required[str | None]', - 'token_last_used_at': 'Required[str | None]', - 'token_last_used_ip': 'Required[str | None]', - 'routes': 'NotRequired[list[RouteData]]', - 'routes_count': 'NotRequired[int]', - 'domains': 'NotRequired[list[DomainData]]', - 'domains_count': 'NotRequired[int]', - 'last_28_days': 'NotRequired[MessageStatsData | None]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', - 'delivery_mode': 'Required[DeliveryMode]', + "ProjectData", + { + "id": "Required[str]", + "name": "Required[str]", + "smtp_enabled": "Required[bool]", + "redact_email_content": "Required[bool]", + "default_route_id": "Required[str | None]", + "token_generated_at": "Required[str | None]", + "token_last_used_at": "Required[str | None]", + "token_last_used_ip": "Required[str | None]", + "routes": "NotRequired[list[RouteData]]", + "routes_count": "NotRequired[int]", + "domains": "NotRequired[list[DomainData]]", + "domains_count": "NotRequired[int]", + "last_28_days": "NotRequired[MessageStatsData | None]", + "created_at": "Required[str]", + "updated_at": "Required[str]", + "delivery_mode": "Required[DeliveryMode]", }, ) ProjectListData = TypedDict( - 'ProjectListData', - { - 'id': 'Required[str]', - 'name': 'Required[str]', - 'smtp_enabled': 'Required[bool]', - 'routes_count': 'Required[int]', - 'domains_count': 'Required[int]', - 'last_28_days': 'Required[MessageStatsData]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', - }, -) - -RbacConflictCode: TypeAlias = Literal['stale_resource', 'owner_protected', 'last_owner', 'built_in_role_immutable', 'custom_role_requires_pro'] -RbacPermission: TypeAlias = Literal['team:manage', 'billing:manage', 'security:manage', 'audit:read', 'support:manage', 'members:read', 'members:manage', 'roles:manage', 'team_tokens:read', 'team_tokens:manage', 'team_tokens:rotate', 'team_tokens:revoke', 'projects:create', 'team_suppressions:read', 'team_suppressions:add', 'team_suppressions:remove', 'projects:read', 'projects:manage', 'projects:delete', 'routes:read', 'routes:manage', 'routes:delete', 'domains:read', 'domains:manage', 'domains:delete', 'project_tokens:read', 'project_tokens:manage', 'project_tokens:rotate', 'project_tokens:revoke', 'webhooks:read', 'webhooks:manage', 'webhooks:delete', 'webhooks:rotate_secret', 'stats:read', 'messages:read', 'messages:read_content', 'messages:send', 'suppressions:read', 'suppressions:add', 'suppressions:remove'] + "ProjectListData", + { + "id": "Required[str]", + "name": "Required[str]", + "smtp_enabled": "Required[bool]", + "routes_count": "Required[int]", + "domains_count": "Required[int]", + "last_28_days": "Required[MessageStatsData]", + "created_at": "Required[str]", + "updated_at": "Required[str]", + }, +) + +RbacConflictCode: TypeAlias = Literal[ + "stale_resource", + "owner_protected", + "last_owner", + "built_in_role_immutable", + "custom_role_requires_pro", +] +RbacPermission: TypeAlias = Literal[ + "team:manage", + "billing:manage", + "security:manage", + "audit:read", + "support:manage", + "members:read", + "members:manage", + "roles:manage", + "team_tokens:read", + "team_tokens:manage", + "team_tokens:rotate", + "team_tokens:revoke", + "projects:create", + "team_suppressions:read", + "team_suppressions:add", + "team_suppressions:remove", + "projects:read", + "projects:manage", + "projects:delete", + "routes:read", + "routes:manage", + "routes:delete", + "domains:read", + "domains:manage", + "domains:delete", + "project_tokens:read", + "project_tokens:manage", + "project_tokens:rotate", + "project_tokens:revoke", + "webhooks:read", + "webhooks:manage", + "webhooks:delete", + "webhooks:rotate_secret", + "stats:read", + "messages:read", + "messages:read_content", + "messages:send", + "suppressions:read", + "suppressions:add", + "suppressions:remove", +] RescheduleMessageRequest = TypedDict( - 'RescheduleMessageRequest', + "RescheduleMessageRequest", { - 'scheduled_at': 'Required[str]', + "scheduled_at": "Required[str]", }, ) RouteListData = TypedDict( - 'RouteListData', + "RouteListData", { - 'id': 'Required[str]', - 'slug': 'Required[str]', - 'name': 'Required[str]', - 'route_type': 'Required[RouteType]', - 'is_default': 'Required[bool]', - 'webhooks_count': 'Required[int]', - 'suppressed_recipients_count': 'Required[int]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', + "id": "Required[str]", + "slug": "Required[str]", + "name": "Required[str]", + "route_type": "Required[RouteType]", + "is_default": "Required[bool]", + "webhooks_count": "Required[int]", + "suppressed_recipients_count": "Required[int]", + "created_at": "Required[str]", + "updated_at": "Required[str]", }, ) StatsInboundData = TypedDict( - 'StatsInboundData', + "StatsInboundData", { - 'received': 'Required[int]', + "received": "Required[int]", }, ) StatsTypeData = TypedDict( - 'StatsTypeData', + "StatsTypeData", { - 'sent': 'Required[int]', - 'hard_bounced': 'Required[int]', - 'spam_complaints': 'Required[int]', + "sent": "Required[int]", + "hard_bounced": "Required[int]", + "spam_complaints": "Required[int]", }, ) StatsDailyData = TypedDict( - 'StatsDailyData', - { - 'date': 'Required[str]', - 'sent': 'Required[int]', - 'delivered': 'Required[int]', - 'hard_bounced': 'Required[int]', - 'spam_complaints': 'Required[int]', - 'opened': 'Required[int | None]', - 'clicked': 'Required[int | None]', - 'inbound': 'Required[StatsInboundData]', - 'transactional': 'Required[StatsTypeData | None]', - 'broadcast': 'Required[StatsTypeData | None]', - 'observed_opened': 'NotRequired[int | None]', - 'human_opened': 'NotRequired[int | None]', - 'privacy_opened': 'NotRequired[int | None]', - 'effective_opened': 'Required[int | None]', - 'machine_opened': 'Required[int | None]', - 'machine_clicked': 'Required[int | None]', + "StatsDailyData", + { + "date": "Required[str]", + "sent": "Required[int]", + "delivered": "Required[int]", + "hard_bounced": "Required[int]", + "spam_complaints": "Required[int]", + "opened": "Required[int | None]", + "clicked": "Required[int | None]", + "inbound": "Required[StatsInboundData]", + "transactional": "Required[StatsTypeData | None]", + "broadcast": "Required[StatsTypeData | None]", + "observed_opened": "NotRequired[int | None]", + "human_opened": "NotRequired[int | None]", + "privacy_opened": "NotRequired[int | None]", + "effective_opened": "Required[int | None]", + "machine_opened": "Required[int | None]", + "machine_clicked": "Required[int | None]", }, ) StatsTotalsData = TypedDict( - 'StatsTotalsData', - { - 'sent': 'Required[int]', - 'delivered': 'Required[int]', - 'hard_bounced': 'Required[int]', - 'spam_complaints': 'Required[int]', - 'opened': 'Required[int | None]', - 'clicked': 'Required[int | None]', - 'inbound': 'Required[StatsInboundData]', - 'transactional': 'Required[StatsTypeData | None]', - 'broadcast': 'Required[StatsTypeData | None]', - 'observed_opened': 'NotRequired[int | None]', - 'human_opened': 'NotRequired[int | None]', - 'privacy_opened': 'NotRequired[int | None]', - 'effective_opened': 'Required[int | None]', - 'machine_opened': 'Required[int | None]', - 'machine_clicked': 'Required[int | None]', + "StatsTotalsData", + { + "sent": "Required[int]", + "delivered": "Required[int]", + "hard_bounced": "Required[int]", + "spam_complaints": "Required[int]", + "opened": "Required[int | None]", + "clicked": "Required[int | None]", + "inbound": "Required[StatsInboundData]", + "transactional": "Required[StatsTypeData | None]", + "broadcast": "Required[StatsTypeData | None]", + "observed_opened": "NotRequired[int | None]", + "human_opened": "NotRequired[int | None]", + "privacy_opened": "NotRequired[int | None]", + "effective_opened": "Required[int | None]", + "machine_opened": "Required[int | None]", + "machine_clicked": "Required[int | None]", }, ) StatsData = TypedDict( - 'StatsData', + "StatsData", { - 'from': 'Required[str]', - 'to': 'Required[str]', - 'totals': 'Required[StatsTotalsData]', - 'daily': 'Required[list[StatsDailyData]]', + "from": "Required[str]", + "to": "Required[str]", + "totals": "Required[StatsTotalsData]", + "daily": "Required[list[StatsDailyData]]", }, ) StatsRequestData = TypedDict( - 'StatsRequestData', + "StatsRequestData", { - 'from': 'Required[str]', - 'to': 'Required[str]', - 'project_id': 'NotRequired[str | None]', - 'include_machine': 'NotRequired[bool]', + "from": "Required[str]", + "to": "Required[str]", + "project_id": "NotRequired[str | None]", + "include_machine": "NotRequired[bool]", }, ) StoreDomainData = TypedDict( - 'StoreDomainData', + "StoreDomainData", { - 'domain': 'Required[str]', + "domain": "Required[str]", }, ) StoreProjectData = TypedDict( - 'StoreProjectData', + "StoreProjectData", { - 'name': 'Required[str]', - 'smtp_enabled': 'NotRequired[bool]', - 'initial_routes': 'NotRequired[InitialRoutes]', - 'short_token': 'NotRequired[bool]', - 'delivery_mode': 'NotRequired[DeliveryMode]', + "name": "Required[str]", + "smtp_enabled": "NotRequired[bool]", + "initial_routes": "NotRequired[InitialRoutes]", + "short_token": "NotRequired[bool]", + "delivery_mode": "NotRequired[DeliveryMode]", }, ) StoreRouteData = TypedDict( - 'StoreRouteData', + "StoreRouteData", { - 'name': 'Required[str]', - 'route_type': 'Required[RouteType]', - 'slug': 'NotRequired[str | None]', + "name": "Required[str]", + "route_type": "Required[RouteType]", + "slug": "NotRequired[str | None]", }, ) -SuppressionReason: TypeAlias = Literal['spam_complaint', 'hard_bounce', 'unsubscribe', 'manual'] -SuppressionScope: TypeAlias = Literal['team', 'project', 'route'] -SuppressionAppliesTo: TypeAlias = Literal['all', 'broadcast'] +SuppressionReason: TypeAlias = Literal["spam_complaint", "hard_bounce", "unsubscribe", "manual"] +SuppressionScope: TypeAlias = Literal["team", "project", "route"] +SuppressionAppliesTo: TypeAlias = Literal["all", "broadcast"] StoreSuppressionData = TypedDict( - 'StoreSuppressionData', - { - 'email': 'NotRequired[str | None]', - 'emails': 'NotRequired[list[str] | None]', - 'reason': 'Required[SuppressionReason]', - 'scope': 'Required[SuppressionScope]', - 'route_id': 'NotRequired[str | None]', - 'project_id': 'NotRequired[str | None]', - 'applies_to': 'NotRequired[SuppressionAppliesTo | None]', - }, -) - -WebhookEvent: TypeAlias = Literal['message.created', 'message.sent', 'message.delivered', 'message.auto_replied', 'message.hard_bounced', 'message.soft_bounced', 'message.spam_complaint', 'message.failed', 'message.suppressed', 'message.unsubscribed', 'message.opened', 'message.clicked', 'message.inbound', 'message.policy_rejected', 'message.scheduled', 'message.rescheduled', 'message.canceled', 'message.released', 'suppression.added', 'suppression.removed', 'webhook.test'] -WebhookScope: TypeAlias = Literal['team', 'project', 'route'] -WebhookDeliveryModeFilter: TypeAlias = Literal['live', 'sandbox', 'both'] + "StoreSuppressionData", + { + "email": "NotRequired[str | None]", + "emails": "NotRequired[list[str] | None]", + "reason": "Required[SuppressionReason]", + "scope": "Required[SuppressionScope]", + "route_id": "NotRequired[str | None]", + "project_id": "NotRequired[str | None]", + "applies_to": "NotRequired[SuppressionAppliesTo | None]", + }, +) + +WebhookEvent: TypeAlias = Literal[ + "message.created", + "message.sent", + "message.delivered", + "message.auto_replied", + "message.hard_bounced", + "message.soft_bounced", + "message.spam_complaint", + "message.failed", + "message.suppressed", + "message.unsubscribed", + "message.opened", + "message.clicked", + "message.inbound", + "message.policy_rejected", + "message.scheduled", + "message.rescheduled", + "message.canceled", + "message.released", + "suppression.added", + "suppression.removed", + "webhook.test", +] +WebhookScope: TypeAlias = Literal["team", "project", "route"] +WebhookDeliveryModeFilter: TypeAlias = Literal["live", "sandbox", "both"] StoreWebhookData = TypedDict( - 'StoreWebhookData', + "StoreWebhookData", { - 'name': 'Required[str]', - 'url': 'Required[str]', - 'events': 'Required[list[WebhookEvent]]', - 'enabled': 'NotRequired[bool | None]', - 'include_machine_events': 'NotRequired[bool | None]', - 'scope': 'NotRequired[WebhookScope | None]', - 'project_ids': 'NotRequired[list[str]]', - 'route_ids': 'NotRequired[list[str]]', - 'route_id': 'NotRequired[str | None]', - 'delivery_mode_filter': 'NotRequired[WebhookDeliveryModeFilter | None]', + "name": "Required[str]", + "url": "Required[str]", + "events": "Required[list[WebhookEvent]]", + "enabled": "NotRequired[bool | None]", + "include_machine_events": "NotRequired[bool | None]", + "scope": "NotRequired[WebhookScope | None]", + "project_ids": "NotRequired[list[str]]", + "route_ids": "NotRequired[list[str]]", + "route_id": "NotRequired[str | None]", + "delivery_mode_filter": "NotRequired[WebhookDeliveryModeFilter | None]", }, ) SuppressionSourceMessageData = TypedDict( - 'SuppressionSourceMessageData', + "SuppressionSourceMessageData", { - 'id': 'Required[str]', - 'available': 'Required[bool]', - 'subject': 'Required[str | None]', - 'created_at': 'Required[str | None]', + "id": "Required[str]", + "available": "Required[bool]", + "subject": "Required[str | None]", + "created_at": "Required[str | None]", }, ) -SuppressionType: TypeAlias = Literal['email', 'domain', 'extension'] +SuppressionType: TypeAlias = Literal["email", "domain", "extension"] SuppressedRecipientData = TypedDict( - 'SuppressedRecipientData', + "SuppressedRecipientData", { - 'id': 'Required[str]', - 'type': 'Required[SuppressionType]', - 'value': 'Required[str]', - 'reason': 'Required[SuppressionReason]', - 'scope': 'Required[SuppressionScope]', - 'applies_to': 'Required[SuppressionAppliesTo]', - 'project_id': 'Required[str | None]', - 'route_id': 'Required[str | None]', - 'source_message': 'NotRequired[SuppressionSourceMessageData | None]', - 'created_at': 'Required[str]', + "id": "Required[str]", + "type": "Required[SuppressionType]", + "value": "Required[str]", + "reason": "Required[SuppressionReason]", + "scope": "Required[SuppressionScope]", + "applies_to": "Required[SuppressionAppliesTo]", + "project_id": "Required[str | None]", + "route_id": "Required[str | None]", + "source_message": "NotRequired[SuppressionSourceMessageData | None]", + "created_at": "Required[str]", }, ) TeamAddonData = TypedDict( - 'TeamAddonData', + "TeamAddonData", { - 'type': 'Required[str | None]', - 'expires_at': 'Required[str | None]', + "type": "Required[str | None]", + "expires_at": "Required[str | None]", }, ) -TeamType: TypeAlias = Literal['personal', 'business'] +TeamType: TypeAlias = Literal["personal", "business"] TeamData = TypedDict( - 'TeamData', + "TeamData", { - 'id': 'Required[str]', - 'name': 'Required[str]', - 'type': 'Required[TeamType]', - 'plan': 'Required[Plan]', - 'included_volume': 'Required[int]', - 'tier': 'Required[int]', - 'verified_at': 'Required[str | None]', - 'features': 'NotRequired[list[str]]', - 'addons': 'NotRequired[list[TeamAddonData]]', - 'created_at': 'Required[str]', - 'domains_count': 'NotRequired[int]', - 'projects_count': 'NotRequired[int]', - 'members_count': 'NotRequired[int]', + "id": "Required[str]", + "name": "Required[str]", + "type": "Required[TeamType]", + "plan": "Required[Plan]", + "included_volume": "Required[int]", + "tier": "Required[int]", + "verified_at": "Required[str | None]", + "features": "NotRequired[list[str]]", + "addons": "NotRequired[list[TeamAddonData]]", + "created_at": "Required[str]", + "domains_count": "NotRequired[int]", + "projects_count": "NotRequired[int]", + "members_count": "NotRequired[int]", }, ) TeamMemberProjectAccessData = TypedDict( - 'TeamMemberProjectAccessData', + "TeamMemberProjectAccessData", { - 'scope': 'Required[ProjectAccessScope]', - 'projects': 'Required[list[dict[str, Any]]]', + "scope": "Required[ProjectAccessScope]", + "projects": "Required[list[dict[str, Any]]]", }, ) TeamMemberData = TypedDict( - 'TeamMemberData', + "TeamMemberData", { - 'id': 'Required[str]', - 'name': 'Required[str]', - 'email': 'Required[str]', - 'role': 'Required[dict[str, Any]]', - 'project_access': 'Required[TeamMemberProjectAccessData]', - 'joined_at': 'Required[str | None]', + "id": "Required[str]", + "name": "Required[str]", + "email": "Required[str]", + "role": "Required[dict[str, Any]]", + "project_access": "Required[TeamMemberProjectAccessData]", + "joined_at": "Required[str | None]", }, ) TeamRoleData = TypedDict( - 'TeamRoleData', + "TeamRoleData", { - 'id': 'Required[str]', - 'name': 'Required[str]', - 'system_key': 'Required[BuiltInTeamRole | None]', - 'permissions': 'Required[list[RbacPermission]]', - 'assignable': 'Required[bool]', + "id": "Required[str]", + "name": "Required[str]", + "system_key": "Required[BuiltInTeamRole | None]", + "permissions": "Required[list[RbacPermission]]", + "assignable": "Required[bool]", }, ) TeamUsagePeriodData = TypedDict( - 'TeamUsagePeriodData', + "TeamUsagePeriodData", { - 'usage': 'Required[int]', - 'last_incremented_at': 'Required[str | None]', - 'period_start': 'Required[str]', - 'period_end': 'Required[str]', + "usage": "Required[int]", + "last_incremented_at": "Required[str | None]", + "period_start": "Required[str]", + "period_end": "Required[str]", }, ) TeamUsageDetailData = TypedDict( - 'TeamUsageDetailData', + "TeamUsageDetailData", { - 'current_period': 'Required[TeamUsagePeriodData]', - 'historical_usage': 'Required[list[TeamUsagePeriodData]]', + "current_period": "Required[TeamUsagePeriodData]", + "historical_usage": "Required[list[TeamUsagePeriodData]]", }, ) UpdateDomainProjectsData = TypedDict( - 'UpdateDomainProjectsData', + "UpdateDomainProjectsData", { - 'project_ids': 'Required[list[str]]', + "project_ids": "Required[list[str]]", }, ) UpdateProjectData = TypedDict( - 'UpdateProjectData', + "UpdateProjectData", { - 'name': 'NotRequired[str | None]', - 'smtp_enabled': 'NotRequired[bool | None]', - 'redact_email_content': 'NotRequired[bool | None]', - 'default_route_id': 'NotRequired[str | None]', - 'delivery_mode': 'NotRequired[DeliveryMode | None]', + "name": "NotRequired[str | None]", + "smtp_enabled": "NotRequired[bool | None]", + "redact_email_content": "NotRequired[bool | None]", + "default_route_id": "NotRequired[str | None]", + "delivery_mode": "NotRequired[DeliveryMode | None]", }, ) UpdateRouteInboundSettingsData = TypedDict( - 'UpdateRouteInboundSettingsData', + "UpdateRouteInboundSettingsData", { - 'inbound_domain': 'NotRequired[str | None]', - 'inbound_spam_threshold': 'NotRequired[float | None]', - 'attachment_delivery': 'NotRequired[AttachmentDelivery | None]', + "inbound_domain": "NotRequired[str | None]", + "inbound_spam_threshold": "NotRequired[float | None]", + "attachment_delivery": "NotRequired[AttachmentDelivery | None]", }, ) UpdateRouteSettingsData = TypedDict( - 'UpdateRouteSettingsData', + "UpdateRouteSettingsData", { - 'track_opens': 'NotRequired[bool | None]', - 'track_clicks': 'NotRequired[bool | None]', - 'generate_plaintext_fallback': 'NotRequired[bool | None]', - 'suppress_auto_responders': 'NotRequired[bool | None]', - 'suppress_disposable_recipients': 'NotRequired[bool | None]', - 'tls': 'NotRequired[TlsPolicy | None]', - 'disable_hosted_unsubscribe': 'NotRequired[bool | None]', - 'redact_email_content': 'NotRequired[bool | None]', + "track_opens": "NotRequired[bool | None]", + "track_clicks": "NotRequired[bool | None]", + "generate_plaintext_fallback": "NotRequired[bool | None]", + "suppress_auto_responders": "NotRequired[bool | None]", + "suppress_disposable_recipients": "NotRequired[bool | None]", + "tls": "NotRequired[TlsPolicy | None]", + "disable_hosted_unsubscribe": "NotRequired[bool | None]", + "redact_email_content": "NotRequired[bool | None]", }, ) UpdateRouteData = TypedDict( - 'UpdateRouteData', + "UpdateRouteData", { - 'name': 'NotRequired[str | None]', - 'settings': 'NotRequired[UpdateRouteSettingsData | None]', - 'inbound_settings': 'NotRequired[UpdateRouteInboundSettingsData | None]', + "name": "NotRequired[str | None]", + "settings": "NotRequired[UpdateRouteSettingsData | None]", + "inbound_settings": "NotRequired[UpdateRouteInboundSettingsData | None]", }, ) UpdateTeamData = TypedDict( - 'UpdateTeamData', + "UpdateTeamData", { - 'name': 'NotRequired[str | None]', + "name": "NotRequired[str | None]", }, ) UpdateTeamMemberAssignmentData = TypedDict( - 'UpdateTeamMemberAssignmentData', + "UpdateTeamMemberAssignmentData", { - 'role_id': 'Required[str]', - 'project_access': 'Required[dict[str, Any]]', + "role_id": "Required[str]", + "project_access": "Required[dict[str, Any]]", }, ) UpdateWebhookData = TypedDict( - 'UpdateWebhookData', + "UpdateWebhookData", { - 'name': 'NotRequired[str]', - 'url': 'NotRequired[str]', - 'events': 'NotRequired[list[WebhookEvent]]', - 'enabled': 'NotRequired[bool]', - 'include_machine_events': 'NotRequired[bool]', - 'scope': 'NotRequired[WebhookScope]', - 'project_ids': 'NotRequired[list[str]]', - 'route_ids': 'NotRequired[list[str]]', - 'route_id': 'NotRequired[str | None]', - 'delivery_mode_filter': 'NotRequired[WebhookDeliveryModeFilter]', + "name": "NotRequired[str]", + "url": "NotRequired[str]", + "events": "NotRequired[list[WebhookEvent]]", + "enabled": "NotRequired[bool]", + "include_machine_events": "NotRequired[bool]", + "scope": "NotRequired[WebhookScope]", + "project_ids": "NotRequired[list[str]]", + "route_ids": "NotRequired[list[str]]", + "route_id": "NotRequired[str | None]", + "delivery_mode_filter": "NotRequired[WebhookDeliveryModeFilter]", }, ) WebhookData = TypedDict( - 'WebhookData', - { - 'id': 'Required[str]', - 'scope': 'Required[WebhookScope]', - 'project_ids': 'Required[list[str]]', - 'route_ids': 'Required[list[str]]', - 'route_id': 'Required[str | None]', - 'name': 'Required[str]', - 'url': 'Required[str]', - 'events': 'Required[list[str]]', - 'enabled': 'Required[bool]', - 'include_machine_events': 'Required[bool]', - 'last_called_at': 'Required[str | None]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', - 'delivery_mode_filter': 'Required[WebhookDeliveryModeFilter]', - }, -) - -WebhookDeliveryStatus: TypeAlias = Literal['pending', 'success', 'failed', 'client_error', 'server_error', 'timeout'] + "WebhookData", + { + "id": "Required[str]", + "scope": "Required[WebhookScope]", + "project_ids": "Required[list[str]]", + "route_ids": "Required[list[str]]", + "route_id": "Required[str | None]", + "name": "Required[str]", + "url": "Required[str]", + "events": "Required[list[str]]", + "enabled": "Required[bool]", + "include_machine_events": "Required[bool]", + "last_called_at": "Required[str | None]", + "created_at": "Required[str]", + "updated_at": "Required[str]", + "delivery_mode_filter": "Required[WebhookDeliveryModeFilter]", + }, +) + +WebhookDeliveryStatus: TypeAlias = Literal[ + "pending", "success", "failed", "client_error", "server_error", "timeout" +] WebhookDeliveryData = TypedDict( - 'WebhookDeliveryData', - { - 'id': 'Required[str]', - 'webhook_id': 'Required[str]', - 'event_type': 'Required[WebhookEvent]', - 'source_scope': 'Required[str | None]', - 'source_project_id': 'Required[str | None]', - 'source_route_id': 'Required[str | None]', - 'status': 'Required[WebhookDeliveryStatus]', - 'attempt_number': 'Required[int]', - 'http_status_code': 'Required[int | None]', - 'duration_ms': 'Required[int | None]', - 'payload': 'Required[list[str]]', - 'response_body': 'Required[str | None]', - 'response_headers': 'Required[list[str] | None]', - 'error_message': 'Required[str | None]', - 'delivered_at': 'Required[str | None]', - 'timestamp': 'Required[str]', - 'sandbox': 'Required[bool]', + "WebhookDeliveryData", + { + "id": "Required[str]", + "webhook_id": "Required[str]", + "event_type": "Required[WebhookEvent]", + "source_scope": "Required[str | None]", + "source_project_id": "Required[str | None]", + "source_route_id": "Required[str | None]", + "status": "Required[WebhookDeliveryStatus]", + "attempt_number": "Required[int]", + "http_status_code": "Required[int | None]", + "duration_ms": "Required[int | None]", + "payload": "Required[list[str]]", + "response_body": "Required[str | None]", + "response_headers": "Required[list[str] | None]", + "error_message": "Required[str | None]", + "delivered_at": "Required[str | None]", + "timestamp": "Required[str]", + "sandbox": "Required[bool]", }, ) WebhookDeliveryListData = TypedDict( - 'WebhookDeliveryListData', + "WebhookDeliveryListData", { - 'id': 'Required[str]', - 'webhook_id': 'Required[str]', - 'event_type': 'Required[WebhookEvent]', - 'source_scope': 'Required[str | None]', - 'source_project_id': 'Required[str | None]', - 'source_route_id': 'Required[str | None]', - 'status': 'Required[WebhookDeliveryStatus]', - 'attempt_number': 'Required[int]', - 'http_status_code': 'Required[int | None]', - 'duration_ms': 'Required[int | None]', - 'delivered_at': 'Required[str | None]', - 'created_at': 'Required[str]', + "id": "Required[str]", + "webhook_id": "Required[str]", + "event_type": "Required[WebhookEvent]", + "source_scope": "Required[str | None]", + "source_project_id": "Required[str | None]", + "source_route_id": "Required[str | None]", + "status": "Required[WebhookDeliveryStatus]", + "attempt_number": "Required[int]", + "http_status_code": "Required[int | None]", + "duration_ms": "Required[int | None]", + "delivered_at": "Required[str | None]", + "created_at": "Required[str]", }, ) WebhookListData = TypedDict( - 'WebhookListData', + "WebhookListData", { - 'id': 'Required[str]', - 'scope': 'Required[WebhookScope]', - 'project_ids': 'Required[list[str]]', - 'route_ids': 'Required[list[str]]', - 'route_id': 'Required[str | None]', - 'name': 'Required[str]', - 'url': 'Required[str]', - 'events': 'Required[list[str]]', - 'enabled': 'Required[bool]', - 'last_called_at': 'Required[str | None]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', + "id": "Required[str]", + "scope": "Required[WebhookScope]", + "project_ids": "Required[list[str]]", + "route_ids": "Required[list[str]]", + "route_id": "Required[str | None]", + "name": "Required[str]", + "url": "Required[str]", + "events": "Required[list[str]]", + "enabled": "Required[bool]", + "last_called_at": "Required[str | None]", + "created_at": "Required[str]", + "updated_at": "Required[str]", }, ) WebhookSecretData = TypedDict( - 'WebhookSecretData', - { - 'id': 'Required[str]', - 'scope': 'Required[WebhookScope]', - 'project_ids': 'Required[list[str]]', - 'route_ids': 'Required[list[str]]', - 'route_id': 'Required[str | None]', - 'name': 'Required[str]', - 'url': 'Required[str]', - 'events': 'Required[list[str]]', - 'enabled': 'Required[bool]', - 'include_machine_events': 'Required[bool]', - 'secret': 'Required[str]', - 'last_called_at': 'Required[str | None]', - 'created_at': 'Required[str]', - 'updated_at': 'Required[str]', - 'delivery_mode_filter': 'Required[WebhookDeliveryModeFilter]', + "WebhookSecretData", + { + "id": "Required[str]", + "scope": "Required[WebhookScope]", + "project_ids": "Required[list[str]]", + "route_ids": "Required[list[str]]", + "route_id": "Required[str | None]", + "name": "Required[str]", + "url": "Required[str]", + "events": "Required[list[str]]", + "enabled": "Required[bool]", + "include_machine_events": "Required[bool]", + "secret": "Required[str]", + "last_called_at": "Required[str | None]", + "created_at": "Required[str]", + "updated_at": "Required[str]", + "delivery_mode_filter": "Required[WebhookDeliveryModeFilter]", }, ) EmailAttachment = TypedDict( - 'EmailAttachment', + "EmailAttachment", { - 'filename': 'Required[str]', - 'content': 'Required[str]', - 'content_type': 'NotRequired[str | None]', - 'content_id': 'NotRequired[str | None]', + "filename": "Required[str]", + "content": "Required[str]", + "content_type": "NotRequired[str | None]", + "content_id": "NotRequired[str | None]", }, ) EmailPayload: TypeAlias = SendMailRequest EmailStatus: TypeAlias = MessageStatus SendMailResponse = TypedDict( - 'SendMailResponse', + "SendMailResponse", { - 'message_id': 'Required[str | None]', - 'status': 'Required[MessageStatus]', - 'scheduled_at': 'NotRequired[str]', - 'sandbox': 'NotRequired[Literal[True]]', - 'sandbox_result': 'NotRequired[SandboxResult]', + "message_id": "Required[str | None]", + "status": "Required[MessageStatus]", + "scheduled_at": "NotRequired[str]", + "sandbox": "NotRequired[Literal[True]]", + "sandbox_result": "NotRequired[SandboxResult]", }, ) SendBatchMailResponse: TypeAlias = list[SendMailResponse] PingResponse: TypeAlias = Literal[200] DomainIndexResponse = TypedDict( - 'DomainIndexResponse', + "DomainIndexResponse", { - 'data': 'Required[list[DomainListData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "data": "Required[list[DomainListData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) @@ -767,244 +897,244 @@ DomainStoreResponse: TypeAlias = DomainData DomainShowResponse: TypeAlias = DomainData DomainDestroyResponse = TypedDict( - 'DomainDestroyResponse', + "DomainDestroyResponse", { - 'message': "Required[Literal['Domain deleted successfully.']]", + "message": "Required[Literal['Domain deleted successfully.']]", }, ) DomainVerifyDnsRecordsResponse = TypedDict( - 'DomainVerifyDnsRecordsResponse', + "DomainVerifyDnsRecordsResponse", { - 'message': 'Required[str]', - 'recommended_failed_records': 'Required[list[dict[str, Any]]]', + "message": "Required[str]", + "recommended_failed_records": "Required[list[dict[str, Any]]]", }, ) DomainVerifySpecificDnsRecordResponse = TypedDict( - 'DomainVerifySpecificDnsRecordResponse', + "DomainVerifySpecificDnsRecordResponse", { - 'message': "Required[Literal['DNS record verified successfully.']]", + "message": "Required[Literal['DNS record verified successfully.']]", }, ) DomainUpdateProjectsRequest: TypeAlias = UpdateDomainProjectsData DomainUpdateProjectsResponse = TypedDict( - 'DomainUpdateProjectsResponse', + "DomainUpdateProjectsResponse", { - 'data': 'Required[DomainData]', - 'message': "Required[Literal['Domain projects updated successfully.']]", + "data": "Required[DomainData]", + "message": "Required[Literal['Domain projects updated successfully.']]", }, ) BlockedFileTypesResponse = TypedDict( - 'BlockedFileTypesResponse', + "BlockedFileTypesResponse", { - 'extensions': 'Required[list[str]]', - 'mime_types': 'Required[list[str]]', + "extensions": "Required[list[str]]", + "mime_types": "Required[list[str]]", }, ) RescheduleMessageResponse = TypedDict( - 'RescheduleMessageResponse', + "RescheduleMessageResponse", { - 'message_id': 'Required[str]', - 'status': 'Required[MessageStatus | None]', - 'scheduled_at': 'Required[str | None]', + "message_id": "Required[str]", + "status": "Required[MessageStatus | None]", + "scheduled_at": "Required[str | None]", }, ) MessageShowResponse: TypeAlias = MessageData CancelScheduledMessageResponse = TypedDict( - 'CancelScheduledMessageResponse', + "CancelScheduledMessageResponse", { - 'message_id': 'Required[str]', - 'status': 'Required[MessageStatus | None]', - 'scheduled_at': 'Required[str | None]', + "message_id": "Required[str]", + "status": "Required[MessageStatus | None]", + "scheduled_at": "Required[str | None]", }, ) MessageIndexResponse = TypedDict( - 'MessageIndexResponse', + "MessageIndexResponse", { - 'data': 'Required[list[MessageListData]]', - 'links': 'Required[list[str]]', - 'meta': 'Required[dict[str, Any]]', + "data": "Required[list[MessageListData]]", + "links": "Required[list[str]]", + "meta": "Required[dict[str, Any]]", }, ) MessageEventsResponse = TypedDict( - 'MessageEventsResponse', + "MessageEventsResponse", { - 'data': 'Required[list[MessageEventData]]', - 'links': 'Required[list[str]]', - 'meta': 'Required[dict[str, Any]]', + "data": "Required[list[MessageEventData]]", + "links": "Required[list[str]]", + "meta": "Required[dict[str, Any]]", }, ) ProcessInboundMessageResponse = TypedDict( - 'ProcessInboundMessageResponse', + "ProcessInboundMessageResponse", { - 'data': 'Required[dict[str, Any]]', + "data": "Required[dict[str, Any]]", }, ) ProjectIndexResponse = TypedDict( - 'ProjectIndexResponse', + "ProjectIndexResponse", { - 'data': 'Required[list[ProjectListData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "data": "Required[list[ProjectListData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) ProjectStoreRequest: TypeAlias = StoreProjectData ProjectStoreResponse = TypedDict( - 'ProjectStoreResponse', + "ProjectStoreResponse", { - 'data': 'Required[ProjectData]', - 'message': "Required[Literal['Project created successfully.']]", - 'api_token': 'Required[str]', + "data": "Required[ProjectData]", + "message": "Required[Literal['Project created successfully.']]", + "api_token": "Required[str]", }, ) ProjectShowResponse: TypeAlias = ProjectData ProjectUpdateRequest: TypeAlias = UpdateProjectData ProjectUpdateResponse = TypedDict( - 'ProjectUpdateResponse', + "ProjectUpdateResponse", { - 'data': 'Required[ProjectData]', - 'message': "Required[Literal['Project updated successfully.']]", + "data": "Required[ProjectData]", + "message": "Required[Literal['Project updated successfully.']]", }, ) ProjectDestroyResponse = TypedDict( - 'ProjectDestroyResponse', + "ProjectDestroyResponse", { - 'message': "Required[Literal['Project deleted successfully.']]", + "message": "Required[Literal['Project deleted successfully.']]", }, ) ProjectRotateTokenResponse = TypedDict( - 'ProjectRotateTokenResponse', + "ProjectRotateTokenResponse", { - 'data': 'Required[ProjectData]', - 'new_token': 'Required[str]', - 'message': "Required[Literal['API token rotated successfully. Please update your integrations.']]", + "data": "Required[ProjectData]", + "new_token": "Required[str]", + "message": "Required[Literal['API token rotated successfully. Please update your integrations.']]", }, ) RouteIndexResponse = TypedDict( - 'RouteIndexResponse', + "RouteIndexResponse", { - 'data': 'Required[list[RouteListData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "data": "Required[list[RouteListData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) RouteStoreRequest: TypeAlias = StoreRouteData RouteStoreResponse = TypedDict( - 'RouteStoreResponse', + "RouteStoreResponse", { - 'data': 'Required[RouteData]', - 'message': "Required[Literal['Route created successfully.']]", + "data": "Required[RouteData]", + "message": "Required[Literal['Route created successfully.']]", }, ) RouteShowResponse: TypeAlias = RouteData RouteUpdateRequest: TypeAlias = UpdateRouteData RouteUpdateResponse = TypedDict( - 'RouteUpdateResponse', + "RouteUpdateResponse", { - 'data': 'Required[RouteData]', - 'message': "Required[Literal['Route updated successfully.']]", + "data": "Required[RouteData]", + "message": "Required[Literal['Route updated successfully.']]", }, ) RouteDestroyResponse = TypedDict( - 'RouteDestroyResponse', + "RouteDestroyResponse", { - 'message': "Required[Literal['Route deleted successfully.']]", + "message": "Required[Literal['Route deleted successfully.']]", }, ) RouteVerifyInboundDomainResponse = TypedDict( - 'RouteVerifyInboundDomainResponse', + "RouteVerifyInboundDomainResponse", { - 'data': 'Required[dict[str, Any]]', + "data": "Required[dict[str, Any]]", }, ) StatsIndexResponse: TypeAlias = StatsData SuppressionIndexResponse = TypedDict( - 'SuppressionIndexResponse', + "SuppressionIndexResponse", { - 'data': 'Required[list[SuppressedRecipientData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "data": "Required[list[SuppressedRecipientData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) SuppressionStoreRequest: TypeAlias = StoreSuppressionData SuppressionStoreResponse = TypedDict( - 'SuppressionStoreResponse', + "SuppressionStoreResponse", { - 'message': "Required[str | Literal['No emails were added.']]", - 'data': 'Required[dict[str, Any]]', + "message": "Required[str | Literal['No emails were added.']]", + "data": "Required[dict[str, Any]]", }, ) SuppressionDestroyResponse = TypedDict( - 'SuppressionDestroyResponse', + "SuppressionDestroyResponse", { - 'success': 'Required[bool]', - 'status': "Required[Literal['removed']]", - 'message': 'Required[str]', - 'confidence': 'NotRequired[float]', + "success": "Required[bool]", + "status": "Required[Literal['removed']]", + "message": "Required[str]", + "confidence": "NotRequired[float]", }, ) TeamShowResponse: TypeAlias = TeamData TeamUpdateRequest: TypeAlias = UpdateTeamData TeamUpdateResponse = TypedDict( - 'TeamUpdateResponse', + "TeamUpdateResponse", { - 'data': 'Required[TeamData]', - 'message': "Required[Literal['Team settings updated successfully.']]", + "data": "Required[TeamData]", + "message": "Required[Literal['Team settings updated successfully.']]", }, ) TeamUsageResponse: TypeAlias = TeamUsageDetailData TeamRolesResponse = TypedDict( - 'TeamRolesResponse', + "TeamRolesResponse", { - 'data': 'Required[list[TeamRoleData]]', + "data": "Required[list[TeamRoleData]]", }, ) TeamMembersResponse = TypedDict( - 'TeamMembersResponse', + "TeamMembersResponse", { - 'data': 'Required[list[TeamMemberData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "data": "Required[list[TeamMemberData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) @@ -1012,70 +1142,70 @@ TeamMembersAssignmentUpdateRequest: TypeAlias = UpdateTeamMemberAssignmentData TeamMembersAssignmentUpdateResponse: TypeAlias = TeamMemberData WebhookIndexResponse = TypedDict( - 'WebhookIndexResponse', + "WebhookIndexResponse", { - 'data': 'Required[list[WebhookListData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "data": "Required[list[WebhookListData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) WebhookStoreRequest: TypeAlias = StoreWebhookData WebhookStoreResponse = TypedDict( - 'WebhookStoreResponse', + "WebhookStoreResponse", { - 'data': 'Required[WebhookSecretData]', - 'message': "Required[Literal['Webhook created successfully. Please save the secret as it will not be shown again.']]", + "data": "Required[WebhookSecretData]", + "message": "Required[Literal['Webhook created successfully. Please save the secret as it will not be shown again.']]", }, ) WebhookShowResponse: TypeAlias = WebhookData WebhookUpdateRequest: TypeAlias = UpdateWebhookData WebhookUpdateResponse = TypedDict( - 'WebhookUpdateResponse', + "WebhookUpdateResponse", { - 'data': 'Required[WebhookData]', - 'message': "Required[Literal['Webhook updated successfully.']]", + "data": "Required[WebhookData]", + "message": "Required[Literal['Webhook updated successfully.']]", }, ) WebhookDestroyResponse = TypedDict( - 'WebhookDestroyResponse', + "WebhookDestroyResponse", { - 'message': "Required[Literal['Webhook deleted successfully.']]", + "message": "Required[Literal['Webhook deleted successfully.']]", }, ) WebhookTestResponse = TypedDict( - 'WebhookTestResponse', + "WebhookTestResponse", { - 'message': "Required[Literal['Test webhook dispatched successfully. Check the deliveries endpoint for results.']]", - 'delivery_id': 'Required[str]', + "message": "Required[Literal['Test webhook dispatched successfully. Check the deliveries endpoint for results.']]", + "delivery_id": "Required[str]", }, ) WebhookRegenerateSecretResponse = TypedDict( - 'WebhookRegenerateSecretResponse', + "WebhookRegenerateSecretResponse", { - 'data': 'Required[WebhookSecretData]', - 'message': "Required[Literal['Webhook secret regenerated successfully. Please update your integration.']]", + "data": "Required[WebhookSecretData]", + "message": "Required[Literal['Webhook secret regenerated successfully. Please update your integration.']]", }, ) WebhookDeliveriesResponse = TypedDict( - 'WebhookDeliveriesResponse', - { - 'data': 'Required[list[WebhookDeliveryListData]]', - 'path': 'Required[str | None]', - 'per_page': 'Required[int]', - 'next_cursor': 'Required[str | None]', - 'next_page_url': 'Required[str | None]', - 'prev_cursor': 'Required[str | None]', - 'prev_page_url': 'Required[str | None]', + "WebhookDeliveriesResponse", + { + "data": "Required[list[WebhookDeliveryListData]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", }, ) From 78c52357180050c11fdc1845b9a6db71ba3dec2e Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld Date: Tue, 29 Sep 2026 22:27:35 +0200 Subject: [PATCH 3/3] fix(api): retain scheduled message API surface --- src/lettermint/endpoints/api.py | 52 ++++++++++++++ src/lettermint/endpoints/email.py | 101 ++++++++++++++++++++++++-- src/lettermint/types.py | 12 ++++ tests/test_api_surface.py | 114 +++++++++++++++++++++++++++++- tests/test_email.py | 64 ++++++++++++++++- 5 files changed, 333 insertions(+), 10 deletions(-) diff --git a/src/lettermint/endpoints/api.py b/src/lettermint/endpoints/api.py index d35a6b9..ba81f95 100644 --- a/src/lettermint/endpoints/api.py +++ b/src/lettermint/endpoints/api.py @@ -78,6 +78,32 @@ def retrieve(self, message_id: str, query: Query | None = None) -> lm_types.Mess ), ) + def reschedule( + self, message_id: str, data: lm_types.RescheduleMessageRequest + ) -> lm_types.RescheduleMessageResponse: + return cast( + lm_types.RescheduleMessageResponse, + self._client.patch( + self._path("/messages/{messageId}", messageId=message_id), data=data + ), + ) + + def cancel(self, message_id: str) -> lm_types.CancelScheduledMessageResponse: + return cast( + lm_types.CancelScheduledMessageResponse, + self._client.post( + self._path("/messages/{messageId}/cancel", messageId=message_id), data={} + ), + ) + + def process(self, message_id: str) -> lm_types.ProcessInboundMessageResponse: + return cast( + lm_types.ProcessInboundMessageResponse, + self._client.post( + self._path("/messages/{messageId}/process", messageId=message_id), data={} + ), + ) + def events(self, message_id: str, query: Query | None = None) -> lm_types.MessageEventsResponse: return cast( lm_types.MessageEventsResponse, @@ -394,6 +420,32 @@ async def retrieve( ), ) + async def reschedule( + self, message_id: str, data: lm_types.RescheduleMessageRequest + ) -> lm_types.RescheduleMessageResponse: + return cast( + lm_types.RescheduleMessageResponse, + await self._client.patch( + self._path("/messages/{messageId}", messageId=message_id), data=data + ), + ) + + async def cancel(self, message_id: str) -> lm_types.CancelScheduledMessageResponse: + return cast( + lm_types.CancelScheduledMessageResponse, + await self._client.post( + self._path("/messages/{messageId}/cancel", messageId=message_id), data={} + ), + ) + + async def process(self, message_id: str) -> lm_types.ProcessInboundMessageResponse: + return cast( + lm_types.ProcessInboundMessageResponse, + await self._client.post( + self._path("/messages/{messageId}/process", messageId=message_id), data={} + ), + ) + async def events( self, message_id: str, query: Query | None = None ) -> lm_types.MessageEventsResponse: diff --git a/src/lettermint/endpoints/email.py b/src/lettermint/endpoints/email.py index 31e4cae..8a35314 100644 --- a/src/lettermint/endpoints/email.py +++ b/src/lettermint/endpoints/email.py @@ -12,7 +12,14 @@ else: from typing_extensions import Self -from ..types import SandboxResult, SendBatchEmailResponse, SendEmailResponse +from ..message_tag import MessageTag, normalize_message_tags +from ..types import ( + SandboxResult, + SendBatchEmailResponse, + SendBatchMailRequest, + SendEmailResponse, + TlsPolicy, +) from .endpoint import AsyncEndpoint, Endpoint if TYPE_CHECKING: @@ -128,6 +135,11 @@ def subject(self, subject: str) -> Self: self._payload["subject"] = subject return self + def scheduled_at(self, scheduled_at: str) -> Self: + """Set the requested delivery time for the email.""" + self._payload["scheduled_at"] = scheduled_at + return self + def html(self, html: str | None) -> Self: """Set the HTML body of the email. @@ -207,6 +219,7 @@ def attach( filename: str, content: str, content_id: str | None = None, + content_type: str | None = None, ) -> Self: """Attach a file to the email. @@ -214,6 +227,7 @@ def attach( filename: The attachment filename. content: The base64-encoded file content. content_id: Optional Content-ID for inline attachments. + content_type: Optional MIME type for the attachment. Returns: The current instance for method chaining. @@ -233,10 +247,17 @@ def attach( } if content_id is not None: attachment["content_id"] = content_id + if content_type is not None: + attachment["content_type"] = content_type self._payload["attachments"].append(attachment) return self + def settings(self, settings: dict[str, bool | TlsPolicy]) -> Self: + """Set per-email settings that override the selected route.""" + self._payload["settings"] = settings + return self + def metadata(self, metadata: dict[str, str]) -> Self: """Set metadata for the email. @@ -264,9 +285,26 @@ def tag(self, tag: str) -> Self: Example: >>> client.email.tag("welcome-campaign") """ + if len(self._payload.get("tags", [])) >= 20: + raise ValueError("A legacy tag and no more than 19 message tags are permitted") self._payload["tag"] = tag return self + def tags(self, tags: list[MessageTag | dict[str, str]]) -> Self: + """Set reusable name-value tags for the email. + + Dictionaries remain supported for backward compatibility. + """ + maximum = 19 if self._payload.get("tag") is not None else 20 + if len(tags) > maximum: + raise ValueError(f"No more than {maximum} message tags are permitted") + normalized = [tag if isinstance(tag, MessageTag) else MessageTag(**tag) for tag in tags] + names = [tag.name for tag in normalized] + if len(names) != len(set(names)): + raise ValueError("Message tag names must be unique and case-sensitive") + self._payload["tags"] = [tag.to_dict() for tag in normalized] + return self + def sandbox_result(self, result: SandboxResult) -> Self: """Select the simulated result for a Sandbox project.""" self._payload["sandbox_result"] = result @@ -302,10 +340,21 @@ def send(self) -> SendEmailResponse: finally: self._reset() - def send_batch(self, payload: list[dict[str, Any]]) -> SendBatchEmailResponse: + def send_batch(self, payload: SendBatchMailRequest) -> SendBatchEmailResponse: """Send multiple emails in one batch.""" - response: SendBatchEmailResponse = self._client.post("/send/batch", data=payload) - return response + headers: dict[str, str] | None = None + if self._idempotency_key is not None: + headers = {"Idempotency-Key": self._idempotency_key} + + try: + response: SendBatchEmailResponse = self._client.post( + "/send/batch", + data=normalize_message_tags(payload), + headers=headers, + ) + return response + finally: + self._reset() def ping(self) -> str: """Ping the Sending API.""" @@ -404,6 +453,11 @@ def subject(self, subject: str) -> Self: self._payload["subject"] = subject return self + def scheduled_at(self, scheduled_at: str) -> Self: + """Set the requested delivery time for the email.""" + self._payload["scheduled_at"] = scheduled_at + return self + def html(self, html: str | None) -> Self: """Set the HTML body of the email. @@ -483,6 +537,7 @@ def attach( filename: str, content: str, content_id: str | None = None, + content_type: str | None = None, ) -> Self: """Attach a file to the email. @@ -490,6 +545,7 @@ def attach( filename: The attachment filename. content: The base64-encoded file content. content_id: Optional Content-ID for inline attachments. + content_type: Optional MIME type for the attachment. Returns: The current instance for method chaining. @@ -503,10 +559,17 @@ def attach( } if content_id is not None: attachment["content_id"] = content_id + if content_type is not None: + attachment["content_type"] = content_type self._payload["attachments"].append(attachment) return self + def settings(self, settings: dict[str, bool | TlsPolicy]) -> Self: + """Set per-email settings that override the selected route.""" + self._payload["settings"] = settings + return self + def metadata(self, metadata: dict[str, str]) -> Self: """Set metadata for the email. @@ -528,9 +591,26 @@ def tag(self, tag: str) -> Self: Returns: The current instance for method chaining. """ + if len(self._payload.get("tags", [])) >= 20: + raise ValueError("A legacy tag and no more than 19 message tags are permitted") self._payload["tag"] = tag return self + def tags(self, tags: list[MessageTag | dict[str, str]]) -> Self: + """Set reusable name-value tags for the email. + + Dictionaries remain supported for backward compatibility. + """ + maximum = 19 if self._payload.get("tag") is not None else 20 + if len(tags) > maximum: + raise ValueError(f"No more than {maximum} message tags are permitted") + normalized = [tag if isinstance(tag, MessageTag) else MessageTag(**tag) for tag in tags] + names = [tag.name for tag in normalized] + if len(names) != len(set(names)): + raise ValueError("Message tag names must be unique and case-sensitive") + self._payload["tags"] = [tag.to_dict() for tag in normalized] + return self + def sandbox_result(self, result: SandboxResult) -> Self: """Select the simulated result for a Sandbox project.""" self._payload["sandbox_result"] = result @@ -564,9 +644,18 @@ async def _send() -> SendEmailResponse: return _send() - async def send_batch(self, payload: list[dict[str, Any]]) -> SendBatchEmailResponse: + async def send_batch(self, payload: SendBatchMailRequest) -> SendBatchEmailResponse: """Send multiple emails in one batch asynchronously.""" - response: SendBatchEmailResponse = await self._client.post("/send/batch", data=payload) + headers: dict[str, str] | None = None + if self._idempotency_key is not None: + headers = {"Idempotency-Key": self._idempotency_key} + self._reset() + + response: SendBatchEmailResponse = await self._client.post( + "/send/batch", + data=normalize_message_tags(payload), + headers=headers, + ) return response async def ping(self) -> str: diff --git a/src/lettermint/types.py b/src/lettermint/types.py index f35f87d..22ecd6f 100644 --- a/src/lettermint/types.py +++ b/src/lettermint/types.py @@ -65,6 +65,18 @@ SendBatchMailRequest: TypeAlias = list[SendMailRequest] AttachmentDelivery: TypeAlias = Literal["inline", "url"] BuiltInTeamRole: TypeAlias = Literal["owner", "admin", "member"] +CursorPaginator = TypedDict( + "CursorPaginator", + { + "data": "Required[list[str]]", + "path": "Required[str | None]", + "per_page": "Required[int]", + "next_cursor": "Required[str | None]", + "next_page_url": "Required[str | None]", + "prev_cursor": "Required[str | None]", + "prev_page_url": "Required[str | None]", + }, +) DkimMode: TypeAlias = Literal["legacy_txt", "managed_cname"] DnsRecordPurpose: TypeAlias = Literal[ "return_path", "dmarc", "dkim_legacy", "dkim_primary", "dkim_secondary" diff --git a/tests/test_api_surface.py b/tests/test_api_surface.py index 05e8d29..22b5eea 100644 --- a/tests/test_api_surface.py +++ b/tests/test_api_surface.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from typing import get_args +from typing import get_args, get_type_hints import pytest import respx @@ -106,7 +106,7 @@ def test_send_batch_posts_list_payload(self) -> None: ) with Lettermint.email("sending-token") as email: - response = email.send_batch( + response = email.idempotency_key("batch-key").send_batch( [ { "from": "sender@example.com", @@ -118,6 +118,7 @@ def test_send_batch_posts_list_payload(self) -> None: assert response[0]["message_id"] == "msg_123" assert json.loads(route.calls.last.request.content)[0]["subject"] == "Hello" + assert route.calls.last.request.headers["Idempotency-Key"] == "batch-key" class TestFullApiEndpoints: @@ -168,6 +169,102 @@ def test_message_raw_body_endpoints(self) -> None: assert html_route.called assert text_route.called + @respx.mock + def test_scheduled_message_endpoints(self) -> None: + reschedule_route = respx.patch("https://api.lettermint.co/v1/messages/message%2Fid").mock( + return_value=Response( + 200, + json={ + "message_id": "message/id", + "status": "scheduled", + "scheduled_at": "2026-08-27T09:00:00Z", + }, + ) + ) + cancel_route = respx.post("https://api.lettermint.co/v1/messages/message%2Fid/cancel").mock( + return_value=Response( + 200, + json={"message_id": "message/id", "status": "canceled", "scheduled_at": None}, + ) + ) + process_route = respx.post( + "https://api.lettermint.co/v1/messages/message%2Fid/process" + ).mock( + return_value=Response( + 202, + json={ + "data": { + "message_id": "message/id", + "status": "queued", + "webhook_target_count": 1, + } + }, + ) + ) + + with Lettermint.api("api-token") as api: + assert ( + api.messages.reschedule("message/id", {"scheduled_at": "2026-08-27T09:00:00Z"})[ + "status" + ] + == "scheduled" + ) + assert api.messages.cancel("message/id")["status"] == "canceled" + assert api.messages.process("message/id")["data"]["status"] == "queued" + + assert json.loads(reschedule_route.calls.last.request.content) == { + "scheduled_at": "2026-08-27T09:00:00Z" + } + assert cancel_route.called + assert process_route.called + + @respx.mock + @pytest.mark.asyncio + async def test_async_scheduled_message_endpoints(self) -> None: + reschedule_route = respx.patch("https://api.lettermint.co/v1/messages/message%2Fid").mock( + return_value=Response( + 200, + json={ + "message_id": "message/id", + "status": "scheduled", + "scheduled_at": "2026-08-27T09:00:00Z", + }, + ) + ) + cancel_route = respx.post("https://api.lettermint.co/v1/messages/message%2Fid/cancel").mock( + return_value=Response( + 200, + json={"message_id": "message/id", "status": "canceled", "scheduled_at": None}, + ) + ) + process_route = respx.post( + "https://api.lettermint.co/v1/messages/message%2Fid/process" + ).mock( + return_value=Response( + 202, + json={ + "data": { + "message_id": "message/id", + "status": "queued", + "webhook_target_count": 1, + } + }, + ) + ) + + async with AsyncLettermint.api("api-token") as api: + assert ( + await api.messages.reschedule( + "message/id", {"scheduled_at": "2026-08-27T09:00:00Z"} + ) + )["status"] == "scheduled" + assert (await api.messages.cancel("message/id"))["status"] == "canceled" + assert (await api.messages.process("message/id"))["data"]["status"] == "queued" + + assert reschedule_route.called + assert cancel_route.called + assert process_route.called + @respx.mock def test_team_role_and_member_assignment_endpoints(self) -> None: roles_route = respx.get("https://api.lettermint.co/v1/team/roles").mock( @@ -209,6 +306,9 @@ def test_documented_operations_are_exposed(self) -> None: (Lettermint.api("token").domains, "update_projects"), (Lettermint.api("token").messages, "list"), (Lettermint.api("token").messages, "retrieve"), + (Lettermint.api("token").messages, "reschedule"), + (Lettermint.api("token").messages, "cancel"), + (Lettermint.api("token").messages, "process"), (Lettermint.api("token").messages, "events"), (Lettermint.api("token").messages, "source"), (Lettermint.api("token").messages, "html"), @@ -249,6 +349,7 @@ def test_documented_operations_are_exposed(self) -> None: missing = [method for endpoint, method in operations if not hasattr(endpoint, method)] + assert len(operations) == 53 assert missing == [] def test_generated_types_match_current_team_schema(self) -> None: @@ -272,6 +373,15 @@ def test_generated_types_match_current_team_schema(self) -> None: assert "included_volume" in lm_types.TeamData.__annotations__ assert "assignable" in lm_types.TeamRoleData.__annotations__ assert "role_id" in lm_types.UpdateTeamMemberAssignmentData.__annotations__ + assert hasattr(lm_types, "RescheduleMessageRequest") + assert hasattr(lm_types, "RescheduleMessageResponse") + assert hasattr(lm_types, "CancelScheduledMessageResponse") + assert hasattr(lm_types, "ProcessInboundMessageResponse") + assert hasattr(lm_types, "CursorPaginator") + assert ( + get_type_hints(type(Lettermint.api("token").messages).cancel)["return"] + is lm_types.CancelScheduledMessageResponse + ) def test_generated_types_include_sandbox_contracts(self) -> None: assert set(get_args(lm_types.DeliveryMode)) == {"live", "sandbox"} diff --git a/tests/test_email.py b/tests/test_email.py index 8b86a46..111b044 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -121,7 +121,7 @@ def test_send_with_attachments(self, api_token: str) -> None: client.email.from_("sender@example.com").to("recipient@example.com").subject( "Test" ).attach("document.pdf", "base64content").attach( - "logo.png", "base64image", "logo@example.com" + "logo.png", "base64image", "logo@example.com", "image/png" ).send() import json @@ -133,6 +133,7 @@ def test_send_with_attachments(self, api_token: str) -> None: "filename": "logo.png", "content": "base64image", "content_id": "logo@example.com", + "content_type": "image/png", } @respx.mock @@ -177,13 +178,52 @@ def test_send_with_metadata_and_tag(self, api_token: str) -> None: with Lettermint(api_token=api_token) as client: client.email.from_("sender@example.com").to("recipient@example.com").subject( "Test" - ).metadata({"campaign_id": "123"}).tag("welcome").send() + ).scheduled_at("2026-10-01T09:00:00Z").metadata({"campaign_id": "123"}).tag( + "welcome" + ).tags([{"name": "campaign", "value": "welcome-v2"}]).settings( + {"track_opens": False, "track_clicks": True, "tls": "enforced"} + ).send() import json body = json.loads(route.calls.last.request.content) assert body["metadata"] == {"campaign_id": "123"} assert body["tag"] == "welcome" + assert body["scheduled_at"] == "2026-10-01T09:00:00Z" + assert body["tags"] == [{"name": "campaign", "value": "welcome-v2"}] + assert body["settings"] == { + "track_opens": False, + "track_clicks": True, + "tls": "enforced", + } + + def test_typed_message_tags_and_legacy_dictionary_support(self, api_token: str) -> None: + from lettermint import MessageTag + + with Lettermint(api_token=api_token) as client: + endpoint = client.email.tags( + [ + MessageTag(name="campaign", value="welcome"), + {"name": "customer", "value": "new"}, + ] + ) + assert endpoint._payload["tags"] == [ + {"name": "campaign", "value": "welcome"}, + {"name": "customer", "value": "new"}, + ] + + def test_rejects_invalid_message_tags(self, api_token: str) -> None: + with Lettermint(api_token=api_token) as client: + with pytest.raises(ValueError): + client.email.tags( + [{"name": "duplicate", "value": "one"}, {"name": "duplicate", "value": "two"}] + ) + with pytest.raises(ValueError): + client.email.tags([{"name": "__LETTERMINT_internal", "value": "one"}]) + with pytest.raises(ValueError): + client.email.tag("legacy").tags( + [{"name": f"tag_{index}", "value": "one"} for index in range(20)] + ) @respx.mock def test_send_with_route(self, api_token: str) -> None: @@ -381,6 +421,26 @@ async def test_send_with_all_options_async(self, api_token: str) -> None: request = route.calls.last.request assert request.headers["Idempotency-Key"] == "unique-key" + @respx.mock + @pytest.mark.asyncio + async def test_send_batch_with_idempotency_key_async(self, api_token: str) -> None: + """Test an asynchronous batch idempotency key.""" + route = respx.post("https://api.lettermint.co/v1/send/batch").mock( + return_value=Response(200, json=[{"message_id": "msg_123", "status": "pending"}]) + ) + payload = [ + { + "from": "sender@example.com", + "to": ["recipient@example.com"], + "subject": "Test", + } + ] + + async with AsyncLettermint(api_token=api_token) as client: + await client.email.idempotency_key("batch-key").send_batch(payload) + + assert route.calls.last.request.headers["Idempotency-Key"] == "batch-key" + @respx.mock @pytest.mark.asyncio async def test_deferred_async_sends_use_payload_snapshots(self, api_token: str) -> None: