Skip to content

Commit 10b4758

Browse files
authored
feat: add contact imports endpoints (#220)
1 parent 4ae9fa4 commit 10b4758

17 files changed

Lines changed: 547 additions & 31 deletions

examples/contact_imports.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import os
2+
import time
3+
4+
import resend
5+
6+
if not os.environ["RESEND_API_KEY"]:
7+
raise EnvironmentError("RESEND_API_KEY is missing")
8+
9+
ts = int(time.time())
10+
csv_path = os.path.join(os.path.dirname(__file__), "contacts.csv")
11+
with open(csv_path, "rb") as f:
12+
file_content = f.read().replace(b"steve@example.com,Steve,Wozniak", f"steve+{ts}@example.com,Steve,Wozniak".encode())
13+
14+
create_params: resend.ContactImports.CreateParams = {
15+
"file": file_content,
16+
"column_map": {
17+
"email": "Email",
18+
"first_name": "First Name",
19+
"last_name": "Last Name",
20+
"properties": {
21+
"plan": {
22+
"column": "Plan",
23+
"type": "string",
24+
},
25+
},
26+
},
27+
"on_conflict": "upsert",
28+
"segments": ["60a2ac5e-0774-456e-817d-ebf40f6dba31"],
29+
"topics": [
30+
{
31+
"id": "6eb54030-9489-4e9c-8de6-cd337c5fef1e",
32+
"subscription": "opt_in",
33+
},
34+
],
35+
}
36+
37+
import_response: resend.ContactImports.CreateContactImportResponse = (
38+
resend.Contacts.Imports.create(create_params)
39+
)
40+
print("Created contact import with ID: {}".format(import_response["id"]))
41+
print(import_response)
42+
43+
contact_import: resend.ContactImport = resend.Contacts.Imports.get(import_response["id"])
44+
print("Retrieved contact import")
45+
print(contact_import)
46+
47+
list_response: resend.ContactImports.ListContactImportsResponse = (
48+
resend.Contacts.Imports.list()
49+
)
50+
print(f"Found {len(list_response['data'])} imports")
51+
print(f"Has more: {list_response['has_more']}")
52+
for item in list_response["data"]:
53+
print(item)

examples/contacts.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Email,First Name,Last Name,Plan
2+
steve@example.com,Steve,Wozniak,pro

examples/with_custom_http_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ def request(
2121
url: str,
2222
headers: Mapping[str, str],
2323
json: Optional[Union[Dict[str, Any], List[Any]]] = None,
24+
files: Optional[Dict[str, Any]] = None,
25+
data: Optional[Dict[str, str]] = None,
2426
) -> Tuple[bytes, int, Dict[str, str]]:
2527
print(f"[HTTP] {method.upper()} {url} with timeout={self.timeout}")
2628
try:
2729
response = requests.request(
2830
method=method,
2931
url=url,
3032
headers=headers,
31-
json=json,
33+
json=json if data is None and files is None else None,
34+
files=files,
35+
data=data,
3236
timeout=self.timeout,
3337
)
3438
return (

resend/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from .contacts._contact_topic import ContactTopic, TopicSubscriptionUpdate
2323
from .contacts._contacts import Contacts
2424
from .contacts._topics import Topics as ContactsTopics
25+
from .contacts.imports._contact_import import ContactImport, ContactImportCounts
26+
from .contacts.imports._contact_imports import ContactImports
2527
from .contacts.segments._contact_segment import ContactSegment
2628
from .contacts.segments._contact_segments import ContactSegments
2729
from .domains._domain import Domain
@@ -83,6 +85,7 @@
8385
"Audiences",
8486
"Automations",
8587
"Contacts",
88+
"ContactImports",
8689
"ContactProperties",
8790
"Broadcasts",
8891
"Events",
@@ -110,6 +113,8 @@
110113
"EventSchema",
111114
"EventSchemaFieldType",
112115
"Contact",
116+
"ContactImport",
117+
"ContactImportCounts",
113118
"ContactSegment",
114119
"ContactSegments",
115120
"ContactProperty",

resend/async_request.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ def __init__(
2424
params: ParamsType,
2525
verb: RequestVerb,
2626
options: Optional[Dict[str, Any]] = None,
27+
files: Optional[Dict[str, Any]] = None,
28+
data: Optional[Dict[str, str]] = None,
2729
):
2830
self.path = path
2931
self.params = params
3032
self.verb = verb
3133
self.options = options
34+
self.files = files
35+
self.data = data
3236
self._response_headers: Dict[str, str] = {}
3337

3438
async def perform(self) -> Union[T, None]:
@@ -97,12 +101,18 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
97101
suggested_action="Run: pip install resend[async]",
98102
)
99103

100-
content, _status_code, resp_headers = await async_client.request(
101-
method=self.verb,
102-
url=url,
103-
headers=headers,
104-
json=json_params,
105-
)
104+
kwargs: Dict[str, Any] = {
105+
"method": self.verb,
106+
"url": url,
107+
"headers": headers,
108+
"json": json_params,
109+
}
110+
if self.files is not None:
111+
kwargs["files"] = self.files
112+
if self.data is not None:
113+
kwargs["data"] = self.data
114+
115+
content, _status_code, resp_headers = await async_client.request(**kwargs)
106116

107117
# Safety net around the HTTP Client
108118
except ResendError:

resend/contacts/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .imports._contact_import import ContactImport, ContactImportCounts
2+
from .imports._contact_imports import ContactImports
3+
4+
__all__ = ["ContactImports", "ContactImport", "ContactImportCounts"]

resend/contacts/_contacts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from ._contact import Contact
1010
from ._topics import Topics
11+
from .imports._contact_imports import ContactImports
1112
from .segments._contact_segments import ContactSegments
1213

1314
# Async imports (optional - only available with pip install resend[async])
@@ -21,6 +22,7 @@ class Contacts:
2122
# Sub-API for managing contact-segment associations
2223
Segments = ContactSegments
2324
Topics = Topics
25+
Imports = ContactImports
2426

2527
class RemoveContactResponse(BaseResponse):
2628
"""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from ._contact_import import ContactImport, ContactImportCounts
2+
from ._contact_imports import ContactImports
3+
4+
__all__ = ["ContactImports", "ContactImport", "ContactImportCounts"]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Optional
2+
3+
from typing_extensions import NotRequired
4+
5+
from resend._base_response import BaseResponse
6+
7+
8+
class ContactImportCounts(BaseResponse):
9+
"""
10+
ContactImportCounts holds row-level statistics for a contact import.
11+
12+
Attributes:
13+
total (int): Total number of rows processed.
14+
created (int): Number of contacts created.
15+
updated (int): Number of contacts updated.
16+
skipped (int): Number of rows skipped.
17+
failed (int): Number of rows that failed.
18+
"""
19+
20+
total: int
21+
created: int
22+
updated: int
23+
skipped: int
24+
failed: int
25+
26+
27+
class ContactImport(BaseResponse):
28+
"""
29+
ContactImport represents a contact import job.
30+
31+
Attributes:
32+
object (str): Always 'contact_import'.
33+
id (str): Unique identifier for the contact import.
34+
status (str): 'queued', 'in_progress', 'completed', or 'failed'.
35+
created_at (str): ISO 8601 timestamp of when the import was created.
36+
counts (ContactImportCounts): Row-level import statistics (present when status is completed or failed).
37+
"""
38+
39+
object: str
40+
id: str
41+
status: str
42+
created_at: str
43+
completed_at: Optional[str]
44+
counts: NotRequired[ContactImportCounts]

0 commit comments

Comments
 (0)