Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ nylas-python Changelog
Unreleased
----------
* Added optional `tracking_options.domain_name` support for custom link and open tracking hostnames in message sends, scheduled sends, drafts, and Transactional Send
* Added Contact metadata request/response models and `metadata_pair` filtering, with contact webhook compatibility documentation

v6.17.0
----------
Expand Down
14 changes: 13 additions & 1 deletion nylas/models/contacts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from enum import Enum
from typing import Optional, List
from typing import Dict, List, Optional
from typing_extensions import TypedDict, NotRequired

from dataclasses_json import dataclass_json
Expand Down Expand Up @@ -132,6 +132,9 @@ class Contact:
given_name: The contact's given name.
job_title: The contact's job title.
manager_name: The contact's manager name.
metadata: Nylas-owned metadata associated with the contact. Metadata is
not written to the provider and does not follow a contact if its
public ID changes.
middle_name: The contact's middle name.
nickname: The contact's nickname.
notes: The contact's notes.
Expand All @@ -158,6 +161,7 @@ class Contact:
given_name: Optional[str] = None
job_title: Optional[str] = None
manager_name: Optional[str] = None
metadata: Optional[Dict[str, str]] = None
middle_name: Optional[str] = None
nickname: Optional[str] = None
notes: Optional[str] = None
Expand Down Expand Up @@ -286,6 +290,9 @@ class CreateContactRequest(TypedDict):
given_name: The contact's given name.
job_title: The contact's job title.
manager_name: The contact's manager name.
metadata: Nylas-owned metadata for the contact. On update, omission or
None preserves existing metadata, an object replaces it, and an
empty object clears it.
middle_name: The contact's middle name.
nickname: The contact's nickname.
notes: The contact's notes.
Expand All @@ -309,6 +316,7 @@ class CreateContactRequest(TypedDict):
given_name: NotRequired[str]
job_title: NotRequired[str]
manager_name: NotRequired[str]
metadata: NotRequired[Optional[Dict[str, str]]]
middle_name: NotRequired[str]
nickname: NotRequired[str]
notes: NotRequired[str]
Expand Down Expand Up @@ -338,6 +346,9 @@ class ListContactsQueryParams(ListQueryParams):
source: Return contacts from a specific source.
group: Return contacts from a specific group.
recurse: Return contacts from all sub-groups of the specified group.
metadata_pair: Filter by one indexed metadata key/value pair. Use one
of key1 through key5. This cannot be combined with provider-side
contact filters; pagination parameters are supported.
select (NotRequired[str]): Comma-separated list of fields to return in the response.
This allows you to receive only the portion of object data that you're interested in.
limit (NotRequired[int]): The maximum number of objects to return.
Expand All @@ -351,6 +362,7 @@ class ListContactsQueryParams(ListQueryParams):
source: NotRequired[SourceType]
group: NotRequired[str]
recurse: NotRequired[bool]
metadata_pair: NotRequired[Dict[str, str]]


class GroupType(str, Enum):
Expand Down
8 changes: 7 additions & 1 deletion nylas/models/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@


class WebhookTriggers(str, Enum):
"""Enum representing the available webhook triggers."""
"""
Enum representing the available webhook triggers.

Native iCloud contacts support CONTACT_UPDATED and CONTACT_DELETED. Yahoo
contacts support neither trigger, including for changes made through the
Nylas API. There is no CONTACT_CREATED trigger.
"""
BOOKING_CREATED = "booking.created"
BOOKING_PENDING = "booking.pending"
BOOKING_RESCHEDULED = "booking.rescheduled"
Expand Down
11 changes: 11 additions & 0 deletions tests/handler/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ def test_build_query_params(self, patched_version_and_sys):
== "https://test.nylas.com/foo?foo=bar&list=a&list=b&list=c&map=key1:value1&map=key2:value2"
)

def test_build_contact_metadata_pair_query_param(self):
url = _build_query_params(
base_url="https://test.nylas.com/v3/grants/abc-123/contacts",
query_params={"metadata_pair": {"key1": "sync_eligible"}},
)

assert (
url
== "https://test.nylas.com/v3/grants/abc-123/contacts?metadata_pair=key1:sync_eligible"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, just noting it: Python emits the colon raw (metadata_pair=key1:sync_eligible) while Node and Ruby percent-encode it (key1%3Async_eligible). Both are valid per RFC 3986 and the API decodes either, so nothing to change here — it's pre-existing serializer behavior in each SDK, not something this PR introduced. Flagging it in case it ever comes up in a support ticket.

)

def test_execute_download_request(self, http_client, patched_request):
response = http_client._execute_download_request(
path="/foo",
Expand Down
21 changes: 21 additions & 0 deletions tests/resources/test_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_contact_deserialization(self):
"im_addresses": [{"type": "other", "im_address": "myjabberaddress"}],
"job_title": "Software Engineer",
"manager_name": "Bill",
"metadata": {"key1": "sync_eligible"},
"middle_name": "Jacob",
"nickname": "JD",
"notes": "Loves ramen",
Expand Down Expand Up @@ -64,6 +65,7 @@ def test_contact_deserialization(self):
]
assert contact.job_title == "Software Engineer"
assert contact.manager_name == "Bill"
assert contact.metadata == {"key1": "sync_eligible"}
assert contact.middle_name == "Jacob"
assert contact.nickname == "JD"
assert contact.notes == "Loves ramen"
Expand Down Expand Up @@ -112,6 +114,23 @@ def test_list_contacts_with_query_params(self, http_client_list_response):
overrides=None,
)

def test_list_contacts_with_metadata_pair(self, http_client_list_response):
Comment thread
quzhi1 marked this conversation as resolved.
contacts = Contacts(http_client_list_response)

contacts.list(
identifier="abc-123",
query_params={"metadata_pair": {"key1": "sync_eligible"}},
)

http_client_list_response._execute.assert_called_once_with(
"GET",
"/v3/grants/abc-123/contacts",
None,
{"metadata_pair": {"key1": "sync_eligible"}},
None,
overrides=None,
)

def test_list_contacts_with_select_param(self, http_client_list_response):
contacts = Contacts(http_client_list_response)

Expand Down Expand Up @@ -219,6 +238,7 @@ def test_create_contact(self, http_client_response):
"given_name": "John",
"surname": "Doe",
"company_name": "Nylas",
"metadata": {"key1": "sync_eligible"},
}

contacts.create(identifier="abc-123", request_body=request_body)
Expand All @@ -238,6 +258,7 @@ def test_update_contact(self, http_client_response):
"given_name": "John",
"surname": "Doe",
"company_name": "Nylas",
"metadata": {},
}

contacts.update(
Expand Down
5 changes: 5 additions & 0 deletions tests/resources/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@


class TestWebhooks:
def test_contact_webhook_contract(self):
assert WebhookTriggers.CONTACT_UPDATED.value == "contact.updated"
assert WebhookTriggers.CONTACT_DELETED.value == "contact.deleted"
assert "contact.created" not in {trigger.value for trigger in WebhookTriggers}

def test_webhook_deserialization(self, http_client):
webhook_json = {
"id": "UMWjAjMeWQ4D8gYF2moonK4486",
Expand Down
Loading