diff --git a/examples/async/automations_async.py b/examples/async/automations_async.py new file mode 100644 index 0000000..1d7d78e --- /dev/null +++ b/examples/async/automations_async.py @@ -0,0 +1,150 @@ +import asyncio +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + + +async def main() -> None: + # Create and publish a template to use in the automation + print("--- Create and publish template ---") + tpl: resend.Templates.CreateResponse = await resend.Templates.create_async( + { + "name": "welcome-email", + "subject": "Welcome!", + "html": "Welcome to our service!", + } + ) + await resend.Templates.publish_async(tpl["id"]) + print(f"Template: {tpl['id']}") + + # --- Create a simple automation (trigger → send_email) --- + print("\n--- Create automation ---") + simple: resend.Automations.CreateResponse = await resend.Automations.create_async( + { + "name": "Welcome Flow", + "status": "disabled", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.created"}, + }, + { + "key": "send_1", + "type": "send_email", + "config": {"template": {"id": tpl["id"]}}, + }, + ], + "connections": [ + {"from": "trigger_1", "to": "send_1"}, + ], + } + ) + automation_id = simple["id"] + print(f"Created automation: {automation_id}") + + # --- Get automation --- + print("\n--- Get automation ---") + automation: resend.Automation = await resend.Automations.get_async(automation_id) + print(f"Name: {automation['name']}, status: {automation['status']}") + for step in automation["steps"]: + print(f" Step key={step['key']} type={step['type']}") + + # --- Update automation --- + print("\n--- Update automation ---") + updated: resend.Automations.UpdateResponse = await resend.Automations.update_async( + { + "automation_id": automation_id, + "status": "enabled", + } + ) + print(f"Updated: {updated['id']}") + + # --- List automations --- + print("\n--- List automations ---") + list_resp: resend.Automations.ListResponse = await resend.Automations.list_async() + print(f"Total: {len(list_resp['data'])}, has_more: {list_resp['has_more']}") + + # --- Stop automation --- + print("\n--- Stop automation ---") + stopped: resend.Automations.StopResponse = await resend.Automations.stop_async( + automation_id + ) + print(f"Stopped: {stopped['id']}, status: {stopped['status']}") + + # --- List runs --- + print("\n--- List runs ---") + runs: resend.Automations.Runs.ListResponse = ( + await resend.Automations.Runs.list_async(automation_id) + ) + print(f"Total runs: {len(runs['data'])}") + if runs["data"]: + run_id = runs["data"][0]["id"] + run: resend.AutomationRun = await resend.Automations.Runs.get_async( + automation_id, run_id + ) + print(f"Run status: {run['status']}") + + # --- Multi-step automation: delay + wait_for_event --- + print("\n--- Create multi-step automation (delay + wait_for_event) ---") + multi: resend.Automations.CreateResponse = await resend.Automations.create_async( + { + "name": "Onboarding Flow", + "status": "disabled", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.created"}, + }, + { + "key": "delay_1", + "type": "delay", + # duration is a human-readable string; "seconds" (int) is also accepted + "config": {"duration": "30 minutes"}, + }, + { + "key": "wait_1", + "type": "wait_for_event", + # timeout is a human-readable string; timeout_seconds is NOT supported + "config": {"event_name": "user.verified", "timeout": "1 hour"}, + }, + { + "key": "send_1", + "type": "send_email", + "config": {"template": {"id": tpl["id"]}}, + }, + ], + "connections": [ + {"from": "trigger_1", "to": "delay_1"}, + {"from": "delay_1", "to": "wait_1"}, + {"from": "wait_1", "to": "send_1", "type": "event_received"}, + {"from": "wait_1", "to": "send_1", "type": "timeout"}, + ], + } + ) + multi_id = multi["id"] + print(f"Created: {multi_id}") + + retrieved: resend.Automation = await resend.Automations.get_async(multi_id) + for step in retrieved["steps"]: + print(f" Step key={step['key']} type={step['type']} config={step['config']}") + + # --- Cleanup --- + print("\n--- Cleanup ---") + del1: resend.Automations.DeleteResponse = await resend.Automations.remove_async( + automation_id + ) + print(f"Deleted automation {del1['id']}: {del1['deleted']}") + del2: resend.Automations.DeleteResponse = await resend.Automations.remove_async( + multi_id + ) + print(f"Deleted automation {del2['id']}: {del2['deleted']}") + await resend.Templates.remove_async(tpl["id"]) + print(f"Deleted template {tpl['id']}") + + +asyncio.run(main()) diff --git a/examples/async/events_async.py b/examples/async/events_async.py new file mode 100644 index 0000000..f43357b --- /dev/null +++ b/examples/async/events_async.py @@ -0,0 +1,95 @@ +import asyncio +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + + +async def main() -> None: + # Create a contact to use with event sends + print("--- Create contact ---") + contact: resend.Contacts.CreateContactResponse = await resend.Contacts.create_async( + { + "email": "test-events-async@example.com", + "first_name": "Test", + "last_name": "User", + } + ) + contact_id = contact["id"] + print(f"Contact: {contact_id}") + + # --- Create event --- + print("\n--- Create event ---") + created: resend.Events.CreateResponse = await resend.Events.create_async( + { + "name": "user.signed_up", + "schema": { + "plan": "string", + "trial_days": "number", + "is_enterprise": "boolean", + "upgraded_at": "date", + }, + } + ) + event_id = created["id"] + print(f"Created event: {event_id}") + + # --- Get event by ID --- + print("\n--- Get event by ID ---") + event: resend.Event = await resend.Events.get_async(event_id) + print(f"Name: {event['name']}, schema: {event['schema']}") + + # --- Get event by name --- + print("\n--- Get event by name ---") + event_by_name: resend.Event = await resend.Events.get_async("user.signed_up") + print(f"Found by name: {event_by_name['name']}") + + # --- Update event schema --- + print("\n--- Update event schema ---") + updated: resend.Events.UpdateResponse = await resend.Events.update_async( + { + "identifier": "user.signed_up", + "schema": {"plan": "string", "source": "string"}, + } + ) + print(f"Updated event: {updated['id']}") + + # --- Send event with contact_id --- + print("\n--- Send event with contact_id ---") + sent: resend.Events.SendResponse = await resend.Events.send_async( + { + "event": "user.signed_up", + "contact_id": contact_id, + "payload": {"plan": "pro"}, + } + ) + print(f"Sent event: {sent['event']}") + + # --- Send event with email --- + print("\n--- Send event with email ---") + sent_email: resend.Events.SendResponse = await resend.Events.send_async( + { + "event": "user.signed_up", + "email": "test-events-async@example.com", + } + ) + print(f"Sent event: {sent_email['event']}") + + # --- List events --- + print("\n--- List events ---") + list_resp: resend.Events.ListResponse = await resend.Events.list_async() + print(f"Total: {len(list_resp['data'])}, has_more: {list_resp['has_more']}") + + # --- Cleanup --- + print("\n--- Delete event ---") + deleted: resend.Events.DeleteResponse = await resend.Events.remove_async(event_id) + print(f"Deleted: {deleted['deleted']}") + + print("\n--- Delete contact ---") + await resend.Contacts.remove_async(id=contact_id) + print(f"Deleted contact: {contact_id}") + + +asyncio.run(main()) diff --git a/examples/async/receiving_email_async.py b/examples/async/receiving_email_async.py index 8f7ad10..b8dd24f 100644 --- a/examples/async/receiving_email_async.py +++ b/examples/async/receiving_email_async.py @@ -79,7 +79,9 @@ async def main() -> None: print("No attachments") print("\n--- Listing All Received Emails ---") - all_emails: EmailsReceiving.ListResponse = await resend.Emails.Receiving.list_async() + all_emails: EmailsReceiving.ListResponse = ( + await resend.Emails.Receiving.list_async() + ) print(f"Total emails in this batch: {len(all_emails['data'])}") print(f"Has more emails: {all_emails['has_more']}") @@ -138,7 +140,9 @@ async def main() -> None: first_attachment = received_email["attachments"][0] attachment_id = first_attachment["id"] - print(f"\n--- Retrieving Attachment Details: {first_attachment['filename']} ---") + print( + f"\n--- Retrieving Attachment Details: {first_attachment['filename']} ---" + ) attachment_details: resend.EmailAttachmentDetails = ( await resend.Emails.Receiving.Attachments.get_async( diff --git a/examples/automations.py b/examples/automations.py new file mode 100644 index 0000000..ec5c39c --- /dev/null +++ b/examples/automations.py @@ -0,0 +1,145 @@ +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + +# Create and publish a template to use in the automation +print("--- Create and publish template ---") +tpl: resend.Templates.CreateResponse = resend.Templates.create( + { + "name": "welcome-email", + "subject": "Welcome!", + "html": "Welcome to our service!", + } +) +resend.Templates.publish(tpl["id"]) +print(f"Template: {tpl['id']}") + +# --- Create a simple automation (trigger → send_email) --- +print("\n--- Create automation ---") +simple: resend.Automations.CreateResponse = resend.Automations.create( + { + "name": "Welcome Flow", + "status": "disabled", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.created"}, + }, + { + "key": "send_1", + "type": "send_email", + "config": {"template": {"id": tpl["id"]}}, + }, + ], + "connections": [ + {"from": "trigger_1", "to": "send_1"}, + ], + } +) +automation_id = simple["id"] +print(f"Created automation: {automation_id}") + +# --- Get automation --- +print("\n--- Get automation ---") +automation: resend.Automation = resend.Automations.get(automation_id) +print(f"Name: {automation['name']}, status: {automation['status']}") +for step in automation["steps"]: + print(f" Step key={step['key']} type={step['type']}") +for conn in automation["connections"]: + print(f" Connection: {conn['from']} -> {conn['to']}") + +# --- Update automation --- +print("\n--- Update automation ---") +updated: resend.Automations.UpdateResponse = resend.Automations.update( + { + "automation_id": automation_id, + "status": "enabled", + } +) +print(f"Updated: {updated['id']}") + +# --- List automations --- +print("\n--- List automations ---") +list_resp: resend.Automations.ListResponse = resend.Automations.list() +print(f"Total: {len(list_resp['data'])}, has_more: {list_resp['has_more']}") + +list_enabled: resend.Automations.ListResponse = resend.Automations.list( + params={"status": "enabled", "limit": 10} +) +print(f"Enabled: {len(list_enabled['data'])}") + +# --- Stop automation --- +print("\n--- Stop automation ---") +stopped: resend.Automations.StopResponse = resend.Automations.stop(automation_id) +print(f"Stopped: {stopped['id']}, status: {stopped['status']}") + +# --- List runs --- +print("\n--- List runs ---") +runs: resend.Automations.Runs.ListResponse = resend.Automations.Runs.list(automation_id) +print(f"Total runs: {len(runs['data'])}") +if runs["data"]: + run_id = runs["data"][0]["id"] + run: resend.AutomationRun = resend.Automations.Runs.get(automation_id, run_id) + print(f"Run status: {run['status']}, steps: {len(run['steps'])}") + for run_step in run["steps"]: + print( + f" Step key={run_step['key']} type={run_step['type']} status={run_step['status']}" + ) + +# --- Multi-step automation: delay + wait_for_event --- +print("\n--- Create multi-step automation (delay + wait_for_event) ---") +multi: resend.Automations.CreateResponse = resend.Automations.create( + { + "name": "Onboarding Flow", + "status": "disabled", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.created"}, + }, + { + "key": "delay_1", + "type": "delay", + # duration is a human-readable string; "seconds" (int) is also accepted + "config": {"duration": "30 minutes"}, + }, + { + "key": "wait_1", + "type": "wait_for_event", + # timeout is a human-readable string; timeout_seconds is NOT supported + "config": {"event_name": "user.verified", "timeout": "1 hour"}, + }, + { + "key": "send_1", + "type": "send_email", + "config": {"template": {"id": tpl["id"]}}, + }, + ], + "connections": [ + {"from": "trigger_1", "to": "delay_1"}, + {"from": "delay_1", "to": "wait_1"}, + {"from": "wait_1", "to": "send_1", "type": "event_received"}, + {"from": "wait_1", "to": "send_1", "type": "timeout"}, + ], + } +) +multi_id = multi["id"] +print(f"Created: {multi_id}") + +retrieved: resend.Automation = resend.Automations.get(multi_id) +for step in retrieved["steps"]: + print(f" Step key={step['key']} type={step['type']} config={step['config']}") + +# --- Delete automations and template --- +print("\n--- Cleanup ---") +del1: resend.Automations.DeleteResponse = resend.Automations.remove(automation_id) +print(f"Deleted automation {del1['id']}: {del1['deleted']}") +del2: resend.Automations.DeleteResponse = resend.Automations.remove(multi_id) +print(f"Deleted automation {del2['id']}: {del2['deleted']}") +resend.Templates.remove(tpl["id"]) +print(f"Deleted template {tpl['id']}") diff --git a/examples/events.py b/examples/events.py new file mode 100644 index 0000000..cdb035e --- /dev/null +++ b/examples/events.py @@ -0,0 +1,107 @@ +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + +# Create a contact to use with event sends +print("--- Create contact ---") +contact: resend.Contacts.CreateContactResponse = resend.Contacts.create( + { + "email": "test-events@example.com", + "first_name": "Test", + "last_name": "User", + } +) +contact_id = contact["id"] +print(f"Contact: {contact_id}") + +# --- Create event without schema --- +print("\n--- Create event without schema ---") +created: resend.Events.CreateResponse = resend.Events.create({"name": "user.signed_up"}) +event_id = created["id"] +print(f"Created event: {event_id}") + +# --- Create event with schema --- +print("\n--- Create event with schema ---") +created_with_schema: resend.Events.CreateResponse = resend.Events.create( + { + "name": "user.upgraded", + "schema": { + "plan": "string", + "trial_days": "number", + "is_enterprise": "boolean", + "upgraded_at": "date", + }, + } +) +print(f"Created event with schema: {created_with_schema['id']}") + +# --- Get event by ID --- +print("\n--- Get event by ID ---") +event: resend.Event = resend.Events.get(event_id) +print(f"ID: {event['id']}") +print(f"Name: {event['name']}") +print(f"Schema: {event['schema']}") + +# --- Get event by name --- +print("\n--- Get event by name ---") +event_by_name: resend.Event = resend.Events.get("user.signed_up") +print(f"Found by name: {event_by_name['name']}") + +# --- Update event schema --- +print("\n--- Update event schema ---") +updated: resend.Events.UpdateResponse = resend.Events.update( + { + "identifier": "user.signed_up", + "schema": {"plan": "string", "source": "string"}, + } +) +print(f"Updated event: {updated['id']}") + +# --- Send event with contact_id --- +print("\n--- Send event with contact_id ---") +sent: resend.Events.SendResponse = resend.Events.send( + { + "event": "user.signed_up", + "contact_id": contact_id, + "payload": {"plan": "pro", "source": "web"}, + } +) +print(f"Sent event: {sent['event']}") + +# --- Send event with email --- +print("\n--- Send event with email ---") +sent_email: resend.Events.SendResponse = resend.Events.send( + { + "event": "user.signed_up", + "email": "test-events@example.com", + } +) +print(f"Sent event: {sent_email['event']}") + +# --- List events --- +print("\n--- List events ---") +list_resp: resend.Events.ListResponse = resend.Events.list() +print(f"Total events: {len(list_resp['data'])}, has_more: {list_resp['has_more']}") +for ev in list_resp["data"]: + print(f" {ev['name']} ({ev['id']})") + +# --- List events with pagination --- +print("\n--- List events with pagination ---") +paginated: resend.Events.ListResponse = resend.Events.list(params={"limit": 5}) +print(f"Retrieved {len(paginated['data'])} events") + +# --- Cleanup --- +print("\n--- Delete event by ID ---") +deleted: resend.Events.DeleteResponse = resend.Events.remove(event_id) +print(f"Deleted: {deleted['deleted']}") + +print("\n--- Delete event by name ---") +deleted_by_name: resend.Events.DeleteResponse = resend.Events.remove("user.upgraded") +print(f"Deleted by name: {deleted_by_name['deleted']}") + +print("\n--- Delete contact ---") +resend.Contacts.remove(id=contact_id) +print(f"Deleted contact: {contact_id}") diff --git a/examples/logs.py b/examples/logs.py index 4e091a6..3717d66 100644 --- a/examples/logs.py +++ b/examples/logs.py @@ -19,9 +19,7 @@ "limit": 10, "after": logs["data"][0]["id"], } - paginated_logs: resend.Logs.ListResponse = resend.Logs.list( - params=paginated_params - ) + paginated_logs: resend.Logs.ListResponse = resend.Logs.list(params=paginated_params) print(f"Retrieved {len(paginated_logs['data'])} logs with pagination") print(f"Has more logs: {paginated_logs['has_more']}") else: diff --git a/resend/__init__.py b/resend/__init__.py index 2bce0f8..2e1c8d5 100644 --- a/resend/__init__.py +++ b/resend/__init__.py @@ -1,10 +1,19 @@ import os -from typing import Union +from typing import Optional, Union from .api_keys._api_key import ApiKey from .api_keys._api_keys import ApiKeys from .audiences._audience import Audience from .audiences._audiences import Audiences +from .automations._automation import (Automation, AutomationConnection, + AutomationConnectionType, + AutomationListItem, + AutomationResponseStep, AutomationRun, + AutomationRunListItem, + AutomationRunStatus, AutomationRunStep, + AutomationStatus, AutomationStep, + AutomationStepType) +from .automations._automations import Automations from .broadcasts._broadcast import Broadcast from .broadcasts._broadcasts import Broadcasts from .contact_properties._contact_properties import ContactProperties @@ -17,8 +26,6 @@ from .contacts.segments._contact_segments import ContactSegments from .domains._domain import Domain from .domains._domains import Domains -from .logs._log import Log -from .logs._logs import Logs from .emails._attachment import Attachment, RemoteAttachment from .emails._attachments import Attachments as EmailAttachments from .emails._batch import Batch, BatchValidationError @@ -28,12 +35,16 @@ ListReceivedEmail, ReceivedEmail) from .emails._receiving import Receiving as EmailsReceiving from .emails._tag import Tag +from .events._event import (Event, EventListItem, EventSchema, + EventSchemaFieldType) +from .events._events import Events from .http_client import HTTPClient from .http_client_async import \ AsyncHTTPClient # Okay to import AsyncHTTPClient since it is just an interface. from .http_client_requests import RequestsClient +from .logs._log import Log +from .logs._logs import Logs from .request import Request -from typing import Optional from .segments._segment import Segment from .segments._segments import Segments from .templates._template import Template, TemplateListItem, Variable @@ -69,9 +80,11 @@ "Domains", "Batch", "Audiences", + "Automations", "Contacts", "ContactProperties", "Broadcasts", + "Events", "Segments", "Templates", "Webhooks", @@ -79,6 +92,22 @@ "Logs", # Types "Audience", + "Automation", + "AutomationConnection", + "AutomationConnectionType", + "AutomationListItem", + "AutomationResponseStep", + "AutomationRun", + "AutomationRunListItem", + "AutomationRunStatus", + "AutomationRunStep", + "AutomationStatus", + "AutomationStep", + "AutomationStepType", + "Event", + "EventListItem", + "EventSchema", + "EventSchemaFieldType", "Contact", "ContactSegment", "ContactSegments", diff --git a/resend/async_request.py b/resend/async_request.py index ebc0987..ad8dcd6 100644 --- a/resend/async_request.py +++ b/resend/async_request.py @@ -84,7 +84,9 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: async_client = resend.default_async_http_client # Priority 2: user set an AsyncHTTPClient on default_http_client (legacy, still supported) - if async_client is None and isinstance(resend.default_http_client, AsyncHTTPClient): + if async_client is None and isinstance( + resend.default_http_client, AsyncHTTPClient + ): async_client = resend.default_http_client if async_client is None: diff --git a/resend/automations/__init__.py b/resend/automations/__init__.py new file mode 100644 index 0000000..f9a0391 --- /dev/null +++ b/resend/automations/__init__.py @@ -0,0 +1,27 @@ +from resend.automations._automation import (Automation, AutomationConnection, + AutomationConnectionType, + AutomationListItem, + AutomationResponseStep, + AutomationRun, + AutomationRunListItem, + AutomationRunStatus, + AutomationRunStep, + AutomationStatus, AutomationStep, + AutomationStepType) +from resend.automations._automations import Automations + +__all__ = [ + "Automation", + "AutomationConnection", + "AutomationConnectionType", + "AutomationListItem", + "AutomationResponseStep", + "AutomationRun", + "AutomationRunListItem", + "AutomationRunStatus", + "AutomationRunStep", + "AutomationStatus", + "AutomationStep", + "AutomationStepType", + "Automations", +] diff --git a/resend/automations/_automation.py b/resend/automations/_automation.py new file mode 100644 index 0000000..541f608 --- /dev/null +++ b/resend/automations/_automation.py @@ -0,0 +1,299 @@ +from typing import Any, Dict, List, Union + +from typing_extensions import Literal, NotRequired, TypedDict + +AutomationStatus = Literal["enabled", "disabled"] +AutomationRunStatus = Literal["running", "completed", "failed", "cancelled"] +AutomationStepType = Literal[ + "trigger", + "send_email", + "delay", + "wait_for_event", + "condition", + "contact_update", + "contact_delete", + "add_to_segment", +] +AutomationConnectionType = Literal[ + "default", + "condition_met", + "condition_not_met", + "timeout", + "event_received", +] + +# Uses functional TypedDict syntax because "from" is a reserved keyword in Python +AutomationConnection = TypedDict( + "AutomationConnection", + { + "from": str, + "to": str, + "type": NotRequired[AutomationConnectionType], + }, +) +""" +AutomationConnection represents a connection between two steps in an automation graph. + +Attributes: + from (str): The key of the source step + to (str): The key of the target step + type (NotRequired[AutomationConnectionType]): The type of connection, defaults to "default" +""" + + +class AutomationStep(TypedDict): + """ + AutomationStep represents a step in an automation workflow request. + + Attributes: + key (str): Unique identifier for the step within the graph + type (AutomationStepType): The type of step + config (Dict[str, Any]): Step-specific configuration object + """ + + key: str + """ + Unique identifier for the step within the graph. + """ + type: AutomationStepType + """ + The type of step. + """ + config: Dict[str, Any] + """ + Step-specific configuration. Shape depends on the step type. + """ + + +class AutomationResponseStep(TypedDict): + """ + AutomationResponseStep represents a step as returned by the API. + + Attributes: + key (str): The step identifier + type (AutomationStepType): The type of step + config (Any): Step-specific configuration + """ + + key: str + """ + The step identifier. + """ + type: AutomationStepType + """ + The type of step. + """ + config: Any + """ + Step-specific configuration. + """ + + +class AutomationRunStep(TypedDict): + """ + AutomationRunStep represents a step execution within an automation run. + + Attributes: + key (str): The step identifier + type (str): The type of step + status (str): Execution status of this step + started_at (Union[str, None]): When the step started executing + completed_at (Union[str, None]): When the step completed executing + output (Any): The step output data + error (Any): Any error that occurred + created_at (str): When the run step was created + """ + + key: str + """ + The step identifier. + """ + type: AutomationStepType + """ + The type of step. + """ + status: str + """ + Execution status of this step. + """ + started_at: Union[str, None] + """ + When the step started executing (ISO 8601 format), or None if not started. + """ + completed_at: Union[str, None] + """ + When the step completed executing (ISO 8601 format), or None if not completed. + """ + output: Any + """ + The step output data. + """ + error: Any + """ + Any error that occurred during step execution. + """ + created_at: str + """ + When the run step was created (ISO 8601 format). + """ + + +class AutomationListItem(TypedDict): + """ + AutomationListItem represents an automation in list responses. + + Attributes: + id (str): The automation ID + name (str): The automation name + status (AutomationStatus): Current status + created_at (str): Creation date/time + updated_at (str): Last update date/time + """ + + id: str + """ + The automation ID. + """ + name: str + """ + The automation name. + """ + status: AutomationStatus + """ + Current status of the automation. + """ + created_at: str + """ + When the automation was created (ISO 8601 format). + """ + updated_at: str + """ + When the automation was last updated (ISO 8601 format). + """ + + +class Automation(TypedDict): + """ + Automation represents a full automation object. + + Attributes: + object (str): The object type, always "automation" + id (str): The automation ID + name (str): The automation name + status (AutomationStatus): Current status + created_at (str): Creation date/time + updated_at (str): Last update date/time + steps (List[AutomationResponseStep]): Steps in the active version + connections (List[AutomationConnection]): Connections between steps + """ + + object: str + """ + The object type, always "automation". + """ + id: str + """ + The automation ID. + """ + name: str + """ + The automation name. + """ + status: AutomationStatus + """ + Current status of the automation. + """ + created_at: str + """ + When the automation was created (ISO 8601 format). + """ + updated_at: str + """ + When the automation was last updated (ISO 8601 format). + """ + steps: List[AutomationResponseStep] + """ + Steps in the active version of the automation. + """ + connections: List[AutomationConnection] + """ + Connections between steps in the active version of the automation. + """ + + +class AutomationRunListItem(TypedDict): + """ + AutomationRunListItem represents an automation run in list responses. + + Attributes: + id (str): The run ID + status (AutomationRunStatus): Current run status + started_at (Union[str, None]): When the run started + completed_at (Union[str, None]): When the run completed + created_at (str): When the run was created + """ + + id: str + """ + The run ID. + """ + status: AutomationRunStatus + """ + Current run status. + """ + started_at: Union[str, None] + """ + When the run started (ISO 8601 format), or None. + """ + completed_at: Union[str, None] + """ + When the run completed (ISO 8601 format), or None. + """ + created_at: str + """ + When the run was created (ISO 8601 format). + """ + + +class AutomationRun(TypedDict): + """ + AutomationRun represents a full automation run object. + + Attributes: + object (str): The object type, always "automation_run" + id (str): The run ID + status (AutomationRunStatus): Current run status + started_at (Union[str, None]): When the run started + completed_at (Union[str, None]): When the run completed + created_at (str): When the run was created + steps (List[AutomationRunStep]): Steps in the run, sorted in graph order + """ + + object: str + """ + The object type, always "automation_run". + """ + id: str + """ + The run ID. + """ + status: AutomationRunStatus + """ + Current run status. + """ + started_at: Union[str, None] + """ + When the run started (ISO 8601 format), or None. + """ + completed_at: Union[str, None] + """ + When the run completed (ISO 8601 format), or None. + """ + created_at: str + """ + When the run was created (ISO 8601 format). + """ + steps: List[AutomationRunStep] + """ + Steps in the run, sorted in graph order. + """ diff --git a/resend/automations/_automations.py b/resend/automations/_automations.py new file mode 100644 index 0000000..6e8e85d --- /dev/null +++ b/resend/automations/_automations.py @@ -0,0 +1,609 @@ +from typing import Any, Dict, List, Optional, cast + +from typing_extensions import NotRequired, TypedDict + +from resend import request +from resend._base_response import BaseResponse +from resend.pagination_helper import PaginationHelper + +from ._automation import (Automation, AutomationConnection, AutomationListItem, + AutomationRun, AutomationRunListItem, + AutomationStatus, AutomationStep) + +# Async imports (optional - only available with pip install resend[async]) +try: + from resend.async_request import AsyncRequest +except ImportError: + pass + + +class Automations: + + class CreateParams(TypedDict): + """ + CreateParams is the class that wraps the parameters for the create method. + + Attributes: + name (str): The name of the automation + steps (List[AutomationStep]): The automation workflow steps (must include at least one trigger) + connections (List[AutomationConnection]): Connections between steps in the automation graph + status (NotRequired[AutomationStatus]): Initial status, defaults to "disabled" + """ + + name: str + """ + The name of the automation. + """ + steps: List[AutomationStep] + """ + The automation workflow steps. Must include at least one trigger step. + """ + connections: List[AutomationConnection] + """ + Connections between steps in the automation graph. + """ + status: NotRequired[AutomationStatus] + """ + Initial status of the automation. Defaults to "disabled". + """ + + class UpdateParams(TypedDict): + """ + UpdateParams is the class that wraps the parameters for the update method. + + Attributes: + automation_id (str): The ID of the automation to update + name (NotRequired[str]): Updated automation name + status (NotRequired[AutomationStatus]): Updated status + steps (NotRequired[List[AutomationStep]]): Updated steps (must be provided together with connections) + connections (NotRequired[List[AutomationConnection]]): Updated connections (must be provided together with steps) + """ + + automation_id: str + """ + The ID of the automation to update. + """ + name: NotRequired[str] + """ + Updated automation name. + """ + status: NotRequired[AutomationStatus] + """ + Updated status. + """ + steps: NotRequired[List[AutomationStep]] + """ + Updated workflow steps. Must be provided together with connections. + """ + connections: NotRequired[List[AutomationConnection]] + """ + Updated connections. Must be provided together with steps. + """ + + class ListParams(TypedDict): + """ + ListParams is the class that wraps the parameters for the list method. + + Attributes: + status (NotRequired[AutomationStatus]): Filter automations by status + limit (NotRequired[int]): Number of automations to retrieve (max 100, min 1) + after (NotRequired[str]): Return items after this cursor + before (NotRequired[str]): Return items before this cursor + """ + + status: NotRequired[AutomationStatus] + """ + Filter automations by status. + """ + limit: NotRequired[int] + """ + Number of automations to retrieve. Maximum is 100, and minimum is 1. + """ + after: NotRequired[str] + """ + Return items after this cursor (for pagination). + Cannot be used with the before parameter. + """ + before: NotRequired[str] + """ + Return items before this cursor (for pagination). + Cannot be used with the after parameter. + """ + + class CreateResponse(BaseResponse): + """ + CreateResponse is the class that wraps the response of the create method. + + Attributes: + object (str): The object type, always "automation" + id (str): The ID of the created automation + """ + + object: str + """ + The object type, always "automation". + """ + id: str + """ + The ID of the created automation. + """ + + class UpdateResponse(BaseResponse): + """ + UpdateResponse is the class that wraps the response of the update method. + + Attributes: + object (str): The object type, always "automation" + id (str): The ID of the updated automation + """ + + object: str + """ + The object type, always "automation". + """ + id: str + """ + The ID of the updated automation. + """ + + class DeleteResponse(BaseResponse): + """ + DeleteResponse is the class that wraps the response of the remove method. + + Attributes: + object (str): The object type, always "automation" + id (str): The ID of the deleted automation + deleted (bool): Whether the automation was successfully deleted + """ + + object: str + """ + The object type, always "automation". + """ + id: str + """ + The ID of the deleted automation. + """ + deleted: bool + """ + Whether the automation was successfully deleted. + """ + + class StopResponse(BaseResponse): + """ + StopResponse is the class that wraps the response of the stop method. + + Attributes: + object (str): The object type, always "automation" + id (str): The ID of the stopped automation + status (str): The status after stopping + """ + + object: str + """ + The object type, always "automation". + """ + id: str + """ + The ID of the stopped automation. + """ + status: str + """ + The status after stopping. + """ + + class ListResponse(BaseResponse): + """ + ListResponse is the class that wraps the response of the list method. + + Attributes: + object (str): The object type, always "list" + data (List[AutomationListItem]): A list of automation objects + has_more (bool): Whether there are more results available + """ + + object: str + """ + The object type, always "list". + """ + data: List[AutomationListItem] + """ + A list of automation objects. + """ + has_more: bool + """ + Whether there are more results available for pagination. + """ + + class Runs: + """ + Sub-namespace for automation run methods. + Accessible as resend.Automations.Runs. + """ + + class ListParams(TypedDict): + """ + ListParams is the class that wraps the parameters for the list method. + + Attributes: + status (NotRequired[str]): Comma-separated filter values: "running", "completed", "failed", "cancelled" + limit (NotRequired[int]): Number of runs to retrieve (max 100, min 1) + after (NotRequired[str]): Return items after this cursor + before (NotRequired[str]): Return items before this cursor + """ + + status: NotRequired[str] + """ + Comma-separated filter values. Valid values: "running", "completed", "failed", "cancelled". + """ + limit: NotRequired[int] + """ + Number of runs to retrieve. Maximum is 100, and minimum is 1. + """ + after: NotRequired[str] + """ + Return items after this cursor (for pagination). + Cannot be used with the before parameter. + """ + before: NotRequired[str] + """ + Return items before this cursor (for pagination). + Cannot be used with the after parameter. + """ + + class ListResponse(BaseResponse): + """ + ListResponse is the class that wraps the response of the list method. + + Attributes: + object (str): The object type, always "list" + data (List[AutomationRunListItem]): A list of automation run objects + has_more (bool): Whether there are more results available + """ + + object: str + """ + The object type, always "list". + """ + data: List[AutomationRunListItem] + """ + A list of automation run objects. + """ + has_more: bool + """ + Whether there are more results available for pagination. + """ + + @classmethod + def list( + cls, + automation_id: str, + params: Optional["Automations.Runs.ListParams"] = None, + ) -> "Automations.Runs.ListResponse": + """ + Retrieve a list of runs for an automation. + see more: https://resend.com/docs/api-reference/automations/list-automation-runs + + Args: + automation_id (str): The automation ID + params (Optional[ListParams]): Optional filter and pagination parameters + - status: Comma-separated filter values: "running", "completed", "failed", "cancelled" + - limit: Number of runs to retrieve (max 100, min 1) + - after: Return items after this cursor + - before: Return items before this cursor + + Returns: + ListResponse: A list of automation run objects + """ + base_path = f"/automations/{automation_id}/runs" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = request.Request[Automations.Runs.ListResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + def get(cls, automation_id: str, run_id: str) -> AutomationRun: + """ + Retrieve a single automation run. + see more: https://resend.com/docs/api-reference/automations/get-automation-run + + Args: + automation_id (str): The automation ID + run_id (str): The run ID + + Returns: + AutomationRun: The automation run object + """ + path = f"/automations/{automation_id}/runs/{run_id}" + resp = request.Request[AutomationRun]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + async def list_async( + cls, + automation_id: str, + params: Optional["Automations.Runs.ListParams"] = None, + ) -> "Automations.Runs.ListResponse": + """ + Retrieve a list of runs for an automation (async). + see more: https://resend.com/docs/api-reference/automations/list-automation-runs + + Args: + automation_id (str): The automation ID + params (Optional[ListParams]): Optional filter and pagination parameters + - status: Comma-separated filter values: "running", "completed", "failed", "cancelled" + - limit: Number of runs to retrieve (max 100, min 1) + - after: Return items after this cursor + - before: Return items before this cursor + + Returns: + ListResponse: A list of automation run objects + """ + base_path = f"/automations/{automation_id}/runs" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = await AsyncRequest[Automations.Runs.ListResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + async def get_async(cls, automation_id: str, run_id: str) -> AutomationRun: + """ + Retrieve a single automation run (async). + see more: https://resend.com/docs/api-reference/automations/get-automation-run + + Args: + automation_id (str): The automation ID + run_id (str): The run ID + + Returns: + AutomationRun: The automation run object + """ + path = f"/automations/{automation_id}/runs/{run_id}" + resp = await AsyncRequest[AutomationRun]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + def create(cls, params: "Automations.CreateParams") -> "Automations.CreateResponse": + """ + Create an automation. + see more: https://resend.com/docs/api-reference/automations/create-automation + + Args: + params (CreateParams): The automation creation parameters + + Returns: + CreateResponse: The created automation response + """ + path = "/automations" + resp = request.Request[Automations.CreateResponse]( + path=path, params=cast(Dict[Any, Any], params), verb="post" + ).perform_with_content() + return resp + + @classmethod + def get(cls, automation_id: str) -> Automation: + """ + Retrieve a single automation. + see more: https://resend.com/docs/api-reference/automations/get-automation + + Args: + automation_id (str): The automation ID + + Returns: + Automation: The automation object + """ + path = f"/automations/{automation_id}" + resp = request.Request[Automation]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + def update(cls, params: "Automations.UpdateParams") -> "Automations.UpdateResponse": + """ + Update an automation. + see more: https://resend.com/docs/api-reference/automations/update-automation + + Args: + params (UpdateParams): The automation update parameters + + Returns: + UpdateResponse: The updated automation response + """ + path = f"/automations/{params['automation_id']}" + body = {k: v for k, v in params.items() if k != "automation_id"} + resp = request.Request[Automations.UpdateResponse]( + path=path, params=cast(Dict[Any, Any], body), verb="patch" + ).perform_with_content() + return resp + + @classmethod + def remove(cls, automation_id: str) -> "Automations.DeleteResponse": + """ + Delete an automation. + see more: https://resend.com/docs/api-reference/automations/delete-automation + + Args: + automation_id (str): The automation ID + + Returns: + DeleteResponse: The delete response + """ + path = f"/automations/{automation_id}" + resp = request.Request[Automations.DeleteResponse]( + path=path, params={}, verb="delete" + ).perform_with_content() + return resp + + @classmethod + def stop(cls, automation_id: str) -> "Automations.StopResponse": + """ + Stop all active runs of an automation. + see more: https://resend.com/docs/api-reference/automations/stop-automation + + Args: + automation_id (str): The automation ID + + Returns: + StopResponse: The stop response + """ + path = f"/automations/{automation_id}/stop" + resp = request.Request[Automations.StopResponse]( + path=path, params={}, verb="post" + ).perform_with_content() + return resp + + @classmethod + def list( + cls, params: Optional["Automations.ListParams"] = None + ) -> "Automations.ListResponse": + """ + Retrieve a list of automations. + see more: https://resend.com/docs/api-reference/automations/list-automations + + Args: + params (Optional[ListParams]): Optional filter and pagination parameters + - status: Filter automations by status ("enabled" or "disabled") + - limit: Number of automations to retrieve (max 100, min 1) + - after: Return items after this cursor + - before: Return items before this cursor + + Returns: + ListResponse: A list of automation objects + """ + base_path = "/automations" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = request.Request[Automations.ListResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + async def create_async( + cls, params: "Automations.CreateParams" + ) -> "Automations.CreateResponse": + """ + Create an automation (async). + see more: https://resend.com/docs/api-reference/automations/create-automation + + Args: + params (CreateParams): The automation creation parameters + + Returns: + CreateResponse: The created automation response + """ + path = "/automations" + resp = await AsyncRequest[Automations.CreateResponse]( + path=path, params=cast(Dict[Any, Any], params), verb="post" + ).perform_with_content() + return resp + + @classmethod + async def get_async(cls, automation_id: str) -> Automation: + """ + Retrieve a single automation (async). + see more: https://resend.com/docs/api-reference/automations/get-automation + + Args: + automation_id (str): The automation ID + + Returns: + Automation: The automation object + """ + path = f"/automations/{automation_id}" + resp = await AsyncRequest[Automation]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + async def update_async( + cls, params: "Automations.UpdateParams" + ) -> "Automations.UpdateResponse": + """ + Update an automation (async). + see more: https://resend.com/docs/api-reference/automations/update-automation + + Args: + params (UpdateParams): The automation update parameters + + Returns: + UpdateResponse: The updated automation response + """ + path = f"/automations/{params['automation_id']}" + body = {k: v for k, v in params.items() if k != "automation_id"} + resp = await AsyncRequest[Automations.UpdateResponse]( + path=path, params=cast(Dict[Any, Any], body), verb="patch" + ).perform_with_content() + return resp + + @classmethod + async def remove_async(cls, automation_id: str) -> "Automations.DeleteResponse": + """ + Delete an automation (async). + see more: https://resend.com/docs/api-reference/automations/delete-automation + + Args: + automation_id (str): The automation ID + + Returns: + DeleteResponse: The delete response + """ + path = f"/automations/{automation_id}" + resp = await AsyncRequest[Automations.DeleteResponse]( + path=path, params={}, verb="delete" + ).perform_with_content() + return resp + + @classmethod + async def stop_async(cls, automation_id: str) -> "Automations.StopResponse": + """ + Stop all active runs of an automation (async). + see more: https://resend.com/docs/api-reference/automations/stop-automation + + Args: + automation_id (str): The automation ID + + Returns: + StopResponse: The stop response + """ + path = f"/automations/{automation_id}/stop" + resp = await AsyncRequest[Automations.StopResponse]( + path=path, params={}, verb="post" + ).perform_with_content() + return resp + + @classmethod + async def list_async( + cls, params: Optional["Automations.ListParams"] = None + ) -> "Automations.ListResponse": + """ + Retrieve a list of automations (async). + see more: https://resend.com/docs/api-reference/automations/list-automations + + Args: + params (Optional[ListParams]): Optional filter and pagination parameters + - status: Filter automations by status ("enabled" or "disabled") + - limit: Number of automations to retrieve (max 100, min 1) + - after: Return items after this cursor + - before: Return items before this cursor + + Returns: + ListResponse: A list of automation objects + """ + base_path = "/automations" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = await AsyncRequest[Automations.ListResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp diff --git a/resend/events/__init__.py b/resend/events/__init__.py new file mode 100644 index 0000000..52e8379 --- /dev/null +++ b/resend/events/__init__.py @@ -0,0 +1,11 @@ +from resend.events._event import (Event, EventListItem, EventSchema, + EventSchemaFieldType) +from resend.events._events import Events + +__all__ = [ + "Event", + "EventListItem", + "EventSchema", + "EventSchemaFieldType", + "Events", +] diff --git a/resend/events/_event.py b/resend/events/_event.py new file mode 100644 index 0000000..6ae9935 --- /dev/null +++ b/resend/events/_event.py @@ -0,0 +1,88 @@ +from typing import Dict, Union + +from typing_extensions import Literal, TypedDict + +EventSchemaFieldType = Literal["string", "number", "boolean", "date"] +""" +EventSchemaFieldType is the type of a field in an event schema. +Supported types: "string", "number", "boolean", "date" +""" + +EventSchema = Dict[str, EventSchemaFieldType] +""" +EventSchema is a flat key/type map defining the structure of an event payload. +Keys are field names, values are the field types. +""" + + +class EventListItem(TypedDict): + """ + EventListItem represents an event in list responses. + + Attributes: + id (str): The event ID (UUID) + name (str): The event name + schema (Union[EventSchema, None]): The event schema definition + created_at (str): Creation date/time + updated_at (Union[str, None]): Last update date/time + """ + + id: str + """ + The event ID (UUID). + """ + name: str + """ + The event name. + """ + schema: Union[EventSchema, None] + """ + The event schema definition, or None if no schema is defined. + """ + created_at: str + """ + When the event was created (ISO 8601 format). + """ + updated_at: Union[str, None] + """ + When the event was last updated (ISO 8601 format), or None. + """ + + +class Event(TypedDict): + """ + Event represents a full event object. + + Attributes: + object (str): The object type, always "event" + id (str): The event ID (UUID) + name (str): The event name + schema (Union[EventSchema, None]): The event schema definition + created_at (str): Creation date/time + updated_at (Union[str, None]): Last update date/time + """ + + object: str + """ + The object type, always "event". + """ + id: str + """ + The event ID (UUID). + """ + name: str + """ + The event name. + """ + schema: Union[EventSchema, None] + """ + The event schema definition, or None if no schema is defined. + """ + created_at: str + """ + When the event was created (ISO 8601 format). + """ + updated_at: Union[str, None] + """ + When the event was last updated (ISO 8601 format), or None. + """ diff --git a/resend/events/_events.py b/resend/events/_events.py new file mode 100644 index 0000000..dd1890d --- /dev/null +++ b/resend/events/_events.py @@ -0,0 +1,451 @@ +from typing import Any, Dict, List, Optional, Union, cast +from urllib.parse import quote + +from typing_extensions import NotRequired, TypedDict + +from resend import request +from resend._base_response import BaseResponse +from resend.pagination_helper import PaginationHelper + +from ._event import Event, EventListItem, EventSchema + +# Async imports (optional - only available with pip install resend[async]) +try: + from resend.async_request import AsyncRequest +except ImportError: + pass + + +class Events: + + class CreateParams(TypedDict): + """ + CreateParams is the class that wraps the parameters for the create method. + + Attributes: + name (str): The name of the event. Cannot start with the "resend:" prefix. + schema (NotRequired[Union[EventSchema, None]]): Flat key/type map defining the payload schema. + """ + + name: str + """ + The name of the event. Cannot start with the "resend:" prefix. + """ + schema: NotRequired[Union[EventSchema, None]] + """ + Flat key/type map defining the event payload schema. + Supported types: "string", "number", "boolean", "date". + """ + + class UpdateParams(TypedDict): + """ + UpdateParams is the class that wraps the parameters for the update method. + + Attributes: + identifier (str): The event ID (UUID) or event name. + schema (Union[EventSchema, None]): Updated schema. Set to None to clear the schema. + """ + + identifier: str + """ + The event ID (UUID) or event name. + """ + schema: Union[EventSchema, None] + """ + Updated schema definition. Set to None to clear the schema. + Supported types: "string", "number", "boolean", "date". + """ + + class SendParams(TypedDict): + """ + SendParams is the class that wraps the parameters for the send method. + Exactly one of contact_id or email must be provided. + + Attributes: + event (str): The name of the event to send. + contact_id (NotRequired[str]): The contact ID to send the event for. + email (NotRequired[str]): The email address to send the event for. + payload (NotRequired[Dict[str, Any]]): Key/value pairs to include with the event. + """ + + event: str + """ + The name of the event to send. + """ + contact_id: NotRequired[str] + """ + The contact ID to send the event for. + Exactly one of contact_id or email must be provided. + """ + email: NotRequired[str] + """ + The email address to send the event for. + Exactly one of contact_id or email must be provided. + """ + payload: NotRequired[Dict[str, Any]] + """ + Key/value pairs to include with the event. + """ + + class ListParams(TypedDict): + """ + ListParams is the class that wraps the parameters for the list method. + + Attributes: + limit (NotRequired[int]): Number of events to retrieve (max 100, min 1) + after (NotRequired[str]): Return items after this cursor + before (NotRequired[str]): Return items before this cursor + """ + + limit: NotRequired[int] + """ + Number of events to retrieve. Maximum is 100, and minimum is 1. + """ + after: NotRequired[str] + """ + Return items after this cursor (for pagination). + Cannot be used with the before parameter. + """ + before: NotRequired[str] + """ + Return items before this cursor (for pagination). + Cannot be used with the after parameter. + """ + + class CreateResponse(BaseResponse): + """ + CreateResponse is the class that wraps the response of the create method. + + Attributes: + object (str): The object type, always "event" + id (str): The ID of the created event + """ + + object: str + """ + The object type, always "event". + """ + id: str + """ + The ID of the created event (UUID). + """ + + class UpdateResponse(BaseResponse): + """ + UpdateResponse is the class that wraps the response of the update method. + + Attributes: + object (str): The object type, always "event" + id (str): The ID of the updated event + """ + + object: str + """ + The object type, always "event". + """ + id: str + """ + The ID of the updated event (UUID). + """ + + class DeleteResponse(BaseResponse): + """ + DeleteResponse is the class that wraps the response of the remove method. + + Attributes: + object (str): The object type, always "event" + id (str): The ID of the deleted event + deleted (bool): Whether the event was successfully deleted + """ + + object: str + """ + The object type, always "event". + """ + id: str + """ + The ID of the deleted event (UUID). + """ + deleted: bool + """ + Whether the event was successfully deleted. + """ + + class SendResponse(BaseResponse): + """ + SendResponse is the class that wraps the response of the send method. + + Attributes: + object (str): The object type, always "event" + event (str): The name of the event that was sent + """ + + object: str + """ + The object type, always "event". + """ + event: str + """ + The name of the event that was sent. + """ + + class ListResponse(BaseResponse): + """ + ListResponse is the class that wraps the response of the list method. + + Attributes: + object (str): The object type, always "list" + data (List[EventListItem]): A list of event objects + has_more (bool): Whether there are more results available + """ + + object: str + """ + The object type, always "list". + """ + data: List[EventListItem] + """ + A list of event objects. + """ + has_more: bool + """ + Whether there are more results available for pagination. + """ + + @classmethod + def create(cls, params: "Events.CreateParams") -> "Events.CreateResponse": + """ + Create an event definition. + see more: https://resend.com/docs/api-reference/events/create-event + + Args: + params (CreateParams): The event creation parameters + + Returns: + CreateResponse: The created event response + """ + path = "/events" + resp = request.Request[Events.CreateResponse]( + path=path, params=cast(Dict[Any, Any], params), verb="post" + ).perform_with_content() + return resp + + @classmethod + def get(cls, identifier: str) -> Event: + """ + Retrieve a single event by ID or name. + see more: https://resend.com/docs/api-reference/events/get-event + + Args: + identifier (str): The event ID (UUID) or event name + + Returns: + Event: The event object + """ + path = f"/events/{quote(identifier, safe='')}" + resp = request.Request[Event]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + def update(cls, params: "Events.UpdateParams") -> "Events.UpdateResponse": + """ + Update an event definition. + see more: https://resend.com/docs/api-reference/events/update-event + + Args: + params (UpdateParams): The event update parameters + + Returns: + UpdateResponse: The updated event response + """ + path = f"/events/{quote(params['identifier'], safe='')}" + body = {k: v for k, v in params.items() if k != "identifier"} + resp = request.Request[Events.UpdateResponse]( + path=path, params=cast(Dict[Any, Any], body), verb="patch" + ).perform_with_content() + return resp + + @classmethod + def remove(cls, identifier: str) -> "Events.DeleteResponse": + """ + Delete an event definition. + see more: https://resend.com/docs/api-reference/events/delete-event + + Args: + identifier (str): The event ID (UUID) or event name + + Returns: + DeleteResponse: The delete response + """ + path = f"/events/{quote(identifier, safe='')}" + resp = request.Request[Events.DeleteResponse]( + path=path, params={}, verb="delete" + ).perform_with_content() + return resp + + @classmethod + def send(cls, params: "Events.SendParams") -> "Events.SendResponse": + """ + Send an event for a contact. + see more: https://resend.com/docs/api-reference/events/send-event + + Args: + params (SendParams): The event send parameters. + Exactly one of contact_id or email must be provided. + + Returns: + SendResponse: The send event response + """ + path = "/events/send" + resp = request.Request[Events.SendResponse]( + path=path, params=cast(Dict[Any, Any], params), verb="post" + ).perform_with_content() + return resp + + @classmethod + def list( + cls, params: Optional["Events.ListParams"] = None + ) -> "Events.ListResponse": + """ + Retrieve a list of event definitions. + see more: https://resend.com/docs/api-reference/events/list-events + + Args: + params (Optional[ListParams]): Optional pagination parameters + - limit: Number of events to retrieve (max 100, min 1) + - after: Return items after this cursor + - before: Return items before this cursor + + Returns: + ListResponse: A list of event objects + """ + base_path = "/events" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = request.Request[Events.ListResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + async def create_async( + cls, params: "Events.CreateParams" + ) -> "Events.CreateResponse": + """ + Create an event definition (async). + see more: https://resend.com/docs/api-reference/events/create-event + + Args: + params (CreateParams): The event creation parameters + + Returns: + CreateResponse: The created event response + """ + path = "/events" + resp = await AsyncRequest[Events.CreateResponse]( + path=path, params=cast(Dict[Any, Any], params), verb="post" + ).perform_with_content() + return resp + + @classmethod + async def get_async(cls, identifier: str) -> Event: + """ + Retrieve a single event by ID or name (async). + see more: https://resend.com/docs/api-reference/events/get-event + + Args: + identifier (str): The event ID (UUID) or event name + + Returns: + Event: The event object + """ + path = f"/events/{quote(identifier, safe='')}" + resp = await AsyncRequest[Event]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp + + @classmethod + async def update_async( + cls, params: "Events.UpdateParams" + ) -> "Events.UpdateResponse": + """ + Update an event definition (async). + see more: https://resend.com/docs/api-reference/events/update-event + + Args: + params (UpdateParams): The event update parameters + + Returns: + UpdateResponse: The updated event response + """ + path = f"/events/{quote(params['identifier'], safe='')}" + body = {k: v for k, v in params.items() if k != "identifier"} + resp = await AsyncRequest[Events.UpdateResponse]( + path=path, params=cast(Dict[Any, Any], body), verb="patch" + ).perform_with_content() + return resp + + @classmethod + async def remove_async(cls, identifier: str) -> "Events.DeleteResponse": + """ + Delete an event definition (async). + see more: https://resend.com/docs/api-reference/events/delete-event + + Args: + identifier (str): The event ID (UUID) or event name + + Returns: + DeleteResponse: The delete response + """ + path = f"/events/{quote(identifier, safe='')}" + resp = await AsyncRequest[Events.DeleteResponse]( + path=path, params={}, verb="delete" + ).perform_with_content() + return resp + + @classmethod + async def send_async(cls, params: "Events.SendParams") -> "Events.SendResponse": + """ + Send an event for a contact (async). + see more: https://resend.com/docs/api-reference/events/send-event + + Args: + params (SendParams): The event send parameters. + Exactly one of contact_id or email must be provided. + + Returns: + SendResponse: The send event response + """ + path = "/events/send" + resp = await AsyncRequest[Events.SendResponse]( + path=path, params=cast(Dict[Any, Any], params), verb="post" + ).perform_with_content() + return resp + + @classmethod + async def list_async( + cls, params: Optional["Events.ListParams"] = None + ) -> "Events.ListResponse": + """ + Retrieve a list of event definitions (async). + see more: https://resend.com/docs/api-reference/events/list-events + + Args: + params (Optional[ListParams]): Optional pagination parameters + - limit: Number of events to retrieve (max 100, min 1) + - after: Return items after this cursor + - before: Return items before this cursor + + Returns: + ListResponse: A list of event objects + """ + base_path = "/events" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + resp = await AsyncRequest[Events.ListResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + return resp diff --git a/resend/http_client_httpx.py b/resend/http_client_httpx.py index d7d6c7d..8ff2373 100644 --- a/resend/http_client_httpx.py +++ b/resend/http_client_httpx.py @@ -1,9 +1,9 @@ from typing import Dict, List, Mapping, Optional, Tuple, Union -from resend.http_client_async import AsyncHTTPClient - import httpx +from resend.http_client_async import AsyncHTTPClient + class HTTPXClient(AsyncHTTPClient): """ diff --git a/tests/async_response_headers_test.py b/tests/async_response_headers_test.py index 7db54f7..e83f19a 100644 --- a/tests/async_response_headers_test.py +++ b/tests/async_response_headers_test.py @@ -4,7 +4,6 @@ import resend - pytestmark = pytest.mark.asyncio @@ -104,16 +103,16 @@ async def test_received_email_async_headers_not_overwritten_by_http_headers( resend.default_async_http_client = mock_client try: - response = await resend.Emails.Receiving.get_async( - email_id="email_456" - ) + response = await resend.Emails.Receiving.get_async(email_id="email_456") assert isinstance(response, dict) assert response["id"] == "email_456" # MIME email headers must be intact assert "headers" in response - assert response["headers"]["List-Unsubscribe"] == "" + assert ( + response["headers"]["List-Unsubscribe"] == "" + ) assert response["headers"]["X-Custom"] == "value" # HTTP response headers must be injected separately under http_headers diff --git a/tests/automations_async_test.py b/tests/automations_async_test.py new file mode 100644 index 0000000..7b8cfda --- /dev/null +++ b/tests/automations_async_test.py @@ -0,0 +1,233 @@ +import pytest + +import resend +from resend.exceptions import NoContentError +from tests.conftest import AsyncResendBaseTest + +# flake8: noqa + +pytestmark = pytest.mark.asyncio + + +class TestResendAutomationsAsync(AsyncResendBaseTest): + async def test_automations_create_async(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + } + ) + + params: resend.Automations.CreateParams = { + "name": "Welcome Sequence", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.signed_up"}, + }, + ], + "connections": [], + } + automation = await resend.Automations.create_async(params) + assert automation["object"] == "automation" + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + async def test_automations_create_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + params: resend.Automations.CreateParams = { + "name": "Welcome Sequence", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.signed_up"}, + }, + ], + "connections": [], + } + with pytest.raises(NoContentError): + _ = await resend.Automations.create_async(params) + + async def test_automations_get_async(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Welcome Sequence", + "status": "enabled", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-02T00:00:00.000Z", + "steps": [], + "connections": [], + } + ) + + automation = await resend.Automations.get_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert automation["name"] == "Welcome Sequence" + assert automation["status"] == "enabled" + + async def test_automations_get_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Automations.get_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + + async def test_automations_update_async(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + } + ) + + params: resend.Automations.UpdateParams = { + "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "status": "enabled", + } + automation = await resend.Automations.update_async(params) + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + async def test_automations_update_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + params: resend.Automations.UpdateParams = { + "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "status": "enabled", + } + with pytest.raises(NoContentError): + _ = await resend.Automations.update_async(params) + + async def test_automations_remove_async(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "deleted": True, + } + ) + + resp = await resend.Automations.remove_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert resp["deleted"] is True + + async def test_automations_remove_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Automations.remove_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + + async def test_automations_stop_async(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "status": "disabled", + } + ) + + resp = await resend.Automations.stop_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert resp["status"] == "disabled" + + async def test_automations_stop_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Automations.stop_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + + async def test_automations_list_async(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Welcome Sequence", + "status": "enabled", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-02T00:00:00.000Z", + }, + ], + } + ) + + automations = await resend.Automations.list_async() + assert automations["object"] == "list" + assert automations["has_more"] is False + assert len(automations["data"]) == 1 + assert automations["data"][0]["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + async def test_automations_list_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Automations.list_async() + + async def test_automations_list_runs_async(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "run_123", + "status": "completed", + "started_at": "2024-01-01T10:00:00.000Z", + "completed_at": "2024-01-01T10:05:00.000Z", + "created_at": "2024-01-01T09:59:00.000Z", + }, + ], + } + ) + + runs = await resend.Automations.Runs.list_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + assert runs["object"] == "list" + assert len(runs["data"]) == 1 + assert runs["data"][0]["id"] == "run_123" + assert runs["data"][0]["status"] == "completed" + + async def test_automations_list_runs_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Automations.Runs.list_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + ) + + async def test_automations_get_run_async(self) -> None: + self.set_mock_json( + { + "object": "automation_run", + "id": "run_123", + "status": "completed", + "started_at": "2024-01-01T10:00:00.000Z", + "completed_at": "2024-01-01T10:05:00.000Z", + "created_at": "2024-01-01T09:59:00.000Z", + "steps": [], + } + ) + + run = await resend.Automations.Runs.get_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "run_123" + ) + assert run["object"] == "automation_run" + assert run["id"] == "run_123" + assert run["status"] == "completed" + + async def test_automations_get_run_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Automations.Runs.get_async( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "run_123" + ) diff --git a/tests/automations_test.py b/tests/automations_test.py new file mode 100644 index 0000000..9f09243 --- /dev/null +++ b/tests/automations_test.py @@ -0,0 +1,350 @@ +import resend +from tests.conftest import ResendBaseTest + +# flake8: noqa + + +class TestResendAutomations(ResendBaseTest): + def test_automations_create(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + } + ) + + params: resend.Automations.CreateParams = { + "name": "Welcome Sequence", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.signed_up"}, + }, + { + "key": "email_1", + "type": "send_email", + "config": {"template": {"id": "tpl_123"}}, + }, + ], + "connections": [ + {"from": "trigger_1", "to": "email_1"}, + ], + } + automation = resend.Automations.create(params) + assert automation["object"] == "automation" + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + def test_automations_create_with_status(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + } + ) + + params: resend.Automations.CreateParams = { + "name": "Welcome Sequence", + "status": "enabled", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.signed_up"}, + }, + ], + "connections": [], + } + automation = resend.Automations.create(params) + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + def test_automations_get(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Welcome Sequence", + "status": "enabled", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-02T00:00:00.000Z", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "config": {"event_name": "user.signed_up"}, + } + ], + "connections": [], + } + ) + + automation = resend.Automations.get("b6d24b8e-af0b-4c3c-be0c-359bbd97381e") + assert automation["object"] == "automation" + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert automation["name"] == "Welcome Sequence" + assert automation["status"] == "enabled" + assert automation["created_at"] == "2024-01-01T00:00:00.000Z" + assert automation["updated_at"] == "2024-01-02T00:00:00.000Z" + assert len(automation["steps"]) == 1 + assert automation["steps"][0]["key"] == "trigger_1" + assert automation["steps"][0]["type"] == "trigger" + assert len(automation["connections"]) == 0 + + def test_automations_update(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + } + ) + + params: resend.Automations.UpdateParams = { + "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "status": "enabled", + } + automation = resend.Automations.update(params) + assert automation["object"] == "automation" + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + def test_automations_update_name(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + } + ) + + params: resend.Automations.UpdateParams = { + "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Updated Sequence", + } + automation = resend.Automations.update(params) + assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + + def test_automations_remove(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "deleted": True, + } + ) + + resp = resend.Automations.remove("b6d24b8e-af0b-4c3c-be0c-359bbd97381e") + assert resp["object"] == "automation" + assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert resp["deleted"] is True + + def test_automations_stop(self) -> None: + self.set_mock_json( + { + "object": "automation", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "status": "disabled", + } + ) + + resp = resend.Automations.stop("b6d24b8e-af0b-4c3c-be0c-359bbd97381e") + assert resp["object"] == "automation" + assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert resp["status"] == "disabled" + + def test_automations_list(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Welcome Sequence", + "status": "enabled", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-02T00:00:00.000Z", + }, + { + "id": "c7e35c9f-bf1c-5d4d-cf1d-460cce08492f", + "name": "Onboarding Flow", + "status": "disabled", + "created_at": "2024-02-01T00:00:00.000Z", + "updated_at": "2024-02-02T00:00:00.000Z", + }, + ], + } + ) + + automations = resend.Automations.list() + assert automations["object"] == "list" + assert automations["has_more"] is False + assert len(automations["data"]) == 2 + + first = automations["data"][0] + assert first["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e" + assert first["name"] == "Welcome Sequence" + assert first["status"] == "enabled" + assert first["created_at"] == "2024-01-01T00:00:00.000Z" + + second = automations["data"][1] + assert second["id"] == "c7e35c9f-bf1c-5d4d-cf1d-460cce08492f" + assert second["status"] == "disabled" + + def test_automations_list_with_status_filter(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Welcome Sequence", + "status": "enabled", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-02T00:00:00.000Z", + }, + ], + } + ) + + params: resend.Automations.ListParams = {"status": "enabled"} + automations = resend.Automations.list(params=params) + assert automations["object"] == "list" + assert len(automations["data"]) == 1 + assert automations["data"][0]["status"] == "enabled" + + def test_automations_list_with_pagination_params(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": True, + "data": [ + { + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "Welcome Sequence", + "status": "enabled", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-02T00:00:00.000Z", + }, + ], + } + ) + + params: resend.Automations.ListParams = { + "limit": 10, + "after": "some-cursor", + } + automations = resend.Automations.list(params=params) + assert automations["has_more"] is True + assert len(automations["data"]) == 1 + + def test_automations_list_runs(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "run_123", + "status": "completed", + "started_at": "2024-01-01T10:00:00.000Z", + "completed_at": "2024-01-01T10:05:00.000Z", + "created_at": "2024-01-01T09:59:00.000Z", + }, + { + "id": "run_456", + "status": "running", + "started_at": "2024-01-02T10:00:00.000Z", + "completed_at": None, + "created_at": "2024-01-02T09:59:00.000Z", + }, + ], + } + ) + + runs = resend.Automations.Runs.list("b6d24b8e-af0b-4c3c-be0c-359bbd97381e") + assert runs["object"] == "list" + assert runs["has_more"] is False + assert len(runs["data"]) == 2 + + first = runs["data"][0] + assert first["id"] == "run_123" + assert first["status"] == "completed" + assert first["started_at"] == "2024-01-01T10:00:00.000Z" + assert first["completed_at"] == "2024-01-01T10:05:00.000Z" + + second = runs["data"][1] + assert second["id"] == "run_456" + assert second["status"] == "running" + assert second["completed_at"] is None + + def test_automations_list_runs_with_status_filter(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "run_123", + "status": "completed", + "started_at": "2024-01-01T10:00:00.000Z", + "completed_at": "2024-01-01T10:05:00.000Z", + "created_at": "2024-01-01T09:59:00.000Z", + }, + ], + } + ) + + params: resend.Automations.Runs.ListParams = {"status": "completed"} + runs = resend.Automations.Runs.list( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", params=params + ) + assert len(runs["data"]) == 1 + assert runs["data"][0]["status"] == "completed" + + def test_automations_get_run(self) -> None: + self.set_mock_json( + { + "object": "automation_run", + "id": "run_123", + "status": "completed", + "started_at": "2024-01-01T10:00:00.000Z", + "completed_at": "2024-01-01T10:05:00.000Z", + "created_at": "2024-01-01T09:59:00.000Z", + "steps": [ + { + "key": "trigger_1", + "type": "trigger", + "status": "completed", + "started_at": "2024-01-01T10:00:00.000Z", + "completed_at": "2024-01-01T10:00:01.000Z", + "output": None, + "error": None, + "created_at": "2024-01-01T09:59:00.000Z", + }, + { + "key": "email_1", + "type": "send_email", + "status": "completed", + "started_at": "2024-01-01T10:01:00.000Z", + "completed_at": "2024-01-01T10:01:02.000Z", + "output": None, + "error": None, + "created_at": "2024-01-01T09:59:00.000Z", + }, + ], + } + ) + + run = resend.Automations.Runs.get( + "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "run_123" + ) + assert run["object"] == "automation_run" + assert run["id"] == "run_123" + assert run["status"] == "completed" + assert run["started_at"] == "2024-01-01T10:00:00.000Z" + assert run["completed_at"] == "2024-01-01T10:05:00.000Z" + assert len(run["steps"]) == 2 + assert run["steps"][0]["key"] == "trigger_1" + assert run["steps"][0]["type"] == "trigger" + assert run["steps"][0]["status"] == "completed" + assert run["steps"][1]["key"] == "email_1" + assert run["steps"][1]["type"] == "send_email" diff --git a/tests/events_async_test.py b/tests/events_async_test.py new file mode 100644 index 0000000..6c1e765 --- /dev/null +++ b/tests/events_async_test.py @@ -0,0 +1,148 @@ +import pytest + +import resend +from resend.exceptions import NoContentError +from tests.conftest import AsyncResendBaseTest + +# flake8: noqa + +pytestmark = pytest.mark.asyncio + + +class TestResendEventsAsync(AsyncResendBaseTest): + async def test_events_create_async(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + } + ) + + params: resend.Events.CreateParams = { + "name": "user.signed_up", + } + event = await resend.Events.create_async(params) + assert event["object"] == "event" + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + + async def test_events_create_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + params: resend.Events.CreateParams = {"name": "user.signed_up"} + with pytest.raises(NoContentError): + _ = await resend.Events.create_async(params) + + async def test_events_get_async(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "name": "user.signed_up", + "schema": {"plan": "string"}, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": None, + } + ) + + event = await resend.Events.get_async("user.signed_up") + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + assert event["name"] == "user.signed_up" + + async def test_events_get_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Events.get_async("user.signed_up") + + async def test_events_update_async(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + } + ) + + params: resend.Events.UpdateParams = { + "identifier": "user.signed_up", + "schema": {"plan": "string"}, + } + event = await resend.Events.update_async(params) + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + + async def test_events_update_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + params: resend.Events.UpdateParams = { + "identifier": "user.signed_up", + "schema": None, + } + with pytest.raises(NoContentError): + _ = await resend.Events.update_async(params) + + async def test_events_remove_async(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "deleted": True, + } + ) + + resp = await resend.Events.remove_async("user.signed_up") + assert resp["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + assert resp["deleted"] is True + + async def test_events_remove_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Events.remove_async("user.signed_up") + + async def test_events_send_async_with_contact_id(self) -> None: + self.set_mock_json( + { + "object": "event", + "event": "user.signed_up", + } + ) + + params: resend.Events.SendParams = { + "event": "user.signed_up", + "contact_id": "78b8d3bc-a55a-45a3-aee6-6ec0a5e13d7e", + } + resp = await resend.Events.send_async(params) + assert resp["object"] == "event" + assert resp["event"] == "user.signed_up" + + async def test_events_send_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + params: resend.Events.SendParams = { + "event": "user.signed_up", + "email": "user@example.com", + } + with pytest.raises(NoContentError): + _ = await resend.Events.send_async(params) + + async def test_events_list_async(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "name": "user.signed_up", + "schema": None, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": None, + }, + ], + } + ) + + events = await resend.Events.list_async() + assert events["object"] == "list" + assert events["has_more"] is False + assert len(events["data"]) == 1 + assert events["data"][0]["name"] == "user.signed_up" + + async def test_events_list_async_raises_when_no_content(self) -> None: + self.set_mock_json(None) + with pytest.raises(NoContentError): + _ = await resend.Events.list_async() diff --git a/tests/events_test.py b/tests/events_test.py new file mode 100644 index 0000000..d6e3514 --- /dev/null +++ b/tests/events_test.py @@ -0,0 +1,245 @@ +import resend +from tests.conftest import ResendBaseTest + +# flake8: noqa + + +class TestResendEvents(ResendBaseTest): + def test_events_create(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + } + ) + + params: resend.Events.CreateParams = { + "name": "user.signed_up", + } + event = resend.Events.create(params) + assert event["object"] == "event" + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + + def test_events_create_with_schema(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + } + ) + + params: resend.Events.CreateParams = { + "name": "user.signed_up", + "schema": { + "plan": "string", + "trial_days": "number", + "is_enterprise": "boolean", + }, + } + event = resend.Events.create(params) + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + + def test_events_get_by_id(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "name": "user.signed_up", + "schema": {"plan": "string"}, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": None, + } + ) + + event = resend.Events.get("56261eea-8f8b-4381-83c6-79fa7120f1cf") + assert event["object"] == "event" + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + assert event["name"] == "user.signed_up" + assert event["schema"] == {"plan": "string"} + assert event["created_at"] == "2024-01-01T00:00:00.000Z" + assert event["updated_at"] is None + + def test_events_get_by_name(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "name": "user.signed_up", + "schema": None, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": None, + } + ) + + event = resend.Events.get("user.signed_up") + assert event["name"] == "user.signed_up" + assert event["schema"] is None + + def test_events_update(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + } + ) + + params: resend.Events.UpdateParams = { + "identifier": "user.signed_up", + "schema": {"plan": "string", "amount": "number"}, + } + event = resend.Events.update(params) + assert event["object"] == "event" + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + + def test_events_update_clear_schema(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + } + ) + + params: resend.Events.UpdateParams = { + "identifier": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "schema": None, + } + event = resend.Events.update(params) + assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + + def test_events_remove(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "deleted": True, + } + ) + + resp = resend.Events.remove("56261eea-8f8b-4381-83c6-79fa7120f1cf") + assert resp["object"] == "event" + assert resp["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + assert resp["deleted"] is True + + def test_events_remove_by_name(self) -> None: + self.set_mock_json( + { + "object": "event", + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "deleted": True, + } + ) + + resp = resend.Events.remove("user.signed_up") + assert resp["deleted"] is True + + def test_events_send_with_contact_id(self) -> None: + self.set_mock_json( + { + "object": "event", + "event": "user.signed_up", + } + ) + + params: resend.Events.SendParams = { + "event": "user.signed_up", + "contact_id": "78b8d3bc-a55a-45a3-aee6-6ec0a5e13d7e", + } + resp = resend.Events.send(params) + assert resp["object"] == "event" + assert resp["event"] == "user.signed_up" + + def test_events_send_with_email(self) -> None: + self.set_mock_json( + { + "object": "event", + "event": "user.signed_up", + } + ) + + params: resend.Events.SendParams = { + "event": "user.signed_up", + "email": "user@example.com", + } + resp = resend.Events.send(params) + assert resp["event"] == "user.signed_up" + + def test_events_send_with_payload(self) -> None: + self.set_mock_json( + { + "object": "event", + "event": "user.signed_up", + } + ) + + params: resend.Events.SendParams = { + "event": "user.signed_up", + "email": "user@example.com", + "payload": {"plan": "pro", "trial_days": 14}, + } + resp = resend.Events.send(params) + assert resp["event"] == "user.signed_up" + + def test_events_list(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "name": "user.signed_up", + "schema": {"plan": "string"}, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": None, + }, + { + "id": "67372ffb-9g9c-5492-94d7-80gb8231g2dg", + "name": "user.upgraded", + "schema": None, + "created_at": "2024-02-01T00:00:00.000Z", + "updated_at": "2024-02-15T00:00:00.000Z", + }, + ], + } + ) + + events = resend.Events.list() + assert events["object"] == "list" + assert events["has_more"] is False + assert len(events["data"]) == 2 + + first = events["data"][0] + assert first["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf" + assert first["name"] == "user.signed_up" + assert first["schema"] == {"plan": "string"} + assert first["updated_at"] is None + + second = events["data"][1] + assert second["name"] == "user.upgraded" + assert second["schema"] is None + assert second["updated_at"] == "2024-02-15T00:00:00.000Z" + + def test_events_list_with_pagination_params(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": True, + "data": [ + { + "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf", + "name": "user.signed_up", + "schema": None, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": None, + } + ], + } + ) + + params: resend.Events.ListParams = { + "limit": 10, + "after": "some-cursor", + } + events = resend.Events.list(params=params) + assert events["has_more"] is True + assert len(events["data"]) == 1