From fccc64afae6c0890019dbd18f6c851502f13d994 Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Thu, 28 Dec 2023 14:50:01 -0300 Subject: [PATCH] feat: Impl contacts API (#60) --- examples/batch_email_send.py | 2 +- examples/contacts.py | 41 +++++++++ resend/__init__.py | 2 + resend/contacts.py | 37 ++++++++ resend/exceptions.py | 30 +++++- resend/request.py | 1 - tests/contacts_test.py | 172 +++++++++++++++++++++++++++++++++++ 7 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 examples/contacts.py create mode 100644 resend/contacts.py create mode 100644 tests/contacts_test.py diff --git a/examples/batch_email_send.py b/examples/batch_email_send.py index 309639d..f271df9 100644 --- a/examples/batch_email_send.py +++ b/examples/batch_email_send.py @@ -22,4 +22,4 @@ ] emails = resend.Batch.send(params) -print(emails) \ No newline at end of file +print(emails) diff --git a/examples/contacts.py b/examples/contacts.py new file mode 100644 index 0000000..0133608 --- /dev/null +++ b/examples/contacts.py @@ -0,0 +1,41 @@ +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + + +audience_id = "78b8d3bc-a55a-45a3-aee6-6ec0a5e13d7e" + +contact = resend.Contacts.create( + { + "audience_id": audience_id, + "email": "sw@example.com", + "first_name": "Steve", + "last_name": "Wozniak", + "unsubscribed": True, + } +) +print("created contact !") +print(contact) + +update_params = { + "audience_id": audience_id, + "id": contact["id"], + "last_name": "Updated", + "unsubscribed": False, +} + +updated = resend.Contacts.update(update_params) +print("updated contact !") +print(updated) + +cont = resend.Contacts.get(audience_id=audience_id, id=contact["id"]) +print(cont) + +contacts = resend.Contacts.list(audience_id=audience_id) +print(contacts) + +rmed = resend.Contacts.remove(audience_id=audience_id, id=contact["id"]) +print(rmed) diff --git a/resend/__init__.py b/resend/__init__.py index eeaa0b3..b845d5d 100644 --- a/resend/__init__.py +++ b/resend/__init__.py @@ -4,6 +4,7 @@ from .api_keys import ApiKeys from .audiences import Audiences from .batch import Batch +from .contacts import Contacts from .domains import Domains from .emails import Emails from .request import Request @@ -25,4 +26,5 @@ "Domains", "Batch", "Audiences", + "Contacts" ] diff --git a/resend/contacts.py b/resend/contacts.py new file mode 100644 index 0000000..0e21dbc --- /dev/null +++ b/resend/contacts.py @@ -0,0 +1,37 @@ +from typing import Dict + +from resend import request + + +class Contacts: + """Contacts API Wrapper""" + + @classmethod + # https://resend.com/docs/api-reference/contacts/create-contact + def create(cls, params={}) -> Dict: + path = f"/audiences/{params['audience_id']}/contacts" + return request.Request(path=path, params=params, verb="post").perform() + + @classmethod + # https://resend.com/docs/api-reference/contacts/update-contact + def update(cls, params={}) -> Dict: + path = f"/audiences/{params['audience_id']}/contacts/{params['id']}" + return request.Request(path=path, params=params, verb="patch").perform() + + @classmethod + # https://resend.com/docs/api-reference/audiences/list-audiences + def list(cls, audience_id) -> Dict: + path = f"/audiences/#{audience_id}/contacts" + return request.Request(path=path, params={}, verb="get").perform() + + @classmethod + # https://resend.com/docs/api-reference/audiences/get-audience + def get(cls, audience_id, id) -> Dict: + path = f"/audiences/{audience_id}/contacts/{id}" + return request.Request(path=path, params={}, verb="get").perform() + + @classmethod + # https://resend.com/docs/api-reference/audiences/delete-audience + def remove(cls, audience_id, id) -> Dict: + path = f"/audiences/{id}/contacts/{id}" + return request.Request(path=path, params={}, verb="delete").perform() diff --git a/resend/exceptions.py b/resend/exceptions.py index 94de6ac..18c44c0 100644 --- a/resend/exceptions.py +++ b/resend/exceptions.py @@ -120,7 +120,7 @@ def __init__( suggested_action = """Check the error message to see the list of missing fields.""" - if message != "": + if message == "": message = default_message ResendError.__init__( @@ -132,11 +132,38 @@ def __init__( ) +class ApplicationError(ResendError): + """see https://resend.com/docs/errors""" + + def __init__( + self, + message, + error_type, + code, + ): + default_message = """ + Something went wrong.""" + + suggested_action = """Contact Resend support.""" + + if message == "": + message = default_message + + ResendError.__init__( + self, + code=code or 500, + message=message, + suggested_action=suggested_action, + error_type=error_type, + ) + + ERRORS: Dict[str, Dict[str, ResendError]] = { "400": {"validation_error": ValidationError}, "422": {"missing_required_fields": MissingRequiredFieldsError}, "401": {"missing_api_key": MissingApiKeyError}, "403": {"invalid_api_key": InvalidApiKeyError}, + "500": {"application_error": ApplicationError}, } @@ -149,4 +176,5 @@ def raise_for_code_and_type(code, error_type, message: str) -> ResendError: # Raise error from errors list error: ResendError = error.get(error_type) + raise error(code=code, message=message, error_type=error_type) diff --git a/resend/request.py b/resend/request.py index 809fd18..6ef39c5 100644 --- a/resend/request.py +++ b/resend/request.py @@ -30,7 +30,6 @@ def perform(self): message=error.get("message"), error_type=error.get("name"), ) - return resp.json() def __get_headers(self) -> Dict: diff --git a/tests/contacts_test.py b/tests/contacts_test.py new file mode 100644 index 0000000..0a4b3b4 --- /dev/null +++ b/tests/contacts_test.py @@ -0,0 +1,172 @@ +import unittest +from unittest.mock import MagicMock, patch + +import resend + +# flake8: noqa + + +class TestResendContacts(unittest.TestCase): + def test_contacts_create(self): + resend.api_key = "re_123" + + patcher = patch("resend.Request.make_request") + mock = patcher.start() + mock.status_code = 200 + m = MagicMock() + m.status_code = 200 + + def mock_json(): + return {"object": "contact", "id": "479e3145-dd38-476b-932c-529ceb705947"} + + m.json = mock_json + mock.return_value = m + + params = { + "audience_id": "48c269ed-9873-4d60-bdd9-cd7e6fc0b9b8", + "email": "steve.wozniak@gmail.com", + "first_name": "Steve", + "last_name": "Wozniak", + "unsubscribed": True, + } + contact = resend.Contacts.create(params) + assert contact["id"] == "479e3145-dd38-476b-932c-529ceb705947" + assert contact["object"] == "contact" + + patcher.stop() + + def test_contacts_update(self): + resend.api_key = "re_123" + + patcher = patch("resend.Request.make_request") + mock = patcher.start() + mock.status_code = 200 + m = MagicMock() + m.status_code = 200 + + def mock_json(): + return { + "object": "contact", + "id": "479e3145-dd38-476b-932c-529ceb705947", + } + + m.json = mock_json + mock.return_value = m + + params = { + "audience_id": "48c269ed-9873-4d60-bdd9-cd7e6fc0b9b8", + "id": "479e3145-dd38-476b-932c-529ceb705947", + "first_name": "Updated", + "unsubscribed": True, + } + contact = resend.Contacts.update(params) + assert contact["id"] == "479e3145-dd38-476b-932c-529ceb705947" + assert contact["object"] == "contact" + + patcher.stop() + + def test_contacts_get(self): + resend.api_key = "re_123" + + patcher = patch("resend.Request.make_request") + mock = patcher.start() + mock.status_code = 200 + m = MagicMock() + m.status_code = 200 + + def mock_json(): + return { + "object": "contact", + "id": "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email": "steve.wozniak@gmail.com", + "first_name": "Steve", + "last_name": "Wozniak", + "created_at": "2023-10-06T23:47:56.678Z", + "unsubscribed": False, + } + + m.json = mock_json + mock.return_value = m + + contact = resend.Contacts.get( + audience_id="48c269ed-9873-4d60-bdd9-cd7e6fc0b9b8", + id="e169aa45-1ecf-4183-9955-b1499d5701d3", + ) + assert contact["object"] == "contact" + assert contact["id"] == "e169aa45-1ecf-4183-9955-b1499d5701d3" + assert contact["email"] == "steve.wozniak@gmail.com" + assert contact["first_name"] == "Steve" + assert contact["last_name"] == "Wozniak" + assert contact["created_at"] == "2023-10-06T23:47:56.678Z" + assert contact["unsubscribed"] is False + + patcher.stop() + + def test_contacts_remove(self): + resend.api_key = "re_123" + + patcher = patch("resend.Request.make_request") + mock = patcher.start() + mock.status_code = 200 + m = MagicMock() + m.status_code = 200 + + def mock_json(): + return { + "object": "contact", + "id": "520784e2-887d-4c25-b53c-4ad46ad38100", + "deleted": True, + } + + m.json = mock_json + mock.return_value = m + + rmed = resend.Contacts.remove( + audience_id="48c269ed-9873-4d60-bdd9-cd7e6fc0b9b8", + id="78261eea-8f8b-4381-83c6-79fa7120f1cf", + ) + assert rmed["object"] == "contact" + assert rmed["id"] == "520784e2-887d-4c25-b53c-4ad46ad38100" + assert rmed["deleted"] is True + + patcher.stop() + + def test_contacts_list(self): + resend.api_key = "re_123" + + patcher = patch("resend.Request.make_request") + mock = patcher.start() + mock.status_code = 200 + m = MagicMock() + m.status_code = 200 + + def mock_json(): + return { + "object": "list", + "data": [ + { + "id": "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email": "steve.wozniak@gmail.com", + "first_name": "Steve", + "last_name": "Wozniak", + "created_at": "2023-10-06T23:47:56.678Z", + "unsubscribed": False, + } + ], + } + + m.json = mock_json + mock.return_value = m + + contacts = resend.Contacts.list( + audience_id="48c269ed-9873-4d60-bdd9-cd7e6fc0b9b8" + ) + assert contacts["object"] == "list" + assert contacts["data"][0]["id"] == "e169aa45-1ecf-4183-9955-b1499d5701d3" + assert contacts["data"][0]["email"] == "steve.wozniak@gmail.com" + assert contacts["data"][0]["first_name"] == "Steve" + assert contacts["data"][0]["last_name"] == "Wozniak" + assert contacts["data"][0]["created_at"] == "2023-10-06T23:47:56.678Z" + assert contacts["data"][0]["unsubscribed"] is False + + patcher.stop()