Skip to content
Open
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
3 changes: 3 additions & 0 deletions resend/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import resend
from resend.exceptions import (NoContentError, ResendError,
raise_for_code_and_type)
from resend.response import ResponseDict
from resend.version import get_version

RequestVerb = Literal["get", "post", "put", "patch", "delete"]
Expand Down Expand Up @@ -38,6 +39,8 @@ def perform(self) -> Union[T, None]:
error_type=data.get("name", "InternalServerError"),
)

if isinstance(data, dict):
data = ResponseDict(data)
return cast(T, data)

def perform_with_content(self) -> T:
Expand Down
18 changes: 18 additions & 0 deletions resend/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Any, Dict


class ResponseDict(Dict[str, Any]):
"""Dict subclass that supports attribute-style access.

This allows SDK responses to be accessed using either dict syntax
(response['data']) or attribute syntax (response.data), providing
consistency with other Resend SDKs (e.g., Node.js).
"""

def __getattr__(self, name: str) -> Any:
try:
return self[name]
except KeyError:
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
2 changes: 1 addition & 1 deletion resend/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.21.0"
__version__ = "2.22.0"


def get_version() -> str:
Expand Down
89 changes: 89 additions & 0 deletions tests/response_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import resend
from tests.conftest import ResendBaseTest

# flake8: noqa


class TestResponseDict(ResendBaseTest):

def test_list_response_supports_dict_access(self) -> None:
self.set_mock_json(
{
"object": "list",
"has_more": False,
"data": [
{
"id": "att-1",
"filename": "avatar.png",
"content_type": "image/png",
"content_disposition": "inline",
"size": 1024,
},
],
}
)

attachments = resend.Emails.Receiving.Attachments.list(
email_id="test-email-id"
)
assert attachments["object"] == "list"
assert attachments["has_more"] is False
assert len(attachments["data"]) == 1
assert attachments["data"][0]["id"] == "att-1"

def test_list_response_supports_attribute_access(self) -> None:
self.set_mock_json(
{
"object": "list",
"has_more": False,
"data": [
{
"id": "att-1",
"filename": "avatar.png",
"content_type": "image/png",
"content_disposition": "inline",
"size": 1024,
},
],
}
)

attachments = resend.Emails.Receiving.Attachments.list(
email_id="test-email-id"
)
assert attachments.object == "list" # type: ignore[attr-defined]
assert attachments.has_more is False # type: ignore[attr-defined]
assert len(attachments.data) == 1 # type: ignore[attr-defined]
assert attachments.data[0]["id"] == "att-1" # type: ignore[attr-defined]

def test_attribute_access_raises_for_missing_key(self) -> None:
self.set_mock_json(
{
"object": "list",
"has_more": False,
"data": [],
}
)

attachments = resend.Emails.Receiving.Attachments.list(
email_id="test-email-id"
)
with self.assertRaises(AttributeError):
_ = attachments.nonexistent # type: ignore[attr-defined]

def test_single_response_supports_attribute_access(self) -> None:
self.set_mock_json(
{
"id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794",
}
)

params: resend.Emails.SendParams = {
"from": "from@email.io",
"to": ["to@email.io"],
"subject": "subject",
"html": "<p>Hello</p>",
}
email = resend.Emails.send(params)
assert email["id"] == "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"
assert email.id == "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" # type: ignore[attr-defined]
Loading