diff --git a/examples/scheduled_email.py b/examples/scheduled_email.py new file mode 100644 index 0000000..5cd316c --- /dev/null +++ b/examples/scheduled_email.py @@ -0,0 +1,41 @@ +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + +params: resend.Emails.SendParams = { + "from": "onboarding@resend.dev", + "to": ["delivered@resend.dev"], + "subject": "hi", + "html": "hello, scheduled email!", + "scheduled_at": "2024-09-05T11:52:01.858Z", +} + +# Throws a resend.exceptions.ValidationError +# when scheduled_at is not in ISO 8601 format. +# +# Here is an example on how to create a date in the ISO 8601 format: +# from datetime import datetime +# datetime.now().isoformat() +email: resend.Email = resend.Emails.send(params) + +print(f"Email scheduled: {email['id']}") + +update_params: resend.Emails.UpdateParams = { + "id": email["id"], + "scheduled_at": "2024-09-07T11:52:01.858Z", +} + +updated_email: resend.Emails.UpdateEmailResponse = resend.Emails.update( + params=update_params +) + +print(f"Email updated: {updated_email['id']}") + +cancel_resp: resend.Emails.CancelScheduledEmailResponse = resend.Emails.cancel( + email_id=email["id"] +) + +print(f"Email canceled: {cancel_resp['id']}") diff --git a/resend/emails/_emails.py b/resend/emails/_emails.py index 9e49fb1..02eff07 100644 --- a/resend/emails/_emails.py +++ b/resend/emails/_emails.py @@ -7,6 +7,41 @@ from resend.emails._email import Email from resend.emails._tag import Tag + +class _UpdateParams(TypedDict): + id: str + """ + The ID of the email to update. + """ + 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). + """ + + +class _UpdateEmailResponse(TypedDict): + object: str + """ + The object type: email + """ + id: str + """ + The ID of the scheduled email that was canceled. + """ + + +class _CancelScheduledEmailResponse(TypedDict): + object: str + """ + The object type: email + """ + id: str + """ + The ID of the scheduled email that was canceled. + """ + + # SendParamsFrom is declared with functional TypedDict syntax here because # "from" is a reserved keyword in Python, and this is the best way to # support type-checking for it. @@ -59,9 +94,43 @@ class _SendParamsDefault(_SendParamsFrom): """ 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). + """ class Emails: + + class CancelScheduledEmailResponse(_CancelScheduledEmailResponse): + """ + CancelScheduledEmailResponse is the type that wraps the response of the email that was canceled + + Attributes: + object (str): The object type + id (str): The ID of the scheduled email that was canceled + """ + + class UpdateEmailResponse(_UpdateEmailResponse): + """ + UpdateEmailResponse is the type for the updated email response. + + Attributes: + object (str): The object type + id (str): The ID of the updated email. + """ + + class UpdateParams(_UpdateParams): + """ + UpdateParams is the class that wraps the parameters for the update method. + + Attributes: + id (str): The ID of the email to update. + 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). + """ + class SendParams(_SendParamsDefault): """SendParams is the class that wraps the parameters for the send method. @@ -118,3 +187,43 @@ def get(cls, email_id: str) -> Email: verb="get", ).perform_with_content() return resp + + @classmethod + def cancel(cls, email_id: str) -> CancelScheduledEmailResponse: + """ + Cancel a scheduled email. + see more: https://resend.com/docs/api-reference/emails/cancel-email + + Args: + email_id (str): The ID of the scheduled email to cancel + + Returns: + CancelScheduledEmailResponse: The response object that contains the ID of the scheduled email that was canceled + """ + path = f"/emails/{email_id}/cancel" + resp = request.Request[_CancelScheduledEmailResponse]( + path=path, + params={}, + verb="post", + ).perform_with_content() + return resp + + @classmethod + def update(cls, params: UpdateParams) -> UpdateEmailResponse: + """ + Update an email. + see more: https://resend.com/docs/api-reference/emails/update-email + + Args: + params (UpdateParams): The email parameters to update + + Returns: + Email: The email object that was updated + """ + path = f"/emails/{params['id']}" + resp = request.Request[_UpdateEmailResponse]( + path=path, + params=cast(Dict[Any, Any], params), + verb="patch", + ).perform_with_content() + return resp diff --git a/resend/version.py b/resend/version.py index 0a10da4..72b5ff9 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.3.0" +__version__ = "2.4.0" def get_version() -> str: diff --git a/tests/emails_test.py b/tests/emails_test.py index 31dc952..c7d8ada 100644 --- a/tests/emails_test.py +++ b/tests/emails_test.py @@ -83,3 +83,29 @@ def test_email_response_html(self) -> None: _ = resend.Emails.send(params) except ResendError as e: assert e.message == "Failed to parse Resend API response. Please try again." + + def test_update_email(self) -> None: + self.set_mock_json( + { + "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794", + } + ) + update_params: resend.Emails.UpdateParams = { + "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794", + "scheduled_at": "2024-09-07T11:52:01.858Z", + } + updated_email: resend.Emails.UpdateEmailResponse = resend.Emails.update( + params=update_params + ) + assert updated_email["id"] == "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" + + def test_cancel_scheduled_email(self) -> None: + self.set_mock_json( + { + "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794", + } + ) + email: resend.Emails.CancelScheduledEmailResponse = resend.Emails.cancel( + email_id="49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" + ) + assert email["id"] == "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"