diff --git a/examples/webhooks.py b/examples/webhooks.py index ee40d5a..2341b54 100644 --- a/examples/webhooks.py +++ b/examples/webhooks.py @@ -59,6 +59,26 @@ else: print("No webhooks available for pagination example") +webhook_events: resend.Webhooks.ListEventsResponse = resend.Webhooks.list_events( + webhook["id"], {"limit": 10} +) +print(f"Found {len(webhook_events['data'])} webhook events") +print(f"Has more webhook events: {webhook_events['has_more']}") +if webhook_events["data"]: + event_id = webhook_events["data"][0]["id"] + webhook_event: resend.Webhooks.GetEventResponse = resend.Webhooks.get_event( + webhook["id"], event_id + ) + print(f"Retrieved webhook event: {webhook_event['id']}") + + attempts: resend.Webhooks.ListEventAttemptsResponse = ( + resend.Webhooks.list_event_attempts(webhook["id"], event_id, {"limit": 10}) + ) + print(f"Found {len(attempts['data'])} delivery attempts") + print(f"Has more delivery attempts: {attempts['has_more']}") +else: + print("No webhook events available") + rm_webhook: resend.Webhooks.DeleteWebhookResponse = resend.Webhooks.remove( webhook_id=webhook["id"] ) diff --git a/resend/webhooks/_webhooks.py b/resend/webhooks/_webhooks.py index abbd99d..7ae4cd0 100644 --- a/resend/webhooks/_webhooks.py +++ b/resend/webhooks/_webhooks.py @@ -5,7 +5,7 @@ from hashlib import sha256 from typing import Any, Dict, List, Optional, cast -from typing_extensions import NotRequired, TypedDict +from typing_extensions import Literal, NotRequired, TypedDict from resend import request from resend._base_response import BaseResponse @@ -25,6 +25,120 @@ class Webhooks: + class ListEventsParams(TypedDict): + limit: NotRequired[int] + """ + Number of webhook events to retrieve. Maximum is 100, and minimum is 1. + """ + after: NotRequired[str] + """ + The ID after which we'll retrieve more webhook events. + """ + + class ListEventAttemptsParams(TypedDict): + limit: NotRequired[int] + """ + Number of delivery attempts to retrieve. Maximum is 100, and minimum is 1. + """ + after: NotRequired[str] + """ + The ID after which we'll retrieve more delivery attempts. + """ + + class WebhookEventSummary(TypedDict): + id: str + """ + The webhook event ID. + """ + type: WebhookEvent + """ + The webhook event type. + """ + created_at: str + """ + The date and time when the webhook event was created. + """ + status: Literal["pending", "attempting", "success", "failed"] + """ + The delivery status of the webhook event. + """ + + class ListEventsResponse(BaseResponse): + object: str + """ + The object type, always "list". + """ + has_more: bool + """ + Whether there are more webhook events available for pagination. + """ + data: List["Webhooks.WebhookEventSummary"] + """ + A list of webhook events. + """ + + class GetEventResponse(BaseResponse): + object: str + """ + The object type, always "webhook_event". + """ + id: str + """ + The webhook event ID. + """ + type: WebhookEvent + """ + The webhook event type. + """ + created_at: str + """ + The date and time when the webhook event was created. + """ + status: Literal["pending", "attempting", "success", "failed"] + """ + The delivery status of the webhook event. + """ + next_attempt_at: Optional[str] + """ + The date and time of the next delivery attempt, if one is scheduled. + """ + payload: WebhookEventPayload + """ + The webhook event payload. + """ + + class WebhookEventAttempt(TypedDict): + id: str + """ + The delivery attempt ID. + """ + http_status_code: int + """ + The HTTP status code returned by the webhook endpoint. + """ + response: str + """ + The response returned by the webhook endpoint. + """ + sent_at: str + """ + The date and time when the delivery attempt was sent. + """ + + class ListEventAttemptsResponse(BaseResponse): + object: str + """ + The object type, always "list". + """ + has_more: bool + """ + Whether there are more delivery attempts available for pagination. + """ + data: List["Webhooks.WebhookEventAttempt"] + """ + A list of webhook event delivery attempts. + """ + class ListParams(TypedDict): limit: NotRequired[int] """ @@ -245,6 +359,38 @@ def list(cls, params: Optional[ListParams] = None) -> ListResponse: ).perform_with_content() return resp + @classmethod + def list_events( + cls, webhook_id: str, params: Optional[ListEventsParams] = None + ) -> ListEventsResponse: + base_path = f"/webhooks/{webhook_id}/events" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + return request.Request[Webhooks.ListEventsResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + + @classmethod + def get_event(cls, webhook_id: str, event_id: str) -> GetEventResponse: + path = f"/webhooks/{webhook_id}/events/{event_id}" + return request.Request[Webhooks.GetEventResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + + @classmethod + def list_event_attempts( + cls, + webhook_id: str, + event_id: str, + params: Optional[ListEventAttemptsParams] = None, + ) -> ListEventAttemptsResponse: + base_path = f"/webhooks/{webhook_id}/events/{event_id}/attempts" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + return request.Request[Webhooks.ListEventAttemptsResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + @classmethod def remove(cls, webhook_id: str) -> DeleteWebhookResponse: """ @@ -438,6 +584,38 @@ async def list_async(cls, params: Optional[ListParams] = None) -> ListResponse: ).perform_with_content() return resp + @classmethod + async def list_events_async( + cls, webhook_id: str, params: Optional[ListEventsParams] = None + ) -> ListEventsResponse: + base_path = f"/webhooks/{webhook_id}/events" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + return await AsyncRequest[Webhooks.ListEventsResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + + @classmethod + async def get_event_async(cls, webhook_id: str, event_id: str) -> GetEventResponse: + path = f"/webhooks/{webhook_id}/events/{event_id}" + return await AsyncRequest[Webhooks.GetEventResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + + @classmethod + async def list_event_attempts_async( + cls, + webhook_id: str, + event_id: str, + params: Optional[ListEventAttemptsParams] = None, + ) -> ListEventAttemptsResponse: + base_path = f"/webhooks/{webhook_id}/events/{event_id}/attempts" + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path(base_path, query_params) + return await AsyncRequest[Webhooks.ListEventAttemptsResponse]( + path=path, params={}, verb="get" + ).perform_with_content() + @classmethod async def remove_async(cls, webhook_id: str) -> DeleteWebhookResponse: """ diff --git a/tests/webhooks_async_test.py b/tests/webhooks_async_test.py index 4109caf..d615b7d 100644 --- a/tests/webhooks_async_test.py +++ b/tests/webhooks_async_test.py @@ -109,6 +109,85 @@ async def test_webhooks_list_async(self) -> None: assert len(webhooks["data"]) == 1 assert webhooks["data"][0]["id"] == "wh_123" + async def test_webhooks_list_events_async(self) -> None: + response = { + "object": "list", + "has_more": True, + "data": [ + { + "id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", + "type": "email.sent", + "created_at": "2024-01-01T00:00:00.000Z", + "status": "success", + } + ], + } + self.set_mock_json(response) + params: resend.Webhooks.ListEventsParams = { + "limit": 10, + "after": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", + } + + events = await resend.Webhooks.list_events_async("wh_123", params) + + assert events == response + self.mock.assert_awaited_once_with( + url="https://api.resend.com/webhooks/wh_123/events?limit=10&after=msg_1srOrx2ZWZBpBUvZwXKQmoEYga2" + ) + + async def test_webhooks_get_event_async(self) -> None: + response = { + "object": "webhook_event", + "id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", + "type": "email.sent", + "created_at": "2024-01-01T00:00:00.000Z", + "status": "pending", + "next_attempt_at": None, + "payload": { + "type": "email.sent", + "created_at": "2024-01-01T00:00:00.000Z", + "data": {}, + }, + } + self.set_mock_json(response) + + event = await resend.Webhooks.get_event_async( + "wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2" + ) + + assert event == response + self.mock.assert_awaited_once_with( + url="https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2" + ) + + async def test_webhooks_list_event_attempts_async(self) -> None: + response = { + "object": "list", + "has_more": False, + "data": [ + { + "id": "atmpt_2ZbUCwvGmIT4mLIN6d3Yz0Ainbd", + "http_status_code": 200, + "response": "OK", + "sent_at": "2024-01-01T00:00:00.000Z", + } + ], + } + self.set_mock_json(response) + params: resend.Webhooks.ListEventAttemptsParams = { + "limit": 5, + "after": "atmpt_2ZbUCwvGmIT4mLIN6d3Yz0Ainbd", + } + + attempts = await resend.Webhooks.list_event_attempts_async( + "wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", params + ) + + assert attempts == response + self.mock.assert_awaited_once_with( + url="https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2/attempts?limit=5&after=atmpt_2ZbUCwvGmIT4mLIN6d3Yz0Ainbd" + ) + async def test_should_list_webhooks_async_raise_exception_when_no_content( self, ) -> None: diff --git a/tests/webhooks_test.py b/tests/webhooks_test.py index f82ec20..351af02 100644 --- a/tests/webhooks_test.py +++ b/tests/webhooks_test.py @@ -81,6 +81,85 @@ def test_webhooks_list(self) -> None: assert len(webhooks["data"]) == 1 assert webhooks["data"][0]["id"] == "wh_123" + def test_webhooks_list_events(self) -> None: + response = { + "object": "list", + "has_more": True, + "data": [ + { + "id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", + "type": "email.sent", + "created_at": "2024-01-01T00:00:00.000Z", + "status": "success", + } + ], + } + self.set_mock_json(response) + params: resend.Webhooks.ListEventsParams = { + "limit": 10, + "after": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", + } + + events = resend.Webhooks.list_events("wh_123", params) + + assert events == response + self.mock.assert_called_once_with( + url="https://api.resend.com/webhooks/wh_123/events?limit=10&after=msg_1srOrx2ZWZBpBUvZwXKQmoEYga2" + ) + + def test_webhooks_get_event(self) -> None: + response = { + "object": "webhook_event", + "id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", + "type": "email.sent", + "created_at": "2024-01-01T00:00:00.000Z", + "status": "pending", + "next_attempt_at": None, + "payload": { + "type": "email.sent", + "created_at": "2024-01-01T00:00:00.000Z", + "data": {}, + }, + } + self.set_mock_json(response) + + event = resend.Webhooks.get_event( + "wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2" + ) + + assert event == response + self.mock.assert_called_once_with( + url="https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2" + ) + + def test_webhooks_list_event_attempts(self) -> None: + response = { + "object": "list", + "has_more": False, + "data": [ + { + "id": "atmpt_2ZbUCwvGmIT4mLIN6d3Yz0Ainbd", + "http_status_code": 200, + "response": "OK", + "sent_at": "2024-01-01T00:00:00.000Z", + } + ], + } + self.set_mock_json(response) + params: resend.Webhooks.ListEventAttemptsParams = { + "limit": 5, + "after": "atmpt_2ZbUCwvGmIT4mLIN6d3Yz0Ainbd", + } + + attempts = resend.Webhooks.list_event_attempts( + "wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", params + ) + + assert attempts == response + self.mock.assert_called_once_with( + url="https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2/attempts?limit=5&after=atmpt_2ZbUCwvGmIT4mLIN6d3Yz0Ainbd" + ) + def test_webhooks_remove(self) -> None: delete_response = {"object": "webhook", "id": "wh_123", "deleted": True} self.set_mock_json(delete_response)