From 6880d7ccc69d754c6e958bfb5d8b4c93a3ff3973 Mon Sep 17 00:00:00 2001 From: drish Date: Tue, 16 Jun 2026 17:16:49 -0300 Subject: [PATCH 01/16] feat: add contact imports endpoints --- resend/__init__.py | 5 + resend/async_request.py | 6 + resend/contacts/__init__.py | 5 + resend/contacts/_contacts.py | 2 + resend/contacts/imports/__init__.py | 4 + resend/contacts/imports/_contact_import.py | 41 ++++ resend/contacts/imports/_contact_imports.py | 257 ++++++++++++++++++++ resend/http_client.py | 4 +- resend/http_client_async.py | 4 +- resend/http_client_httpx.py | 25 +- resend/http_client_requests.py | 28 ++- resend/request.py | 6 + tests/contact_imports_test.py | 105 ++++++++ 13 files changed, 475 insertions(+), 17 deletions(-) create mode 100644 resend/contacts/imports/__init__.py create mode 100644 resend/contacts/imports/_contact_import.py create mode 100644 resend/contacts/imports/_contact_imports.py create mode 100644 tests/contact_imports_test.py diff --git a/resend/__init__.py b/resend/__init__.py index 32e3fa7..7b4b4e8 100644 --- a/resend/__init__.py +++ b/resend/__init__.py @@ -22,6 +22,8 @@ from .contacts._contact_topic import ContactTopic, TopicSubscriptionUpdate from .contacts._contacts import Contacts from .contacts._topics import Topics as ContactsTopics +from .contacts.imports._contact_import import ContactImport, ContactImportCounts +from .contacts.imports._contact_imports import ContactImports from .contacts.segments._contact_segment import ContactSegment from .contacts.segments._contact_segments import ContactSegments from .domains._domain import Domain @@ -83,6 +85,7 @@ "Audiences", "Automations", "Contacts", + "ContactImports", "ContactProperties", "Broadcasts", "Events", @@ -110,6 +113,8 @@ "EventSchema", "EventSchemaFieldType", "Contact", + "ContactImport", + "ContactImportCounts", "ContactSegment", "ContactSegments", "ContactProperty", diff --git a/resend/async_request.py b/resend/async_request.py index ad8dcd6..580dd24 100644 --- a/resend/async_request.py +++ b/resend/async_request.py @@ -24,11 +24,15 @@ def __init__( params: ParamsType, verb: RequestVerb, options: Optional[Dict[str, Any]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ): self.path = path self.params = params self.verb = verb self.options = options + self.files = files + self.data = data self._response_headers: Dict[str, str] = {} async def perform(self) -> Union[T, None]: @@ -102,6 +106,8 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: url=url, headers=headers, json=json_params, + files=self.files, + data=self.data, ) # Safety net around the HTTP Client diff --git a/resend/contacts/__init__.py b/resend/contacts/__init__.py index e69de29..e006a7c 100644 --- a/resend/contacts/__init__.py +++ b/resend/contacts/__init__.py @@ -0,0 +1,5 @@ +from .imports._contact_import import ContactImport as ContactImportObj +from .imports._contact_import import ContactImportCounts +from .imports._contact_imports import ContactImports + +__all__ = ["ContactImports", "ContactImportObj", "ContactImportCounts"] diff --git a/resend/contacts/_contacts.py b/resend/contacts/_contacts.py index 3650a6c..5be40fd 100644 --- a/resend/contacts/_contacts.py +++ b/resend/contacts/_contacts.py @@ -8,6 +8,7 @@ from ._contact import Contact from ._topics import Topics +from .imports._contact_imports import ContactImports from .segments._contact_segments import ContactSegments # Async imports (optional - only available with pip install resend[async]) @@ -21,6 +22,7 @@ class Contacts: # Sub-API for managing contact-segment associations Segments = ContactSegments Topics = Topics + Imports = ContactImports class RemoveContactResponse(BaseResponse): """ diff --git a/resend/contacts/imports/__init__.py b/resend/contacts/imports/__init__.py new file mode 100644 index 0000000..2229463 --- /dev/null +++ b/resend/contacts/imports/__init__.py @@ -0,0 +1,4 @@ +from ._contact_import import ContactImport, ContactImportCounts +from ._contact_imports import ContactImports + +__all__ = ["ContactImports", "ContactImport", "ContactImportCounts"] diff --git a/resend/contacts/imports/_contact_import.py b/resend/contacts/imports/_contact_import.py new file mode 100644 index 0000000..27d29e9 --- /dev/null +++ b/resend/contacts/imports/_contact_import.py @@ -0,0 +1,41 @@ +from typing import Optional + +from resend._base_response import BaseResponse + + +class ContactImportCounts(BaseResponse): + """ + ContactImportCounts holds row-level statistics for a contact import. + + Attributes: + total (int): Total number of rows processed. + created (int): Number of contacts created. + updated (int): Number of contacts updated. + skipped (int): Number of rows skipped. + failed (int): Number of rows that failed. + """ + + total: int + created: int + updated: int + skipped: int + failed: int + + +class ContactImport(BaseResponse): + """ + ContactImport represents a contact import job. + + Attributes: + object (str): Always 'contact_import'. + id (str): Unique identifier for the contact import. + status (str): 'queued', 'in_progress', 'completed', or 'failed'. + created_at (str): ISO 8601 timestamp of when the import was created. + counts (ContactImportCounts): Row-level import statistics (present when status is completed or failed). + """ + + object: str + id: str + status: str + created_at: str + counts: Optional[ContactImportCounts] diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py new file mode 100644 index 0000000..b7394f8 --- /dev/null +++ b/resend/contacts/imports/_contact_imports.py @@ -0,0 +1,257 @@ +import json as json_lib +from typing import Any, Dict, List, Optional, cast + +from typing_extensions import Literal, NotRequired, TypedDict + +from resend import request +from resend._base_response import BaseResponse +from resend.pagination_helper import PaginationHelper + +from ._contact_import import ContactImport + +try: + from resend.async_request import AsyncRequest +except ImportError: + pass + + +class ContactImports: + class CreateContactImportResponse(BaseResponse): + """ + CreateContactImportResponse wraps the response from a successful import creation. + + Attributes: + object (str): Always 'contact_import'. + id (str): Unique identifier for the created import job. + """ + + object: str + id: str + + class ListContactImportsResponse(BaseResponse): + """ + ListContactImportsResponse wraps a paginated list of contact imports. + + Attributes: + object (str): Always 'list'. + data (List[ContactImport]): The list of import objects. + has_more (bool): Whether additional pages of results exist. + """ + + object: str + data: List[ContactImport] + has_more: bool + + class CreateParams(TypedDict): + file: bytes + """ + CSV file content to import. Maximum size is 50MB. (required) + """ + filename: NotRequired[str] + """ + Filename used in the multipart upload. Defaults to 'import.csv'. + """ + column_map: NotRequired[Dict[str, Any]] + """ + Maps contact fields and custom property keys to CSV column names. + Will be JSON-encoded before sending. + Example: {"email": "Email", "first_name": "First Name"} + """ + on_conflict: NotRequired[Literal["upsert", "skip"]] + """ + Strategy when an imported contact already exists: 'upsert' or 'skip' (default 'skip'). + """ + segments: NotRequired[List[str]] + """ + List of segment IDs to add imported contacts to. + Will be serialized as [{"id": "..."}] before sending. + """ + + class ListParams(TypedDict): + status: NotRequired[Literal["queued", "in_progress", "completed", "failed"]] + """ + Filter imports by status. + """ + limit: NotRequired[int] + """ + Number of imports to retrieve. Maximum is 100. + """ + after: NotRequired[str] + """ + Cursor for forward pagination (exclusive). + """ + before: NotRequired[str] + """ + Cursor for backward pagination (exclusive). + """ + + @classmethod + def create(cls, params: CreateParams) -> CreateContactImportResponse: + """ + Create a new contact import from a CSV file. + see more: https://resend.com/docs/api-reference/contacts/create-contact-import + + Args: + params (CreateParams): Import parameters including the CSV file content. + + Returns: + CreateContactImportResponse: The created import job with its ID. + """ + if not params.get("file"): + raise ValueError("file is required") + + filename = params.get("filename", "import.csv") + files: Dict[str, Any] = { + "file": (filename, params["file"], "text/csv"), + } + form_data: Dict[str, str] = {} + if "column_map" in params: + form_data["column_map"] = json_lib.dumps(params["column_map"]) + if "on_conflict" in params: + form_data["on_conflict"] = params["on_conflict"] # type: ignore[assignment] + if "segments" in params: + form_data["segments"] = json_lib.dumps( + [{"id": sid} for sid in params["segments"]] # type: ignore[union-attr] + ) + + resp = request.Request[ContactImports.CreateContactImportResponse]( + path="/contacts/imports", + params={}, + verb="post", + files=files, + data=form_data, + ).perform_with_content() + return resp + + @classmethod + def get(cls, id: str) -> ContactImport: + """ + Retrieve a single contact import by ID. + see more: https://resend.com/docs/api-reference/contacts/get-contact-import + + Args: + id (str): The contact import ID. + + Returns: + ContactImport: The contact import object. + """ + if not id: + raise ValueError("id is required") + + resp = request.Request[ContactImport]( + path=f"/contacts/imports/{id}", + params={}, + verb="get", + ).perform_with_content() + return resp + + @classmethod + def list(cls, params: Optional[ListParams] = None) -> ListContactImportsResponse: + """ + Retrieve a list of contact imports. + see more: https://resend.com/docs/api-reference/contacts/list-contact-imports + + Args: + params (Optional[ListParams]): Optional filtering and pagination parameters. + + Returns: + ListContactImportsResponse: Paginated list of contact import objects. + """ + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path("/contacts/imports", query_params) + # Append status filter if provided (PaginationHelper only handles limit/after/before) + if params and "status" in params: + separator = "&" if "?" in path else "?" + path = f"{path}{separator}status={params['status']}" + + resp = request.Request[ContactImports.ListContactImportsResponse]( + path=path, + params={}, + verb="get", + ).perform_with_content() + return resp + + @classmethod + async def create_async(cls, params: CreateParams) -> CreateContactImportResponse: + """ + Create a new contact import from a CSV file (async). + see more: https://resend.com/docs/api-reference/contacts/create-contact-import + + Args: + params (CreateParams): Import parameters including the CSV file content. + + Returns: + CreateContactImportResponse: The created import job with its ID. + """ + if not params.get("file"): + raise ValueError("file is required") + + filename = params.get("filename", "import.csv") + files: Dict[str, Any] = { + "file": (filename, params["file"], "text/csv"), + } + form_data: Dict[str, str] = {} + if "column_map" in params: + form_data["column_map"] = json_lib.dumps(params["column_map"]) + if "on_conflict" in params: + form_data["on_conflict"] = params["on_conflict"] # type: ignore[assignment] + if "segments" in params: + form_data["segments"] = json_lib.dumps( + [{"id": sid} for sid in params["segments"]] # type: ignore[union-attr] + ) + + resp = await AsyncRequest[ContactImports.CreateContactImportResponse]( + path="/contacts/imports", + params={}, + verb="post", + files=files, + data=form_data, + ).perform_with_content() + return resp + + @classmethod + async def get_async(cls, id: str) -> ContactImport: + """ + Retrieve a single contact import by ID (async). + see more: https://resend.com/docs/api-reference/contacts/get-contact-import + + Args: + id (str): The contact import ID. + + Returns: + ContactImport: The contact import object. + """ + if not id: + raise ValueError("id is required") + + resp = await AsyncRequest[ContactImport]( + path=f"/contacts/imports/{id}", + params={}, + verb="get", + ).perform_with_content() + return resp + + @classmethod + async def list_async(cls, params: Optional[ListParams] = None) -> ListContactImportsResponse: + """ + Retrieve a list of contact imports (async). + see more: https://resend.com/docs/api-reference/contacts/list-contact-imports + + Args: + params (Optional[ListParams]): Optional filtering and pagination parameters. + + Returns: + ListContactImportsResponse: Paginated list of contact import objects. + """ + query_params = cast(Dict[Any, Any], params) if params else None + path = PaginationHelper.build_paginated_path("/contacts/imports", query_params) + if params and "status" in params: + separator = "&" if "?" in path else "?" + path = f"{path}{separator}status={params['status']}" + + resp = await AsyncRequest[ContactImports.ListContactImportsResponse]( + path=path, + params={}, + verb="get", + ).perform_with_content() + return resp diff --git a/resend/http_client.py b/resend/http_client.py index 0863420..ceb5f1d 100644 --- a/resend/http_client.py +++ b/resend/http_client.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, List, Mapping, Optional, Tuple, Union +from typing import Any, Dict, List, Mapping, Optional, Tuple, Union class HTTPClient(ABC): @@ -16,5 +16,7 @@ def request( url: str, headers: Mapping[str, str], json: Optional[Union[Dict[str, object], List[object]]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ) -> Tuple[bytes, int, Mapping[str, str]]: pass diff --git a/resend/http_client_async.py b/resend/http_client_async.py index 4df324d..b7eedf0 100644 --- a/resend/http_client_async.py +++ b/resend/http_client_async.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, List, Mapping, Optional, Tuple, Union +from typing import Any, Dict, List, Mapping, Optional, Tuple, Union class AsyncHTTPClient(ABC): @@ -16,5 +16,7 @@ async def request( url: str, headers: Mapping[str, str], json: Optional[Union[Dict[str, object], List[object]]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ) -> Tuple[bytes, int, Mapping[str, str]]: pass diff --git a/resend/http_client_httpx.py b/resend/http_client_httpx.py index 8ff2373..3c1f38b 100644 --- a/resend/http_client_httpx.py +++ b/resend/http_client_httpx.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Mapping, Optional, Tuple, Union +from typing import Any, Dict, List, Mapping, Optional, Tuple, Union import httpx @@ -19,15 +19,26 @@ async def request( url: str, headers: Mapping[str, str], json: Optional[Union[Dict[str, object], List[object]]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ) -> Tuple[bytes, int, Mapping[str, str]]: try: async with httpx.AsyncClient(timeout=self._timeout) as client: - resp = await client.request( - method=method, - url=url, - headers=headers, - json=json, - ) + if files is not None: + resp = await client.request( + method=method, + url=url, + headers=headers, + files=files, + data=data, + ) + else: + resp = await client.request( + method=method, + url=url, + headers=headers, + json=json, + ) return resp.content, resp.status_code, resp.headers except httpx.RequestError as e: # This gets caught by the async request.perform() method diff --git a/resend/http_client_requests.py b/resend/http_client_requests.py index 6c78a2c..22c09c6 100644 --- a/resend/http_client_requests.py +++ b/resend/http_client_requests.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Mapping, Optional, Tuple, Union +from typing import Any, Dict, List, Mapping, Optional, Tuple, Union import requests @@ -19,15 +19,27 @@ def request( url: str, headers: Mapping[str, str], json: Optional[Union[Dict[str, object], List[object]]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ) -> Tuple[bytes, int, Mapping[str, str]]: try: - resp = requests.request( - method=method, - url=url, - headers=headers, - json=json, - timeout=self._timeout, - ) + if files is not None: + resp = requests.request( + method=method, + url=url, + headers=headers, + files=files, + data=data, + timeout=self._timeout, + ) + else: + resp = requests.request( + method=method, + url=url, + headers=headers, + json=json, + timeout=self._timeout, + ) return resp.content, resp.status_code, resp.headers except requests.RequestException as e: # This gets caught by the request.perform() method diff --git a/resend/request.py b/resend/request.py index 7b4fa74..d3efa99 100644 --- a/resend/request.py +++ b/resend/request.py @@ -23,11 +23,15 @@ def __init__( params: ParamsType, verb: RequestVerb, options: Optional[Dict[str, Any]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ): self.path = path self.params = params self.verb = verb self.options = options + self.files = files + self.data = data self._response_headers: Dict[str, str] = {} def perform(self) -> Union[T, None]: @@ -89,6 +93,8 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: url=url, headers=headers, json=json_params, + files=self.files, + data=self.data, ) # Safety net around the HTTP Client diff --git a/tests/contact_imports_test.py b/tests/contact_imports_test.py new file mode 100644 index 0000000..792d443 --- /dev/null +++ b/tests/contact_imports_test.py @@ -0,0 +1,105 @@ +import resend +from tests.conftest import ResendBaseTest + +# flake8: noqa + + +class TestContactImports(ResendBaseTest): + def test_create_contact_import(self) -> None: + self.set_mock_json( + {"object": "contact_import", "id": "479e3145-dd38-476b-932c-529ceb705947"} + ) + + params: resend.ContactImports.CreateParams = { + "file": b"email,first_name\nsteve@example.com,Steve", + } + resp: resend.ContactImports.CreateContactImportResponse = resend.Contacts.Imports.create(params) + assert resp["id"] == "479e3145-dd38-476b-932c-529ceb705947" + assert resp["object"] == "contact_import" + + def test_create_contact_import_with_options(self) -> None: + self.set_mock_json( + {"object": "contact_import", "id": "479e3145-dd38-476b-932c-529ceb705947"} + ) + + params: resend.ContactImports.CreateParams = { + "file": b"email,first_name\nsteve@example.com,Steve", + "filename": "contacts.csv", + "on_conflict": "upsert", + "column_map": {"email": "email", "first_name": "first_name"}, + "segments": ["seg-123"], + } + resp = resend.Contacts.Imports.create(params) + assert resp["id"] == "479e3145-dd38-476b-932c-529ceb705947" + + def test_create_contact_import_missing_file(self) -> None: + try: + resend.Contacts.Imports.create({"file": b""}) + except ValueError as e: + assert str(e) == "file is required" + + def test_get_contact_import(self) -> None: + self.set_mock_json( + { + "object": "contact_import", + "id": "479e3145-dd38-476b-932c-529ceb705947", + "status": "completed", + "created_at": "2023-10-06T23:47:56.678Z", + "counts": { + "total": 100, + "created": 80, + "updated": 10, + "skipped": 5, + "failed": 5, + }, + } + ) + + result: resend.ContactImport = resend.Contacts.Imports.get( + "479e3145-dd38-476b-932c-529ceb705947" + ) + assert result["id"] == "479e3145-dd38-476b-932c-529ceb705947" + assert result["status"] == "completed" + assert result["counts"]["total"] == 100 + + def test_get_contact_import_missing_id(self) -> None: + try: + resend.Contacts.Imports.get("") + except ValueError as e: + assert str(e) == "id is required" + + def test_list_contact_imports(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [ + { + "object": "contact_import", + "id": "479e3145-dd38-476b-932c-529ceb705947", + "status": "completed", + "created_at": "2023-10-06T23:47:56.678Z", + } + ], + } + ) + + result: resend.ContactImports.ListContactImportsResponse = resend.Contacts.Imports.list() + assert result["object"] == "list" + assert result["has_more"] is False + assert len(result["data"]) == 1 + assert result["data"][0]["id"] == "479e3145-dd38-476b-932c-529ceb705947" + + def test_list_contact_imports_with_filters(self) -> None: + self.set_mock_json( + { + "object": "list", + "has_more": False, + "data": [], + } + ) + + result = resend.Contacts.Imports.list( + {"status": "completed", "limit": 10} + ) + assert result["object"] == "list" From 2f4c8deb2e619f69c3ed9d1abf59c3deb18fccdb Mon Sep 17 00:00:00 2001 From: drish Date: Tue, 16 Jun 2026 17:22:35 -0300 Subject: [PATCH 02/16] fix: mypy type errors in contact imports --- examples/with_custom_http_client.py | 2 ++ resend/contacts/imports/_contact_imports.py | 8 ++++---- tests/contact_imports_test.py | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/with_custom_http_client.py b/examples/with_custom_http_client.py index 71f980c..0f2fa62 100644 --- a/examples/with_custom_http_client.py +++ b/examples/with_custom_http_client.py @@ -21,6 +21,8 @@ def request( url: str, headers: Mapping[str, str], json: Optional[Union[Dict[str, Any], List[Any]]] = None, + files: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, str]] = None, ) -> Tuple[bytes, int, Dict[str, str]]: print(f"[HTTP] {method.upper()} {url} with timeout={self.timeout}") try: diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index b7394f8..192e475 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -108,10 +108,10 @@ def create(cls, params: CreateParams) -> CreateContactImportResponse: if "column_map" in params: form_data["column_map"] = json_lib.dumps(params["column_map"]) if "on_conflict" in params: - form_data["on_conflict"] = params["on_conflict"] # type: ignore[assignment] + form_data["on_conflict"] = params["on_conflict"] if "segments" in params: form_data["segments"] = json_lib.dumps( - [{"id": sid} for sid in params["segments"]] # type: ignore[union-attr] + [{"id": sid} for sid in params["segments"]] ) resp = request.Request[ContactImports.CreateContactImportResponse]( @@ -194,10 +194,10 @@ async def create_async(cls, params: CreateParams) -> CreateContactImportResponse if "column_map" in params: form_data["column_map"] = json_lib.dumps(params["column_map"]) if "on_conflict" in params: - form_data["on_conflict"] = params["on_conflict"] # type: ignore[assignment] + form_data["on_conflict"] = params["on_conflict"] if "segments" in params: form_data["segments"] = json_lib.dumps( - [{"id": sid} for sid in params["segments"]] # type: ignore[union-attr] + [{"id": sid} for sid in params["segments"]] ) resp = await AsyncRequest[ContactImports.CreateContactImportResponse]( diff --git a/tests/contact_imports_test.py b/tests/contact_imports_test.py index 792d443..db49ab4 100644 --- a/tests/contact_imports_test.py +++ b/tests/contact_imports_test.py @@ -60,7 +60,9 @@ def test_get_contact_import(self) -> None: ) assert result["id"] == "479e3145-dd38-476b-932c-529ceb705947" assert result["status"] == "completed" - assert result["counts"]["total"] == 100 + counts = result["counts"] + assert counts is not None + assert counts["total"] == 100 def test_get_contact_import_missing_id(self) -> None: try: From d0ce8d6c0d205221d655a13ca453021054d42adb Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 11:49:04 -0300 Subject: [PATCH 03/16] feat: add contact imports example --- examples/contact_imports.py | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 examples/contact_imports.py diff --git a/examples/contact_imports.py b/examples/contact_imports.py new file mode 100644 index 0000000..ac02b64 --- /dev/null +++ b/examples/contact_imports.py @@ -0,0 +1,43 @@ +import os + +import resend + +if not os.environ["RESEND_API_KEY"]: + raise EnvironmentError("RESEND_API_KEY is missing") + +file_content = b"Email,First Name,Last Name,Plan\nsteve@example.com,Steve,Wozniak,pro" + +create_params: resend.ContactImports.CreateParams = { + "file": file_content, + "column_map": { + "email": "Email", + "first_name": "First Name", + "last_name": "Last Name", + "properties": { + "plan": { + "column": "Plan", + "type": "string", + }, + }, + }, + "on_conflict": "upsert", + "segments": ["60a2ac5e-0774-456e-817d-ebf40f6dba31"], +} + +import_response: resend.ContactImports.CreateContactImportResponse = ( + resend.Contacts.Imports.create(create_params) +) +print("Created contact import with ID: {}".format(import_response["id"])) +print(import_response) + +contact_import: resend.ContactImport = resend.Contacts.Imports.get(import_response["id"]) +print("Retrieved contact import") +print(contact_import) + +list_response: resend.ContactImports.ListContactImportsResponse = ( + resend.Contacts.Imports.list() +) +print(f"Found {len(list_response['data'])} imports") +print(f"Has more: {list_response['has_more']}") +for item in list_response["data"]: + print(item) From e200bb3dd746871431f19514a36c21b42b824571 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:06:18 -0300 Subject: [PATCH 04/16] feat: add topics to CreateParams and use contacts.csv in example --- examples/contact_imports.py | 10 +++++++++- examples/contacts.csv | 2 ++ resend/contacts/imports/_contact_imports.py | 9 +++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 examples/contacts.csv diff --git a/examples/contact_imports.py b/examples/contact_imports.py index ac02b64..1443ec9 100644 --- a/examples/contact_imports.py +++ b/examples/contact_imports.py @@ -5,7 +5,9 @@ if not os.environ["RESEND_API_KEY"]: raise EnvironmentError("RESEND_API_KEY is missing") -file_content = b"Email,First Name,Last Name,Plan\nsteve@example.com,Steve,Wozniak,pro" +csv_path = os.path.join(os.path.dirname(__file__), "contacts.csv") +with open(csv_path, "rb") as f: + file_content = f.read() create_params: resend.ContactImports.CreateParams = { "file": file_content, @@ -22,6 +24,12 @@ }, "on_conflict": "upsert", "segments": ["60a2ac5e-0774-456e-817d-ebf40f6dba31"], + "topics": [ + { + "id": "059ac693-2fc8-4c13-8b27-01350d638a17", + "subscription": "opt_in", + }, + ], } import_response: resend.ContactImports.CreateContactImportResponse = ( diff --git a/examples/contacts.csv b/examples/contacts.csv new file mode 100644 index 0000000..798dbf4 --- /dev/null +++ b/examples/contacts.csv @@ -0,0 +1,2 @@ +Email,First Name,Last Name,Plan +steve@example.com,Steve,Wozniak,pro diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index 192e475..970f645 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -66,6 +66,11 @@ class CreateParams(TypedDict): List of segment IDs to add imported contacts to. Will be serialized as [{"id": "..."}] before sending. """ + topics: NotRequired[List[Dict[str, str]]] + """ + List of topic subscriptions for imported contacts. + Each entry must have 'id' and 'subscription' ('opt_in' or 'opt_out'). + """ class ListParams(TypedDict): status: NotRequired[Literal["queued", "in_progress", "completed", "failed"]] @@ -113,6 +118,8 @@ def create(cls, params: CreateParams) -> CreateContactImportResponse: form_data["segments"] = json_lib.dumps( [{"id": sid} for sid in params["segments"]] ) + if "topics" in params: + form_data["topics"] = json_lib.dumps(params["topics"]) resp = request.Request[ContactImports.CreateContactImportResponse]( path="/contacts/imports", @@ -199,6 +206,8 @@ async def create_async(cls, params: CreateParams) -> CreateContactImportResponse form_data["segments"] = json_lib.dumps( [{"id": sid} for sid in params["segments"]] ) + if "topics" in params: + form_data["topics"] = json_lib.dumps(params["topics"]) resp = await AsyncRequest[ContactImports.CreateContactImportResponse]( path="/contacts/imports", From 37f21f50bb27acb183c35b49a4a7b6fea5febe3f Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:06:30 -0300 Subject: [PATCH 05/16] chore: update topic id in contact imports example --- examples/contact_imports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/contact_imports.py b/examples/contact_imports.py index 1443ec9..4d7c0bd 100644 --- a/examples/contact_imports.py +++ b/examples/contact_imports.py @@ -26,7 +26,7 @@ "segments": ["60a2ac5e-0774-456e-817d-ebf40f6dba31"], "topics": [ { - "id": "059ac693-2fc8-4c13-8b27-01350d638a17", + "id": "284edd7e-b042-46dd-b5ee-a8a88a9ec65f", "subscription": "opt_in", }, ], From e8dda2ca903e5b629509d43eae7e5e69da2b1814 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:16:21 -0300 Subject: [PATCH 06/16] chore: update topic id and add timestamp to contact imports example --- examples/contact_imports.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/contact_imports.py b/examples/contact_imports.py index 4d7c0bd..e2857e9 100644 --- a/examples/contact_imports.py +++ b/examples/contact_imports.py @@ -1,13 +1,15 @@ import os +import time import resend if not os.environ["RESEND_API_KEY"]: raise EnvironmentError("RESEND_API_KEY is missing") +ts = int(time.time()) csv_path = os.path.join(os.path.dirname(__file__), "contacts.csv") with open(csv_path, "rb") as f: - file_content = f.read() + file_content = f.read().replace(b"steve@example.com,Steve,Wozniak", f"steve+{ts}@example.com,Steve,Wozniak".encode()) create_params: resend.ContactImports.CreateParams = { "file": file_content, @@ -26,7 +28,7 @@ "segments": ["60a2ac5e-0774-456e-817d-ebf40f6dba31"], "topics": [ { - "id": "284edd7e-b042-46dd-b5ee-a8a88a9ec65f", + "id": "6eb54030-9489-4e9c-8de6-cd337c5fef1e", "subscription": "opt_in", }, ], From a386f6257004b9659a3512ab84ea55b4e38a4d5a Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:18:39 -0300 Subject: [PATCH 07/16] fix: use NotRequired for counts, remove duplicate status param, guard files/data kwargs --- resend/async_request.py | 20 ++++++++++++-------- resend/contacts/imports/_contact_import.py | 4 ++-- resend/contacts/imports/_contact_imports.py | 9 --------- resend/request.py | 20 ++++++++++++-------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/resend/async_request.py b/resend/async_request.py index 580dd24..3b792ed 100644 --- a/resend/async_request.py +++ b/resend/async_request.py @@ -101,14 +101,18 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: suggested_action="Run: pip install resend[async]", ) - content, _status_code, resp_headers = await async_client.request( - method=self.verb, - url=url, - headers=headers, - json=json_params, - files=self.files, - data=self.data, - ) + kwargs: Dict[str, Any] = { + "method": self.verb, + "url": url, + "headers": headers, + "json": json_params, + } + if self.files is not None: + kwargs["files"] = self.files + if self.data is not None: + kwargs["data"] = self.data + + content, _status_code, resp_headers = await async_client.request(**kwargs) # Safety net around the HTTP Client except ResendError: diff --git a/resend/contacts/imports/_contact_import.py b/resend/contacts/imports/_contact_import.py index 27d29e9..824225f 100644 --- a/resend/contacts/imports/_contact_import.py +++ b/resend/contacts/imports/_contact_import.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing_extensions import NotRequired from resend._base_response import BaseResponse @@ -38,4 +38,4 @@ class ContactImport(BaseResponse): id: str status: str created_at: str - counts: Optional[ContactImportCounts] + counts: NotRequired[ContactImportCounts] diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index 970f645..bd5437c 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -166,11 +166,6 @@ def list(cls, params: Optional[ListParams] = None) -> ListContactImportsResponse """ query_params = cast(Dict[Any, Any], params) if params else None path = PaginationHelper.build_paginated_path("/contacts/imports", query_params) - # Append status filter if provided (PaginationHelper only handles limit/after/before) - if params and "status" in params: - separator = "&" if "?" in path else "?" - path = f"{path}{separator}status={params['status']}" - resp = request.Request[ContactImports.ListContactImportsResponse]( path=path, params={}, @@ -254,10 +249,6 @@ async def list_async(cls, params: Optional[ListParams] = None) -> ListContactImp """ query_params = cast(Dict[Any, Any], params) if params else None path = PaginationHelper.build_paginated_path("/contacts/imports", query_params) - if params and "status" in params: - separator = "&" if "?" in path else "?" - path = f"{path}{separator}status={params['status']}" - resp = await AsyncRequest[ContactImports.ListContactImportsResponse]( path=path, params={}, diff --git a/resend/request.py b/resend/request.py index d3efa99..7e3bcef 100644 --- a/resend/request.py +++ b/resend/request.py @@ -88,14 +88,18 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: sync_client = cast(HTTPClient, resend.default_http_client) - content, _status_code, resp_headers = sync_client.request( - method=self.verb, - url=url, - headers=headers, - json=json_params, - files=self.files, - data=self.data, - ) + kwargs: Dict[str, Any] = { + "method": self.verb, + "url": url, + "headers": headers, + "json": json_params, + } + if self.files is not None: + kwargs["files"] = self.files + if self.data is not None: + kwargs["data"] = self.data + + content, _status_code, resp_headers = sync_client.request(**kwargs) # Safety net around the HTTP Client except Exception as e: From 96e8f9b05f78fa708deaa3d00d403db5ee1f24a7 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:21:33 -0300 Subject: [PATCH 08/16] fix: pass data in non-multipart requests and tighten error assertions in tests --- resend/http_client_httpx.py | 1 + resend/http_client_requests.py | 1 + tests/contact_imports_test.py | 10 ++++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/resend/http_client_httpx.py b/resend/http_client_httpx.py index 3c1f38b..5ace249 100644 --- a/resend/http_client_httpx.py +++ b/resend/http_client_httpx.py @@ -38,6 +38,7 @@ async def request( url=url, headers=headers, json=json, + data=data, ) return resp.content, resp.status_code, resp.headers except httpx.RequestError as e: diff --git a/resend/http_client_requests.py b/resend/http_client_requests.py index 22c09c6..d8f678a 100644 --- a/resend/http_client_requests.py +++ b/resend/http_client_requests.py @@ -38,6 +38,7 @@ def request( url=url, headers=headers, json=json, + data=data, timeout=self._timeout, ) return resp.content, resp.status_code, resp.headers diff --git a/tests/contact_imports_test.py b/tests/contact_imports_test.py index db49ab4..de26645 100644 --- a/tests/contact_imports_test.py +++ b/tests/contact_imports_test.py @@ -33,10 +33,9 @@ def test_create_contact_import_with_options(self) -> None: assert resp["id"] == "479e3145-dd38-476b-932c-529ceb705947" def test_create_contact_import_missing_file(self) -> None: - try: + with self.assertRaises(ValueError) as ctx: resend.Contacts.Imports.create({"file": b""}) - except ValueError as e: - assert str(e) == "file is required" + assert str(ctx.exception) == "file is required" def test_get_contact_import(self) -> None: self.set_mock_json( @@ -65,10 +64,9 @@ def test_get_contact_import(self) -> None: assert counts["total"] == 100 def test_get_contact_import_missing_id(self) -> None: - try: + with self.assertRaises(ValueError) as ctx: resend.Contacts.Imports.get("") - except ValueError as e: - assert str(e) == "id is required" + assert str(ctx.exception) == "id is required" def test_list_contact_imports(self) -> None: self.set_mock_json( From 0fbe4ef45f667a8f8f9df1dbb9737cdc11f114cd Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:32:38 -0300 Subject: [PATCH 09/16] refactor: extract _build_multipart helper and fix json/data mutual exclusion in http clients --- resend/contacts/imports/_contact_imports.py | 51 ++++++++------------- resend/http_client_httpx.py | 2 +- resend/http_client_requests.py | 2 +- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index bd5437c..9ceeae7 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -90,21 +90,12 @@ class ListParams(TypedDict): Cursor for backward pagination (exclusive). """ - @classmethod - def create(cls, params: CreateParams) -> CreateContactImportResponse: - """ - Create a new contact import from a CSV file. - see more: https://resend.com/docs/api-reference/contacts/create-contact-import - - Args: - params (CreateParams): Import parameters including the CSV file content. - - Returns: - CreateContactImportResponse: The created import job with its ID. - """ + @staticmethod + def _build_multipart( + params: "ContactImports.CreateParams", + ) -> tuple: if not params.get("file"): raise ValueError("file is required") - filename = params.get("filename", "import.csv") files: Dict[str, Any] = { "file": (filename, params["file"], "text/csv"), @@ -120,7 +111,21 @@ def create(cls, params: CreateParams) -> CreateContactImportResponse: ) if "topics" in params: form_data["topics"] = json_lib.dumps(params["topics"]) + return files, form_data + + @classmethod + def create(cls, params: CreateParams) -> CreateContactImportResponse: + """ + Create a new contact import from a CSV file. + see more: https://resend.com/docs/api-reference/contacts/create-contact-import + + Args: + params (CreateParams): Import parameters including the CSV file content. + Returns: + CreateContactImportResponse: The created import job with its ID. + """ + files, form_data = cls._build_multipart(params) resp = request.Request[ContactImports.CreateContactImportResponse]( path="/contacts/imports", params={}, @@ -185,25 +190,7 @@ async def create_async(cls, params: CreateParams) -> CreateContactImportResponse Returns: CreateContactImportResponse: The created import job with its ID. """ - if not params.get("file"): - raise ValueError("file is required") - - filename = params.get("filename", "import.csv") - files: Dict[str, Any] = { - "file": (filename, params["file"], "text/csv"), - } - form_data: Dict[str, str] = {} - if "column_map" in params: - form_data["column_map"] = json_lib.dumps(params["column_map"]) - if "on_conflict" in params: - form_data["on_conflict"] = params["on_conflict"] - if "segments" in params: - form_data["segments"] = json_lib.dumps( - [{"id": sid} for sid in params["segments"]] - ) - if "topics" in params: - form_data["topics"] = json_lib.dumps(params["topics"]) - + files, form_data = cls._build_multipart(params) resp = await AsyncRequest[ContactImports.CreateContactImportResponse]( path="/contacts/imports", params={}, diff --git a/resend/http_client_httpx.py b/resend/http_client_httpx.py index 5ace249..32ea193 100644 --- a/resend/http_client_httpx.py +++ b/resend/http_client_httpx.py @@ -37,7 +37,7 @@ async def request( method=method, url=url, headers=headers, - json=json, + json=json if data is None else None, data=data, ) return resp.content, resp.status_code, resp.headers diff --git a/resend/http_client_requests.py b/resend/http_client_requests.py index d8f678a..8caf308 100644 --- a/resend/http_client_requests.py +++ b/resend/http_client_requests.py @@ -37,7 +37,7 @@ def request( method=method, url=url, headers=headers, - json=json, + json=json if data is None else None, data=data, timeout=self._timeout, ) From 30e0c8dfe4218ff44c402d57d3b9bea3aa13e328 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:34:00 -0300 Subject: [PATCH 10/16] fix: use Tuple from typing for py3.7 compat in _build_multipart return type --- resend/contacts/imports/_contact_imports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index 9ceeae7..0b3b392 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -1,5 +1,5 @@ import json as json_lib -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, Tuple, cast from typing_extensions import Literal, NotRequired, TypedDict @@ -93,7 +93,7 @@ class ListParams(TypedDict): @staticmethod def _build_multipart( params: "ContactImports.CreateParams", - ) -> tuple: + ) -> Tuple[Dict[str, Any], Dict[str, str]]: if not params.get("file"): raise ValueError("file is required") filename = params.get("filename", "import.csv") From cee06506ad0e256b60cdfcd206ba09829e452be5 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:50:43 -0300 Subject: [PATCH 11/16] fix: add completed_at, fix contacts __init__ alias, forward files/data in custom http client example --- examples/with_custom_http_client.py | 4 +++- resend/contacts/__init__.py | 5 ++--- resend/contacts/imports/_contact_import.py | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/with_custom_http_client.py b/examples/with_custom_http_client.py index 0f2fa62..adeb133 100644 --- a/examples/with_custom_http_client.py +++ b/examples/with_custom_http_client.py @@ -30,7 +30,9 @@ def request( method=method, url=url, headers=headers, - json=json, + json=json if data is None and files is None else None, + files=files, + data=data, timeout=self.timeout, ) return ( diff --git a/resend/contacts/__init__.py b/resend/contacts/__init__.py index e006a7c..b02803d 100644 --- a/resend/contacts/__init__.py +++ b/resend/contacts/__init__.py @@ -1,5 +1,4 @@ -from .imports._contact_import import ContactImport as ContactImportObj -from .imports._contact_import import ContactImportCounts +from .imports._contact_import import ContactImport, ContactImportCounts from .imports._contact_imports import ContactImports -__all__ = ["ContactImports", "ContactImportObj", "ContactImportCounts"] +__all__ = ["ContactImports", "ContactImport", "ContactImportCounts"] diff --git a/resend/contacts/imports/_contact_import.py b/resend/contacts/imports/_contact_import.py index 824225f..992d48d 100644 --- a/resend/contacts/imports/_contact_import.py +++ b/resend/contacts/imports/_contact_import.py @@ -1,3 +1,5 @@ +from typing import Optional + from typing_extensions import NotRequired from resend._base_response import BaseResponse @@ -38,4 +40,5 @@ class ContactImport(BaseResponse): id: str status: str created_at: str + completed_at: Optional[str] counts: NotRequired[ContactImportCounts] From 99a0e6970a4822a0b2ce91cd69e5fb6059cae3fe Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:55:00 -0300 Subject: [PATCH 12/16] refactor: use resend.Contacts.Imports.* type access pattern in example and tests --- examples/contact_imports.py | 6 +++--- tests/contact_imports_test.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/contact_imports.py b/examples/contact_imports.py index e2857e9..c336bed 100644 --- a/examples/contact_imports.py +++ b/examples/contact_imports.py @@ -11,7 +11,7 @@ with open(csv_path, "rb") as f: file_content = f.read().replace(b"steve@example.com,Steve,Wozniak", f"steve+{ts}@example.com,Steve,Wozniak".encode()) -create_params: resend.ContactImports.CreateParams = { +create_params: resend.Contacts.Imports.CreateParams = { "file": file_content, "column_map": { "email": "Email", @@ -34,7 +34,7 @@ ], } -import_response: resend.ContactImports.CreateContactImportResponse = ( +import_response: resend.Contacts.Imports.CreateContactImportResponse = ( resend.Contacts.Imports.create(create_params) ) print("Created contact import with ID: {}".format(import_response["id"])) @@ -44,7 +44,7 @@ print("Retrieved contact import") print(contact_import) -list_response: resend.ContactImports.ListContactImportsResponse = ( +list_response: resend.Contacts.Imports.ListContactImportsResponse = ( resend.Contacts.Imports.list() ) print(f"Found {len(list_response['data'])} imports") diff --git a/tests/contact_imports_test.py b/tests/contact_imports_test.py index de26645..99717f9 100644 --- a/tests/contact_imports_test.py +++ b/tests/contact_imports_test.py @@ -10,10 +10,10 @@ def test_create_contact_import(self) -> None: {"object": "contact_import", "id": "479e3145-dd38-476b-932c-529ceb705947"} ) - params: resend.ContactImports.CreateParams = { + params: resend.Contacts.Imports.CreateParams = { "file": b"email,first_name\nsteve@example.com,Steve", } - resp: resend.ContactImports.CreateContactImportResponse = resend.Contacts.Imports.create(params) + resp: resend.Contacts.Imports.CreateContactImportResponse = resend.Contacts.Imports.create(params) assert resp["id"] == "479e3145-dd38-476b-932c-529ceb705947" assert resp["object"] == "contact_import" @@ -22,7 +22,7 @@ def test_create_contact_import_with_options(self) -> None: {"object": "contact_import", "id": "479e3145-dd38-476b-932c-529ceb705947"} ) - params: resend.ContactImports.CreateParams = { + params: resend.Contacts.Imports.CreateParams = { "file": b"email,first_name\nsteve@example.com,Steve", "filename": "contacts.csv", "on_conflict": "upsert", @@ -84,7 +84,7 @@ def test_list_contact_imports(self) -> None: } ) - result: resend.ContactImports.ListContactImportsResponse = resend.Contacts.Imports.list() + result: resend.Contacts.Imports.ListContactImportsResponse = resend.Contacts.Imports.list() assert result["object"] == "list" assert result["has_more"] is False assert len(result["data"]) == 1 From 6350c89c85907223b3b338a426ff94e27012633b Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:56:49 -0300 Subject: [PATCH 13/16] refactor: expose ContactImport on ContactImports class for consistent resend.Contacts.Imports.* access --- examples/contact_imports.py | 2 +- resend/contacts/imports/_contact_imports.py | 2 ++ tests/contact_imports_test.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/contact_imports.py b/examples/contact_imports.py index c336bed..c98813f 100644 --- a/examples/contact_imports.py +++ b/examples/contact_imports.py @@ -40,7 +40,7 @@ print("Created contact import with ID: {}".format(import_response["id"])) print(import_response) -contact_import: resend.ContactImport = resend.Contacts.Imports.get(import_response["id"]) +contact_import: resend.Contacts.Imports.ContactImport = resend.Contacts.Imports.get(import_response["id"]) print("Retrieved contact import") print(contact_import) diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index 0b3b392..feeb2fc 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -16,6 +16,8 @@ class ContactImports: + ContactImport = ContactImport + class CreateContactImportResponse(BaseResponse): """ CreateContactImportResponse wraps the response from a successful import creation. diff --git a/tests/contact_imports_test.py b/tests/contact_imports_test.py index 99717f9..24f0afa 100644 --- a/tests/contact_imports_test.py +++ b/tests/contact_imports_test.py @@ -54,7 +54,7 @@ def test_get_contact_import(self) -> None: } ) - result: resend.ContactImport = resend.Contacts.Imports.get( + result: resend.Contacts.Imports.ContactImport = resend.Contacts.Imports.get( "479e3145-dd38-476b-932c-529ceb705947" ) assert result["id"] == "479e3145-dd38-476b-932c-529ceb705947" From dc4ce98c2d96efa2771c3357846d15f5afd30545 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 12:58:33 -0300 Subject: [PATCH 14/16] fix: use direct class refs for type annotations, matching ContactSegments pattern --- examples/contact_imports.py | 8 ++++---- resend/contacts/imports/_contact_imports.py | 2 -- tests/contact_imports_test.py | 10 +++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/contact_imports.py b/examples/contact_imports.py index c98813f..e2857e9 100644 --- a/examples/contact_imports.py +++ b/examples/contact_imports.py @@ -11,7 +11,7 @@ with open(csv_path, "rb") as f: file_content = f.read().replace(b"steve@example.com,Steve,Wozniak", f"steve+{ts}@example.com,Steve,Wozniak".encode()) -create_params: resend.Contacts.Imports.CreateParams = { +create_params: resend.ContactImports.CreateParams = { "file": file_content, "column_map": { "email": "Email", @@ -34,17 +34,17 @@ ], } -import_response: resend.Contacts.Imports.CreateContactImportResponse = ( +import_response: resend.ContactImports.CreateContactImportResponse = ( resend.Contacts.Imports.create(create_params) ) print("Created contact import with ID: {}".format(import_response["id"])) print(import_response) -contact_import: resend.Contacts.Imports.ContactImport = resend.Contacts.Imports.get(import_response["id"]) +contact_import: resend.ContactImport = resend.Contacts.Imports.get(import_response["id"]) print("Retrieved contact import") print(contact_import) -list_response: resend.Contacts.Imports.ListContactImportsResponse = ( +list_response: resend.ContactImports.ListContactImportsResponse = ( resend.Contacts.Imports.list() ) print(f"Found {len(list_response['data'])} imports") diff --git a/resend/contacts/imports/_contact_imports.py b/resend/contacts/imports/_contact_imports.py index feeb2fc..0b3b392 100644 --- a/resend/contacts/imports/_contact_imports.py +++ b/resend/contacts/imports/_contact_imports.py @@ -16,8 +16,6 @@ class ContactImports: - ContactImport = ContactImport - class CreateContactImportResponse(BaseResponse): """ CreateContactImportResponse wraps the response from a successful import creation. diff --git a/tests/contact_imports_test.py b/tests/contact_imports_test.py index 24f0afa..de26645 100644 --- a/tests/contact_imports_test.py +++ b/tests/contact_imports_test.py @@ -10,10 +10,10 @@ def test_create_contact_import(self) -> None: {"object": "contact_import", "id": "479e3145-dd38-476b-932c-529ceb705947"} ) - params: resend.Contacts.Imports.CreateParams = { + params: resend.ContactImports.CreateParams = { "file": b"email,first_name\nsteve@example.com,Steve", } - resp: resend.Contacts.Imports.CreateContactImportResponse = resend.Contacts.Imports.create(params) + resp: resend.ContactImports.CreateContactImportResponse = resend.Contacts.Imports.create(params) assert resp["id"] == "479e3145-dd38-476b-932c-529ceb705947" assert resp["object"] == "contact_import" @@ -22,7 +22,7 @@ def test_create_contact_import_with_options(self) -> None: {"object": "contact_import", "id": "479e3145-dd38-476b-932c-529ceb705947"} ) - params: resend.Contacts.Imports.CreateParams = { + params: resend.ContactImports.CreateParams = { "file": b"email,first_name\nsteve@example.com,Steve", "filename": "contacts.csv", "on_conflict": "upsert", @@ -54,7 +54,7 @@ def test_get_contact_import(self) -> None: } ) - result: resend.Contacts.Imports.ContactImport = resend.Contacts.Imports.get( + result: resend.ContactImport = resend.Contacts.Imports.get( "479e3145-dd38-476b-932c-529ceb705947" ) assert result["id"] == "479e3145-dd38-476b-932c-529ceb705947" @@ -84,7 +84,7 @@ def test_list_contact_imports(self) -> None: } ) - result: resend.Contacts.Imports.ListContactImportsResponse = resend.Contacts.Imports.list() + result: resend.ContactImports.ListContactImportsResponse = resend.Contacts.Imports.list() assert result["object"] == "list" assert result["has_more"] is False assert len(result["data"]) == 1 From 2380c5fac139dd71e976e6a6e41438e49d15f83c Mon Sep 17 00:00:00 2001 From: Derich Pacheco Date: Wed, 17 Jun 2026 10:31:12 -0300 Subject: [PATCH 15/16] chore: bump version to 2.31.0 (#221) --- resend/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resend/version.py b/resend/version.py index 7037d1d..d00b6d1 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.30.1" +__version__ = "2.31.0" def get_version() -> str: From 1dbed3e1b9b767e7bb226ff96354183f3aa577f5 Mon Sep 17 00:00:00 2001 From: drish Date: Wed, 17 Jun 2026 13:04:29 -0300 Subject: [PATCH 16/16] chore: bump version to 2.32.0 --- resend/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resend/version.py b/resend/version.py index d00b6d1..ca53631 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.31.0" +__version__ = "2.32.0" def get_version() -> str: