Skip to content

Commit b46e064

Browse files
committed
feat: batch send async module
1 parent e6be21a commit b46e064

3 files changed

Lines changed: 189 additions & 0 deletions

File tree

examples/batch_email_send_async.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import asyncio
2+
import os
3+
from typing import List
4+
5+
import resend
6+
import resend.exceptions
7+
8+
if not os.environ["RESEND_API_KEY"]:
9+
raise EnvironmentError("RESEND_API_KEY is missing")
10+
11+
# Set up async HTTP client
12+
resend.default_http_client = resend.HTTPXClient()
13+
14+
15+
async def main() -> None:
16+
params: List[resend.Emails.SendParams] = [
17+
{
18+
"from": "onboarding@resend.dev",
19+
"to": ["delivered@resend.dev"],
20+
"subject": "hey",
21+
"html": "<strong>hello, world!</strong>",
22+
},
23+
{
24+
"from": "onboarding@resend.dev",
25+
"to": ["delivered@resend.dev"],
26+
"subject": "hello",
27+
"html": "<strong>hello, world!</strong>",
28+
},
29+
]
30+
31+
try:
32+
# Send batch emails
33+
print("sending without idempotency_key")
34+
emails: resend.Batch.SendResponse = await resend.Batch.send_async(params)
35+
for email in emails["data"]:
36+
print(f"Email id: {email['id']}")
37+
except resend.exceptions.ResendError as err:
38+
print("Failed to send batch emails")
39+
print(f"Error: {err}")
40+
exit(1)
41+
42+
try:
43+
# Send batch emails with idempotency_key
44+
print("sending with idempotency_key")
45+
46+
options: resend.Batch.SendOptions = {
47+
"idempotency_key": "af477dc78aa9fa91fff3b8c0d4a2e1a5",
48+
}
49+
50+
e: resend.Batch.SendResponse = await resend.Batch.send_async(
51+
params, options=options
52+
)
53+
for email in e["data"]:
54+
print(f"Email id: {email['id']}")
55+
except resend.exceptions.ResendError as err:
56+
print("Failed to send batch emails")
57+
print(f"Error: {err}")
58+
exit(1)
59+
60+
61+
if __name__ == "__main__":
62+
asyncio.run(main())

resend/emails/_batch.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
from ._email import Email
88
from ._emails import Emails
99

10+
# Async imports (optional - only available with pip install resend[async])
11+
try:
12+
from resend.async_request import AsyncRequest
13+
except ImportError:
14+
pass
15+
1016

1117
class _SendOptions(TypedDict):
1218
idempotency_key: NotRequired[str]
@@ -68,3 +74,28 @@ def send(
6874
options=cast(Dict[Any, Any], options),
6975
).perform_with_content()
7076
return resp
77+
78+
@classmethod
79+
async def send_async(
80+
cls, params: List[Emails.SendParams], options: Optional[SendOptions] = None
81+
) -> SendResponse:
82+
"""
83+
Trigger up to 100 batch emails at once (async).
84+
see more: https://resend.com/docs/api-reference/emails/send-batch-emails
85+
86+
Args:
87+
params (List[Emails.SendParams]): The list of emails to send
88+
options (Optional[SendOptions]): Batch options, ie: idempotency_key
89+
90+
Returns:
91+
SendResponse: A list of email objects
92+
"""
93+
path = "/emails/batch"
94+
95+
resp = await AsyncRequest[_SendResponse](
96+
path=path,
97+
params=cast(List[Dict[Any, Any]], params),
98+
verb="post",
99+
options=cast(Dict[Any, Any], options),
100+
).perform_with_content()
101+
return resp

tests/batch_emails_async_test.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from typing import List
2+
3+
import resend
4+
from resend.exceptions import NoContentError
5+
from tests.conftest import ResendBaseTest
6+
7+
# flake8: noqa
8+
9+
10+
class TestResendBatchSendAsync(ResendBaseTest):
11+
async def test_batch_email_send_async(self) -> None:
12+
self.set_mock_json(
13+
{
14+
"data": [
15+
{"id": "ae2014de-c168-4c61-8267-70d2662a1ce1"},
16+
{"id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"},
17+
]
18+
}
19+
)
20+
21+
params: List[resend.Emails.SendParams] = [
22+
{
23+
"from": "from@resend.dev",
24+
"to": ["to@resend.dev"],
25+
"subject": "hey",
26+
"html": "<strong>hello, world!</strong>",
27+
},
28+
{
29+
"from": "from@resend.dev",
30+
"to": ["to@resend.dev"],
31+
"subject": "hello",
32+
"html": "<strong>hello, world!</strong>",
33+
},
34+
]
35+
36+
emails: resend.Batch.SendResponse = await resend.Batch.send_async(params)
37+
assert len(emails["data"]) == 2
38+
assert emails["data"][0]["id"] == "ae2014de-c168-4c61-8267-70d2662a1ce1"
39+
assert emails["data"][1]["id"] == "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"
40+
41+
async def test_batch_email_send_async_with_options(self) -> None:
42+
self.set_mock_json(
43+
{
44+
"data": [
45+
{"id": "ae2014de-c168-4c61-8267-70d2662a1ce1"},
46+
{"id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"},
47+
]
48+
}
49+
)
50+
51+
params: List[resend.Emails.SendParams] = [
52+
{
53+
"from": "from@resend.dev",
54+
"to": ["to@resend.dev"],
55+
"subject": "hey",
56+
"html": "<strong>hello, world!</strong>",
57+
},
58+
{
59+
"from": "from@resend.dev",
60+
"to": ["to@resend.dev"],
61+
"subject": "hello",
62+
"html": "<strong>hello, world!</strong>",
63+
},
64+
]
65+
66+
options: resend.Batch.SendOptions = {
67+
"idempotency_key": "af477dc78aa9fa91fff3b8c0d4a2e1a5",
68+
}
69+
70+
emails: resend.Batch.SendResponse = await resend.Batch.send_async(
71+
params, options=options
72+
)
73+
assert len(emails["data"]) == 2
74+
assert emails["data"][0]["id"] == "ae2014de-c168-4c61-8267-70d2662a1ce1"
75+
assert emails["data"][1]["id"] == "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"
76+
77+
async def test_should_send_batch_email_async_raise_exception_when_no_content(
78+
self,
79+
) -> None:
80+
self.set_mock_json(None)
81+
params: List[resend.Emails.SendParams] = [
82+
{
83+
"from": "from@resend.dev",
84+
"to": ["to@resend.dev"],
85+
"subject": "hey",
86+
"html": "<strong>hello, world!</strong>",
87+
},
88+
{
89+
"from": "from@resend.dev",
90+
"to": ["to@resend.dev"],
91+
"subject": "hello",
92+
"html": "<strong>hello, world!</strong>",
93+
},
94+
]
95+
with self.assertRaises(NoContentError):
96+
_ = await resend.Batch.send_async(params)

0 commit comments

Comments
 (0)