Skip to content

Commit

Permalink
Feat/generics request (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroimpulcetto authored May 30, 2024
1 parent 1214f73 commit 1367cfd
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 68 deletions.
18 changes: 9 additions & 9 deletions resend/api_keys/_api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,10 @@ def create(cls, params: CreateParams) -> ApiKey:
ApiKey: The new API key object
"""
path = "/api-keys"
return cast(
ApiKey,
request.Request(
path=path, params=cast(Dict[Any, Any], params), verb="post"
).perform(),
)
resp = request.Request[ApiKey](
path=path, params=cast(Dict[Any, Any], params), verb="post"
).perform_with_content()
return resp

@classmethod
def list(cls) -> ListResponse:
Expand All @@ -70,8 +68,10 @@ def list(cls) -> ListResponse:
ListResponse: A list of API key objects
"""
path = "/api-keys"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(_ListResponse, resp)
resp = request.Request[_ListResponse](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def remove(cls, api_key_id: str) -> None:
Expand All @@ -88,5 +88,5 @@ def remove(cls, api_key_id: str) -> None:
path = f"/api-keys/{api_key_id}"

# This would raise if failed
request.Request(path=path, params={}, verb="delete").perform()
request.Request[None](path=path, params={}, verb="delete").perform()
return None
24 changes: 15 additions & 9 deletions resend/audiences/_audiences.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ def create(cls, params: CreateParams) -> Audience:
Audience: The new audience object
"""
path = "/audiences"
resp = request.Request(
resp = request.Request[Audience](
path=path, params=cast(Dict[Any, Any], params), verb="post"
).perform()
return cast(Audience, resp)
).perform_with_content()
return resp

@classmethod
def list(cls) -> ListResponse:
Expand All @@ -58,8 +58,10 @@ def list(cls) -> ListResponse:
ListResponse: A list of audience objects
"""
path = "/audiences/"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(_ListResponse, resp)
resp = request.Request[_ListResponse](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def get(cls, id: str) -> Audience:
Expand All @@ -74,8 +76,10 @@ def get(cls, id: str) -> Audience:
Audience: The audience object
"""
path = f"/audiences/{id}"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(Audience, resp)
resp = request.Request[Audience](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def remove(cls, id: str) -> Audience:
Expand All @@ -90,5 +94,7 @@ def remove(cls, id: str) -> Audience:
Audience: The audience object
"""
path = f"/audiences/{id}"
resp = request.Request(path=path, params={}, verb="delete").perform()
return cast(Audience, resp)
resp = request.Request[Audience](
path=path, params={}, verb="delete"
).perform_with_content()
return resp
30 changes: 18 additions & 12 deletions resend/contacts/_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ def create(cls, params: CreateParams) -> Contact:
Contact: The new contact object
"""
path = f"/audiences/{params['audience_id']}/contacts"
resp = request.Request(
resp = request.Request[Contact](
path=path, params=cast(Dict[Any, Any], params), verb="post"
).perform()
return cast(Contact, resp)
).perform_with_content()
return resp

@classmethod
def update(cls, params: UpdateParams) -> Contact:
Expand All @@ -103,10 +103,10 @@ def update(cls, params: UpdateParams) -> Contact:
Contact: The updated contact object
"""
path = f"/audiences/{params['audience_id']}/contacts/{params['id']}"
resp = request.Request(
resp = request.Request[Contact](
path=path, params=cast(Dict[Any, Any], params), verb="patch"
).perform()
return cast(Contact, resp)
).perform_with_content()
return resp

@classmethod
def list(cls, audience_id: str) -> ListResponse:
Expand All @@ -121,8 +121,10 @@ def list(cls, audience_id: str) -> ListResponse:
ListResponse: A list of contact objects
"""
path = f"/audiences/{audience_id}/contacts"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(_ListResponse, resp)
resp = request.Request[_ListResponse](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def get(cls, id: str, audience_id: str) -> Contact:
Expand All @@ -138,8 +140,10 @@ def get(cls, id: str, audience_id: str) -> Contact:
Contact: The contact object
"""
path = f"/audiences/{audience_id}/contacts/{id}"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(Contact, resp)
resp = request.Request[Contact](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def remove(cls, audience_id: str, id: str = "", email: str = "") -> Contact:
Expand All @@ -160,5 +164,7 @@ def remove(cls, audience_id: str, id: str = "", email: str = "") -> Contact:
raise ValueError("id or email must be provided")
path = f"/audiences/{audience_id}/contacts/{contact}"

resp = request.Request(path=path, params={}, verb="delete").perform()
return cast(Contact, resp)
resp = request.Request[Contact](
path=path, params={}, verb="delete"
).perform_with_content()
return resp
36 changes: 22 additions & 14 deletions resend/domains/_domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ def create(cls, params: CreateParams) -> Domain:
Domain: The new domain object
"""
path = "/domains"
resp = request.Request(
resp = request.Request[Domain](
path=path, params=cast(Dict[Any, Any], params), verb="post"
).perform()
return cast(Domain, resp)
).perform_with_content()
return resp

@classmethod
def update(cls, params: UpdateParams) -> Domain:
Expand All @@ -79,10 +79,10 @@ def update(cls, params: UpdateParams) -> Domain:
Domain: The updated domain object
"""
path = f"/domains/{params['id']}"
resp = request.Request(
resp = request.Request[Domain](
path=path, params=cast(Dict[Any, Any], params), verb="patch"
).perform()
return cast(Domain, resp)
).perform_with_content()
return resp

@classmethod
def get(cls, domain_id: str) -> Domain:
Expand All @@ -97,8 +97,10 @@ def get(cls, domain_id: str) -> Domain:
Domain: The domain object
"""
path = f"/domains/{domain_id}"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(Domain, resp)
resp = request.Request[Domain](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def list(cls) -> ListResponse:
Expand All @@ -110,8 +112,10 @@ def list(cls) -> ListResponse:
ListResponse: A list of domain objects
"""
path = "/domains"
resp = request.Request(path=path, params={}, verb="get").perform()
return cast(_ListResponse, resp)
resp = request.Request[_ListResponse](
path=path, params={}, verb="get"
).perform_with_content()
return resp

@classmethod
def remove(cls, domain_id: str) -> Domain:
Expand All @@ -126,8 +130,10 @@ def remove(cls, domain_id: str) -> Domain:
Domain: The removed domain object
"""
path = f"/domains/{domain_id}"
resp = request.Request(path=path, params={}, verb="delete").perform()
return cast(Domain, resp)
resp = request.Request[Domain](
path=path, params={}, verb="delete"
).perform_with_content()
return resp

@classmethod
def verify(cls, domain_id: str) -> Domain:
Expand All @@ -142,5 +148,7 @@ def verify(cls, domain_id: str) -> Domain:
Domain: The verified domain object
"""
path = f"/domains/{domain_id}/verify"
resp = request.Request(path=path, params={}, verb="post").perform()
return cast(Domain, resp)
resp = request.Request[Domain](
path=path, params={}, verb="post"
).perform_with_content()
return resp
10 changes: 4 additions & 6 deletions resend/emails/_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ def send(cls, params: List[Emails.SendParams]) -> SendResponse:
"""
path = "/emails/batch"

return cast(
_SendResponse,
request.Request(
path=path, params=cast(List[Dict[Any, Any]], params), verb="post"
).perform(),
)
resp = request.Request[_SendResponse](
path=path, params=cast(List[Dict[Any, Any]], params), verb="post"
).perform_with_content()
return resp
20 changes: 12 additions & 8 deletions resend/emails/_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,12 @@ def send(cls, params: SendParams) -> Email:
Email: The email object that was sent
"""
path = "/emails"

return cast(
Email,
request.Request(
path=path, params=cast(Dict[Any, Any], params), verb="post"
).perform(),
)
resp = request.Request[Email](
path=path,
params=cast(Dict[Any, Any], params),
verb="post",
).perform_with_content()
return resp

@classmethod
def get(cls, email_id: str) -> Email:
Expand All @@ -113,4 +112,9 @@ def get(cls, email_id: str) -> Email:
Email: The email object that was retrieved
"""
path = f"/emails/{email_id}"
return cast(Email, request.Request(path=path, params={}, verb="get").perform())
resp = request.Request[Email](
path=path,
params={},
verb="get",
).perform_with_content()
return resp
9 changes: 9 additions & 0 deletions resend/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,12 @@ def raise_for_code_and_type(
raise ResendError(
code=code, message=message, error_type=error_type, suggested_action=""
)


class NoContentError(Exception):
"""Raised when the response body is empty."""

def __init__(self) -> None:
self.message = """No content was returned from the API.
Please contact Resend support."""
Exception.__init__(self, self.message)
31 changes: 24 additions & 7 deletions resend/request.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from typing import Any, Dict, List, Union
from typing import Any, Dict, Generic, List, Union, cast

import requests
from typing_extensions import Literal
from typing_extensions import Literal, TypeVar

import resend
from resend.exceptions import raise_for_code_and_type
from resend.exceptions import NoContentError, raise_for_code_and_type
from resend.version import get_version

RequestVerb = Literal["get", "post", "put", "patch", "delete"]

T = TypeVar("T")


# This class wraps the HTTP request creation logic
class Request:
class Request(Generic[T]):
def __init__(
self,
path: str,
Expand All @@ -22,13 +24,13 @@ def __init__(
self.params = params
self.verb = verb

def perform(self) -> Any:
def perform(self) -> Union[T, None]:
"""Is the main function that makes the HTTP request
to the Resend API. It uses the path, params, and verb attributes
to make the request.
Returns:
Dict: The JSON response from the API
Union[T, None]: A generic type of the Request class or None
Raises:
requests.HTTPError: If the request fails
Expand All @@ -47,7 +49,22 @@ def perform(self) -> Any:
message=error.get("message"),
error_type=error.get("name"),
)
return resp.json()
return cast(T, resp.json())

def perform_with_content(self) -> T:
"""
Perform an HTTP request and return the response content.
Returns:
T: The content of the response
Raises:
NoContentError: If the response content is `None`.
"""
resp = self.perform()
if resp is None:
raise NoContentError()
return resp

def __get_headers(self) -> Dict[Any, Any]:
"""get_headers returns the HTTP headers that will be
Expand Down
14 changes: 14 additions & 0 deletions tests/api_keys_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import resend
from resend.exceptions import NoContentError
from tests.conftest import ResendBaseTest

# flake8: noqa
Expand All @@ -19,6 +20,14 @@ def test_api_keys_create(self) -> None:
key: resend.ApiKey = resend.ApiKeys.create(params)
assert key["id"] == "dacf4072-4119-4d88-932f-6202748ac7c8"

def test_should_create_api_key_raise_exception_when_no_content(self) -> None:
self.set_mock_json(None)
params: resend.ApiKeys.CreateParams = {
"name": "prod",
}
with self.assertRaises(NoContentError):
_ = resend.ApiKeys.create(params)

def test_api_keys_list(self) -> None:
self.set_mock_json(
{
Expand All @@ -38,6 +47,11 @@ def test_api_keys_list(self) -> None:
assert key["name"] == "Production"
assert key["created_at"] == "2023-04-08T00:11:13.110779+00:00"

def test_should_list_api_key_raise_exception_when_no_content(self) -> None:
self.set_mock_json(None)
with self.assertRaises(NoContentError):
_ = resend.ApiKeys.list()

def test_api_keys_remove(self) -> None:
self.set_mock_text("")

Expand Down
Loading

0 comments on commit 1367cfd

Please sign in to comment.