From eff80a22683b691b1df461e1c9650cfa88908f11 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 20:54:34 +0000 Subject: [PATCH 1/2] feat: add Batch.SendParams with scheduled_at, tags, and attachments Introduce a dedicated Batch.SendParams type for batch email sends so scheduled_at, tags, and attachments are explicitly documented and typed. Update batch send signatures, tests, and examples to use the new type. Co-authored-by: cpenned --- examples/async/batch_email_send_async.py | 2 +- examples/batch_email_send.py | 4 +- resend/emails/_batch.py | 30 ++++++++++--- resend/emails/_emails.py | 2 + tests/batch_emails_async_test.py | 6 +-- tests/batch_emails_test.py | 54 +++++++++++++++++++++--- 6 files changed, 82 insertions(+), 16 deletions(-) diff --git a/examples/async/batch_email_send_async.py b/examples/async/batch_email_send_async.py index 9433a88..ef1a840 100644 --- a/examples/async/batch_email_send_async.py +++ b/examples/async/batch_email_send_async.py @@ -12,7 +12,7 @@ async def main() -> None: - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "onboarding@resend.dev", "to": ["delivered@resend.dev"], diff --git a/examples/batch_email_send.py b/examples/batch_email_send.py index f667667..bb237dc 100644 --- a/examples/batch_email_send.py +++ b/examples/batch_email_send.py @@ -8,7 +8,7 @@ raise EnvironmentError("RESEND_API_KEY is missing") -params: List[resend.Emails.SendParams] = [ +params: List[resend.Batch.SendParams] = [ { "from": "onboarding@resend.dev", "to": ["delivered@resend.dev"], @@ -55,7 +55,7 @@ print("sending with permissive validation mode") # Example with some invalid emails to demonstrate error handling - mixed_params: List[resend.Emails.SendParams] = [ + mixed_params: List[resend.Batch.SendParams] = [ { "from": "onboarding@resend.dev", "to": ["delivered@resend.dev"], diff --git a/resend/emails/_batch.py b/resend/emails/_batch.py index 9aebc0c..5623299 100644 --- a/resend/emails/_batch.py +++ b/resend/emails/_batch.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, Union, cast from typing_extensions import Literal, NotRequired, TypedDict @@ -42,6 +42,26 @@ class BatchValidationError(TypedDict): class Batch: + class SendParams(Emails.SendParams): + """SendParams is the class that wraps the parameters for each email in a batch send. + + Attributes: + from (NotRequired[str]): The email address to send the email from. + to (Union[str, List[str]]): List of email addresses to send the email to. + subject (NotRequired[str]): The subject of the email. + bcc (NotRequired[Union[List[str], str]]): Bcc + cc (NotRequired[Union[List[str], str]]): Cc + reply_to (NotRequired[Union[List[str], str]]): Reply to + html (NotRequired[str]): The HTML content of the email. + text (NotRequired[str]): The text content of the email. + headers (NotRequired[Dict[str, str]]): Custom headers to be added to the email. + attachments (NotRequired[List[Union[Attachment, RemoteAttachment]]]): List of attachments to be added to the email. + tags (NotRequired[List[Tag]]): List of tags to be added to the email. + scheduled_at (NotRequired[str]): Schedule email to be sent later. + The date should be in ISO 8601 format (e.g: 2024-08-05T11:52:01.858Z). + template (NotRequired[EmailTemplate]): Template configuration for sending emails using predefined templates. + """ + class SendOptions(TypedDict): """ SendOptions is the class that wraps the options for the batch send method. @@ -79,14 +99,14 @@ class SendResponse(BaseResponse): @classmethod def send( - cls, params: List[Emails.SendParams], options: Optional[SendOptions] = None + cls, params: List[SendParams], options: Optional[SendOptions] = None ) -> SendResponse: """ Trigger up to 100 batch emails at once. see more: https://resend.com/docs/api-reference/emails/send-batch-emails Args: - params (List[Emails.SendParams]): The list of emails to send + params (List[SendParams]): The list of emails to send options (Optional[SendOptions]): Batch options, including batch_validation mode Returns: @@ -104,14 +124,14 @@ def send( @classmethod async def send_async( - cls, params: List[Emails.SendParams], options: Optional[SendOptions] = None + cls, params: List[SendParams], options: Optional[SendOptions] = None ) -> SendResponse: """ Trigger up to 100 batch emails at once (async). see more: https://resend.com/docs/api-reference/emails/send-batch-emails Args: - params (List[Emails.SendParams]): The list of emails to send + params (List[SendParams]): The list of emails to send options (Optional[SendOptions]): Batch options, ie: idempotency_key Returns: diff --git a/resend/emails/_emails.py b/resend/emails/_emails.py index bcf301d..b9d1c1b 100644 --- a/resend/emails/_emails.py +++ b/resend/emails/_emails.py @@ -181,6 +181,8 @@ class SendParams(_SendParamsDefault): headers (NotRequired[Dict[str, str]]): Custom headers to be added to the email. attachments (NotRequired[List[Union[Attachment, RemoteAttachment]]]): List of attachments to be added to the email. tags (NotRequired[List[Tag]]): List of tags to be added to the email. + scheduled_at (NotRequired[str]): Schedule email to be sent later. + The date should be in ISO 8601 format (e.g: 2024-08-05T11:52:01.858Z). template (NotRequired[EmailTemplate]): Template configuration for sending emails using predefined templates. """ diff --git a/tests/batch_emails_async_test.py b/tests/batch_emails_async_test.py index 3e4d6e4..7ce1a00 100644 --- a/tests/batch_emails_async_test.py +++ b/tests/batch_emails_async_test.py @@ -22,7 +22,7 @@ async def test_batch_email_send_async(self) -> None: } ) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -52,7 +52,7 @@ async def test_batch_email_send_async_with_options(self) -> None: } ) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -82,7 +82,7 @@ async def test_should_send_batch_email_async_raise_exception_when_no_content( self, ) -> None: self.set_mock_json(None) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], diff --git a/tests/batch_emails_test.py b/tests/batch_emails_test.py index 6e1c123..e7c2c98 100644 --- a/tests/batch_emails_test.py +++ b/tests/batch_emails_test.py @@ -1,6 +1,8 @@ from typing import List +from unittest.mock import patch import resend +from resend import request from resend.exceptions import NoContentError from tests.conftest import ResendBaseTest @@ -18,7 +20,7 @@ def test_batch_email_send(self) -> None: } ) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -48,7 +50,7 @@ def test_batch_email_send_with_options(self) -> None: } ) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -74,7 +76,7 @@ def test_batch_email_send_with_options(self) -> None: def test_should_send_batch_email_raise_exception_when_no_content(self) -> None: self.set_mock_json(None) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -101,7 +103,7 @@ def test_batch_email_send_with_strict_validation_mode(self) -> None: } ) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -141,7 +143,7 @@ def test_batch_email_send_with_permissive_validation_mode(self) -> None: } ) - params: List[resend.Emails.SendParams] = [ + params: List[resend.Batch.SendParams] = [ { "from": "from@resend.dev", "to": ["to@resend.dev"], @@ -167,3 +169,45 @@ def test_batch_email_send_with_permissive_validation_mode(self) -> None: assert len(emails["errors"]) == 1 assert emails["errors"][0]["index"] == 1 assert emails["errors"][0]["message"] == "The `to` field is missing." + + def test_batch_email_send_with_tags_attachments_and_scheduled_at(self) -> None: + self.set_mock_json( + { + "data": [{"id": "ae2014de-c168-4c61-8267-70d2662a1ce1"}] + } + ) + + attachment: resend.Attachment = { + "filename": "invoice.pdf", + "content": [1, 2, 3], + "content_type": "application/pdf", + } + + params: List[resend.Batch.SendParams] = [ + { + "from": "from@resend.dev", + "to": ["to@resend.dev"], + "subject": "hey", + "html": "hello, world!", + "tags": [{"name": "category", "value": "notification"}], + "scheduled_at": "2024-09-05T11:52:01.858Z", + "attachments": [attachment], + } + ] + + captured_params: list = [] + original_init = request.Request.__init__ + + def capturing_init(instance, *args, **kwargs): + captured_params.append(kwargs.get("params")) + return original_init(instance, *args, **kwargs) + + with patch.object(request.Request, "__init__", capturing_init): + emails: resend.Batch.SendResponse = resend.Batch.send(params) + + assert emails["data"][0]["id"] == "ae2014de-c168-4c61-8267-70d2662a1ce1" + + sent_params = captured_params[0] + assert sent_params[0]["tags"] == [{"name": "category", "value": "notification"}] + assert sent_params[0]["scheduled_at"] == "2024-09-05T11:52:01.858Z" + assert sent_params[0]["attachments"][0]["filename"] == "invoice.pdf" From f97748d1dc3a50131a798d1a355db80e35618a65 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 20:56:27 +0000 Subject: [PATCH 2/2] fix: resolve lint and mypy failures in batch send changes Co-authored-by: cpenned --- resend/emails/_batch.py | 2 +- tests/batch_emails_test.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resend/emails/_batch.py b/resend/emails/_batch.py index 5623299..554067d 100644 --- a/resend/emails/_batch.py +++ b/resend/emails/_batch.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, cast from typing_extensions import Literal, NotRequired, TypedDict diff --git a/tests/batch_emails_test.py b/tests/batch_emails_test.py index e7c2c98..f29e23a 100644 --- a/tests/batch_emails_test.py +++ b/tests/batch_emails_test.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Any, List from unittest.mock import patch import resend @@ -195,12 +195,12 @@ def test_batch_email_send_with_tags_attachments_and_scheduled_at(self) -> None: } ] - captured_params: list = [] + captured_params: List[Any] = [] original_init = request.Request.__init__ - def capturing_init(instance, *args, **kwargs): + def capturing_init(instance: Any, *args: Any, **kwargs: Any) -> None: captured_params.append(kwargs.get("params")) - return original_init(instance, *args, **kwargs) + original_init(instance, *args, **kwargs) with patch.object(request.Request, "__init__", capturing_init): emails: resend.Batch.SendResponse = resend.Batch.send(params)