Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
53 changes: 53 additions & 0 deletions examples/contact_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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().replace(b"steve@example.com,Steve,Wozniak", f"steve+{ts}@example.com,Steve,Wozniak".encode())

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"],
"topics": [
{
"id": "6eb54030-9489-4e9c-8de6-cd337c5fef1e",
"subscription": "opt_in",
},
],
}

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)
2 changes: 2 additions & 0 deletions examples/contacts.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Email,First Name,Last Name,Plan
steve@example.com,Steve,Wozniak,pro
6 changes: 5 additions & 1 deletion examples/with_custom_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ 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:
response = requests.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 (
Expand Down
5 changes: 5 additions & 0 deletions resend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +85,7 @@
"Audiences",
"Automations",
"Contacts",
"ContactImports",
"ContactProperties",
"Broadcasts",
"Events",
Expand Down Expand Up @@ -110,6 +113,8 @@
"EventSchema",
"EventSchemaFieldType",
"Contact",
"ContactImport",
"ContactImportCounts",
"ContactSegment",
"ContactSegments",
"ContactProperty",
Expand Down
22 changes: 16 additions & 6 deletions resend/async_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -97,12 +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,
)
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:
Expand Down
4 changes: 4 additions & 0 deletions resend/contacts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .imports._contact_import import ContactImport, ContactImportCounts
from .imports._contact_imports import ContactImports

__all__ = ["ContactImports", "ContactImport", "ContactImportCounts"]
2 changes: 2 additions & 0 deletions resend/contacts/_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -21,6 +22,7 @@ class Contacts:
# Sub-API for managing contact-segment associations
Segments = ContactSegments
Topics = Topics
Imports = ContactImports

class RemoveContactResponse(BaseResponse):
"""
Expand Down
4 changes: 4 additions & 0 deletions resend/contacts/imports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._contact_import import ContactImport, ContactImportCounts
from ._contact_imports import ContactImports

__all__ = ["ContactImports", "ContactImport", "ContactImportCounts"]
44 changes: 44 additions & 0 deletions resend/contacts/imports/_contact_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Optional

from typing_extensions import NotRequired

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
completed_at: Optional[str]
counts: NotRequired[ContactImportCounts]
Loading
Loading