From 52a8e09d90e703e6426a6a41a4b8001eb2f33bc3 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 29 Dec 2023 12:17:47 -0500 Subject: [PATCH 1/4] Add contact objects --- nylas/models/contacts.py | 154 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 nylas/models/contacts.py diff --git a/nylas/models/contacts.py b/nylas/models/contacts.py new file mode 100644 index 0000000..730e6dd --- /dev/null +++ b/nylas/models/contacts.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Optional, List +from typing_extensions import TypedDict, NotRequired + +from dataclasses_json import dataclass_json + + +class ContactType(str, Enum): + work = "work" + home = "home" + other = "other" + + +class SourceType(str, Enum): + address_book = "address_book" + inbox = "inbox" + domain = "domain" + + +@dataclass_json +@dataclass +class PhoneNumber: + number: Optional[str] = None + type: Optional[ContactType] = None + + +@dataclass_json +@dataclass +class PhysicalAddress: + format: Optional[str] = None + street_address: Optional[str] = None + city: Optional[str] = None + postal_code: Optional[str] = None + state: Optional[str] = None + country: Optional[str] = None + type: Optional[ContactType] = None + + +@dataclass_json +@dataclass +class WebPage: + url: Optional[str] = None + type: Optional[ContactType] = None + + +@dataclass_json +@dataclass +class ContactEmail: + email: Optional[str] = None + type: Optional[ContactType] = None + + +@dataclass_json +@dataclass +class ContactGroupId: + id: str + + +@dataclass_json +@dataclass +class InstantMessagingAddress: + im_address: Optional[str] = None + type: Optional[ContactType] = None + + +@dataclass_json +@dataclass +class Contact: + id: str + grant_id: str + object: str = "contact" + birthday: Optional[str] = None + company_name: Optional[str] = None + display_name: Optional[str] = None + emails: Optional[List[ContactEmail]] = None + im_addresses: Optional[List[InstantMessagingAddress]] = None + given_name: Optional[str] = None + job_title: Optional[str] = None + manager_name: Optional[str] = None + middle_name: Optional[str] = None + nickname: Optional[str] = None + notes: Optional[str] = None + office_location: Optional[str] = None + picture_url: Optional[str] = None + picture: Optional[str] = None + suffix: Optional[str] = None + surname: Optional[str] = None + source: Optional[SourceType] = None + phone_numbers: Optional[List[PhoneNumber]] = None + physical_addresses: Optional[List[PhysicalAddress]] = None + web_pages: Optional[List[WebPage]] = None + groups: Optional[List[ContactGroupId]] = None + + +class WriteablePhoneNumber(TypedDict): + number: NotRequired[str] + type: NotRequired[ContactType] + + +class WriteablePhysicalAddress(TypedDict): + format: NotRequired[str] + street_address: NotRequired[str] + city: NotRequired[str] + postal_code: NotRequired[str] + state: NotRequired[str] + country: NotRequired[str] + type: NotRequired[ContactType] + + +class WriteableWebPage(TypedDict): + url: NotRequired[str] + type: NotRequired[ContactType] + + +class WriteableContactEmail(TypedDict): + email: NotRequired[str] + type: NotRequired[ContactType] + + +class WriteableContactGroupId(TypedDict): + id: str + + +class WriteableInstantMessagingAddress(TypedDict): + im_address: NotRequired[str] + type: NotRequired[ContactType] + + +class CreateContactRequest(TypedDict): + birthday: NotRequired[str] + company_name: NotRequired[str] + display_name: NotRequired[str] + emails: NotRequired[List[WriteableContactEmail]] + im_addresses: NotRequired[List[WriteableInstantMessagingAddress]] + given_name: NotRequired[str] + job_title: NotRequired[str] + manager_name: NotRequired[str] + middle_name: NotRequired[str] + nickname: NotRequired[str] + notes: NotRequired[str] + office_location: NotRequired[str] + picture_url: NotRequired[str] + picture: NotRequired[str] + suffix: NotRequired[str] + surname: NotRequired[str] + source: NotRequired[SourceType] + phone_numbers: NotRequired[List[WriteablePhoneNumber]] + physical_addresses: NotRequired[List[WriteablePhysicalAddress]] + web_pages: NotRequired[List[WriteableWebPage]] + groups: NotRequired[List[WriteableContactGroupId]] + + +UpdateContactRequest = CreateContactRequest From 1824a9bd269ec66c34fe273cd57e3d06ea08c2df Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:08:54 -0500 Subject: [PATCH 2/4] Add Contact CRUD support --- nylas/models/contacts.py | 32 ++++++++++ nylas/resources/contacts.py | 120 ++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 nylas/resources/contacts.py diff --git a/nylas/models/contacts.py b/nylas/models/contacts.py index 730e6dd..4dccf7b 100644 --- a/nylas/models/contacts.py +++ b/nylas/models/contacts.py @@ -5,6 +5,8 @@ from dataclasses_json import dataclass_json +from nylas.models.list_query_params import ListQueryParams + class ContactType(str, Enum): work = "work" @@ -152,3 +154,33 @@ class CreateContactRequest(TypedDict): UpdateContactRequest = CreateContactRequest + + +class ListContactsQueryParams(ListQueryParams): + """ + Interface of the query parameters for listing calendars. + + Attributes: + limit (NotRequired[int]): The maximum number of objects to return. + This field defaults to 50. The maximum allowed value is 200. + page_token (NotRequired[str]): An identifier that specifies which page of data to return. + This value should be taken from a ListResponse object's next_cursor parameter. + email: Returns the contacts matching the exact contact's email. + phone_number: Returns the contacts matching the contact's exact phone number + source: Returns the contacts matching from the address book or auto-generated contacts from emails. + For example of contacts only from the address book: /contacts?source=address_bookor + for only autogenerated contacts:/contacts?source=inbox` + group: Returns the contacts belonging to the Contact Group matching this ID + recurse: When set to true, returns the contacts also within the specified Contact Group subgroups, + if the group parameter is set. + """ + + email: NotRequired[str] + phone_number: NotRequired[str] + source: NotRequired[SourceType] + group: NotRequired[str] + recurse: NotRequired[bool] + + +class FindContactQueryParams(TypedDict): + profile_picture: NotRequired[bool] diff --git a/nylas/resources/contacts.py b/nylas/resources/contacts.py new file mode 100644 index 0000000..2a453a1 --- /dev/null +++ b/nylas/resources/contacts.py @@ -0,0 +1,120 @@ +from nylas.handler.api_resources import ( + ListableApiResource, + FindableApiResource, + CreatableApiResource, + UpdatableApiResource, + DestroyableApiResource, +) +from nylas.models.contacts import ( + Contact, + CreateContactRequest, + UpdateContactRequest, + ListContactsQueryParams, + FindContactQueryParams, +) +from nylas.models.response import Response, ListResponse, DeleteResponse + + +class Contacts( + ListableApiResource, + FindableApiResource, + CreatableApiResource, + UpdatableApiResource, + DestroyableApiResource, +): + def list( + self, identifier: str, query_params: ListContactsQueryParams = None + ) -> ListResponse[Contact]: + """ + Return all Contacts. + + Args: + identifier: The identifier of the Grant to act upon. + query_params: The query parameters to include in the request. + + Returns: + The list of contacts. + """ + + return super(Contacts, self).list( + path=f"/v3/grants/{identifier}/contacts", + query_params=query_params, + response_type=Contact, + ) + + def find( + self, + identifier: str, + contact_id: str, + query_params: FindContactQueryParams = None, + ) -> Response[Contact]: + """ + Return a Contact. + + Args: + identifier: The identifier of the Grant to act upon. + contact_id: The ID of the contact to retrieve. + query_params: The query parameters to include in the request. + + Returns: + The Contact. + """ + return super(Contacts, self).find( + path=f"/v3/grants/{identifier}/contacts/{contact_id}", + response_type=Contact, + query_params=query_params, + ) + + def create( + self, identifier: str, request_body: CreateContactRequest + ) -> Response[Contact]: + """ + Create a Contact. + + Args: + identifier: The identifier of the Grant to act upon. + request_body: The values to create the Contact with. + + Returns: + The created Contact. + """ + return super(Contacts, self).create( + path=f"/v3/grants/{identifier}/contacts", + response_type=Contact, + request_body=request_body, + ) + + def update( + self, identifier: str, contact_id: str, request_body: UpdateContactRequest + ) -> Response[Contact]: + """ + Update a Contact. + + Args: + identifier: The identifier of the Grant to act upon. + contact_id: The ID of the Contact to update. Use "primary" to refer to the primary Contact associated with the Grant. + request_body: The values to update the Contact with. + + Returns: + The updated Contact. + """ + return super(Contacts, self).update( + path=f"/v3/grants/{identifier}/contacts/{contact_id}", + response_type=Contact, + request_body=request_body, + ) + + def destroy(self, identifier: str, contact_id: str) -> DeleteResponse: + """ + Delete a Contact. + + Args: + identifier: The identifier of the Grant to act upon. + contact_id: The ID of the Contact to delete. Use "primary" to refer to the primary Contact associated with the Grant. + + Returns: + The deletion response. + """ + return super(Contacts, self).destroy( + path=f"/v3/grants/{identifier}/contacts/{contact_id}" + ) From a231311e1049b10231893fc4c5f5db833b7d073e Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:23:38 -0500 Subject: [PATCH 3/4] Add contact group support --- nylas/models/contacts.py | 20 ++++++++++++++++++++ nylas/resources/contacts.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/nylas/models/contacts.py b/nylas/models/contacts.py index 4dccf7b..d24490e 100644 --- a/nylas/models/contacts.py +++ b/nylas/models/contacts.py @@ -184,3 +184,23 @@ class ListContactsQueryParams(ListQueryParams): class FindContactQueryParams(TypedDict): profile_picture: NotRequired[bool] + + +class GroupType(str, Enum): + user = "user" + system = "system" + other = "other" + + +@dataclass_json +@dataclass +class ContactGroup: + id: str + grant_id: str + object: str = "contact_group" + group_type: Optional[GroupType] = None + name: Optional[str] = None + path: Optional[str] = None + + +ListContactGroupsQueryParams = ListQueryParams diff --git a/nylas/resources/contacts.py b/nylas/resources/contacts.py index 2a453a1..28f83a0 100644 --- a/nylas/resources/contacts.py +++ b/nylas/resources/contacts.py @@ -11,6 +11,8 @@ UpdateContactRequest, ListContactsQueryParams, FindContactQueryParams, + ListContactGroupsQueryParams, + ContactGroup, ) from nylas.models.response import Response, ListResponse, DeleteResponse @@ -28,7 +30,7 @@ def list( """ Return all Contacts. - Args: + Attributes: identifier: The identifier of the Grant to act upon. query_params: The query parameters to include in the request. @@ -51,7 +53,7 @@ def find( """ Return a Contact. - Args: + Attributes: identifier: The identifier of the Grant to act upon. contact_id: The ID of the contact to retrieve. query_params: The query parameters to include in the request. @@ -71,7 +73,7 @@ def create( """ Create a Contact. - Args: + Attributes: identifier: The identifier of the Grant to act upon. request_body: The values to create the Contact with. @@ -90,7 +92,7 @@ def update( """ Update a Contact. - Args: + Attributes: identifier: The identifier of the Grant to act upon. contact_id: The ID of the Contact to update. Use "primary" to refer to the primary Contact associated with the Grant. request_body: The values to update the Contact with. @@ -108,7 +110,7 @@ def destroy(self, identifier: str, contact_id: str) -> DeleteResponse: """ Delete a Contact. - Args: + Attributes: identifier: The identifier of the Grant to act upon. contact_id: The ID of the Contact to delete. Use "primary" to refer to the primary Contact associated with the Grant. @@ -118,3 +120,24 @@ def destroy(self, identifier: str, contact_id: str) -> DeleteResponse: return super(Contacts, self).destroy( path=f"/v3/grants/{identifier}/contacts/{contact_id}" ) + + def list_groups( + self, identifier: str, query_params: ListContactGroupsQueryParams = None + ) -> ListResponse[ContactGroup]: + """ + Return all contact groups. + + Attributes: + identifier: The identifier of the Grant to act upon. + query_params: The query parameters to include in the request. + + Returns: + The list of contact groups. + """ + json_response = self._http_client._execute( + method="GET", + path=f"/v3/grants/{identifier}/contacts/groups", + query_params=query_params, + ) + + return ListResponse.from_dict(json_response, ContactGroup) From b331d07c4ae5ed54e4fa23794dc98f7bc97b0763 Mon Sep 17 00:00:00 2001 From: Blag Date: Sat, 30 Dec 2023 09:50:43 -0500 Subject: [PATCH 4/4] Fixes for contacts api Found some bugs on AV-1465-3-0-ga-python-sdk-contacts-api so this PR should address them as I have fully tested them. --- nylas/client.py | 12 +++++++++++- nylas/models/contacts.py | 1 + nylas/models/events.py | 2 +- nylas/resources/auth.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/nylas/client.py b/nylas/client.py index 0707487..0f21d61 100644 --- a/nylas/client.py +++ b/nylas/client.py @@ -12,7 +12,7 @@ from nylas.resources.messages import Messages from nylas.resources.threads import Threads from nylas.resources.webhooks import Webhooks - +from nylas.resources.contacts import Contacts class Client(object): """ @@ -148,3 +148,13 @@ def webhooks(self) -> Webhooks: The Webhooks API. """ return Webhooks(self.http_client) + + @property + def contacts(self) -> Contacts: + """ + Access the Contacts API. + + Returns: + The Contacts API. + """ + return Contacts(self.http_client) diff --git a/nylas/models/contacts.py b/nylas/models/contacts.py index d24490e..46949d9 100644 --- a/nylas/models/contacts.py +++ b/nylas/models/contacts.py @@ -11,6 +11,7 @@ class ContactType(str, Enum): work = "work" home = "home" + mobile = "mobile" other = "other" diff --git a/nylas/models/events.py b/nylas/models/events.py index f146d40..ee88ae8 100644 --- a/nylas/models/events.py +++ b/nylas/models/events.py @@ -310,7 +310,6 @@ class Event: grant_id: str calendar_id: str busy: bool - read_only: bool created_at: int updated_at: int participants: List[Participant] @@ -319,6 +318,7 @@ class Event: default=None, metadata=config(decoder=_decode_conferencing) ) object: str = "event" + read_only: Optional[bool] = None description: Optional[str] = None location: Optional[str] = None ical_uid: Optional[str] = None diff --git a/nylas/resources/auth.py b/nylas/resources/auth.py index 711c027..d4b46d1 100644 --- a/nylas/resources/auth.py +++ b/nylas/resources/auth.py @@ -3,7 +3,7 @@ import urllib.parse import uuid -from nylas.models.grant import CreateGrantRequest, Grant +from nylas.models.grants import CreateGrantRequest, Grant from nylas.models.auth import ( CodeExchangeResponse,