Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
)
Expand Down
180 changes: 179 additions & 1 deletion resend/webhooks/_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
"""
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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:
"""
Expand Down
79 changes: 79 additions & 0 deletions tests/webhooks_async_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading