Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
good-names=id,k,v,cc,to,ip
max-line-length=120

[DESIGN]
max-public-methods=25

[MESSAGES CONTROL]
disable=
missing-module-docstring,
Expand Down
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ nylas-python Changelog
======================
Unreleased
----------
* Aligned Lists create support with the public `POST /v3/lists` schema and added create response/schema coverage
* Added Manage Domains service-account auth support with canonical signed request bodies, bearer-auth suppression, encoded domain path segments, and `dmarc`/`arc` verification types
* Added Workspaces resource (`client.workspaces`) with `list`, `find`, `create`, `update` (PATCH), `destroy`, `auto_group`, `manual_assign`, `default`, `policy_id`, and `rule_ids`
* Corrected RedirectUris `update` to use PATCH instead of PUT; added `deleted_at` to the RedirectUri model and made `platform` optional on create
* Verified and extended Applications: added `update` (PATCH `/v3/applications`) and public response fields (`idp_settings`, hosted-authentication legal URLs, `domain`, `blocked`, timestamps)
* Fix draft and other JSON API requests failing with "only JSON and multipart supported" by sending `Content-Type: application/json` instead of `application/json; charset=utf-8`

v6.15.0
----------
* Added Lists support (`Client.lists`, `/v3/lists`): list, create, find, update, and delete lists, plus `list_items`, `add_items`, and `remove_items` for `/v3/lists/{list_id}/items`, with typed request/response models in `nylas.models.lists`
* Added Manage Domains (`Client.domains`, `/v3/admin/domains`): list, create, find, update, delete, `get_info`, and `verify` with models in `nylas.models.domains`; optional `ServiceAccountSigner` (`nylas.handler.service_account`) for service-account headers (`X-Nylas-Kid`, `X-Nylas-Nonce`, `X-Nylas-Timestamp`, `X-Nylas-Signature`) on each `Domains` method; new `cryptography` dependency, RSA signing, and `HttpClient` `serialized_json_body` so signed payloads match the wire body
* Added Manage Domains (`Client.domains`, `/v3/admin/domains`): list, create, find, update, delete, `get_info`, and `verify` with models in `nylas.models.domains`; optional `ServiceAccountSigner` (`nylas.handler.service_account`) for service-account auth headers (`X-Nylas-Kid`, `X-Nylas-Nonce`, `X-Nylas-Timestamp`, `X-Nylas-Signature`) on each `Domains` method; new `cryptography` dependency, RSA signing, bearer-auth suppression for signed domain requests, and `HttpClient` `serialized_json_body` so signed payloads match the wire body
* Added Transactional Send: `Client.transactional_send.send()` for `POST /v3/domains/{domain_name}/messages/send`, with `TransactionalSendMessageRequest` and `TransactionalTemplate` models (JSON and multipart send behavior aligned with grant `messages.send`)
* Added Policies support (`Client.policies`, `/v3/policies`): list, create, find, update, and delete, with typed request/response models in `nylas.models.policies`
* Added Rules support (`Client.rules`): list, create, find, update, and delete for `/v3/rules`, plus `list_evaluations` for `/v3/grants/{grant_id}/rule-evaluations`, with typed request/response models in `nylas.models.rules`
Expand Down Expand Up @@ -548,4 +553,3 @@ Added tests
v0.3.5
------
Drafts can now be sent without an implicit intermediate save to the mail provider.

11 changes: 11 additions & 0 deletions nylas/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from nylas.resources.scheduler import Scheduler
from nylas.resources.notetakers import Notetakers
from nylas.resources.rules import Rules
from nylas.resources.workspaces import Workspaces


class Client:
Expand Down Expand Up @@ -246,3 +247,13 @@ def notetakers(self) -> Notetakers:
The Notetakers API.
"""
return Notetakers(self.http_client)

@property
def workspaces(self) -> Workspaces:
"""
Access the Workspaces API.

Returns:
The Workspaces API.
"""
return Workspaces(self.http_client)
3 changes: 3 additions & 0 deletions nylas/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ class RequestOverrides(TypedDict):
api_uri: The API URI to use for the request.
timeout: The timeout to use for the request.
headers: Additional headers to include in the request.
skip_auth: Suppress the default bearer Authorization header for endpoints
that use a different authentication mechanism.
"""

api_key: NotRequired[str]
api_uri: NotRequired[str]
timeout: NotRequired[int]
headers: NotRequired[dict]
skip_auth: NotRequired[bool]


DEFAULT_REGION = Region.US
Expand Down
14 changes: 10 additions & 4 deletions nylas/handler/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]:
or "connect/revoke" in parsed_url.path
):
parsed_error = NylasOAuthErrorResponse.from_dict(response_data)
raise NylasOAuthError(parsed_error, response.status_code, response.headers)
raise NylasOAuthError(
parsed_error, response.status_code, response.headers
)

parsed_error = NylasApiErrorResponse.from_dict(response_data)
raise NylasApiError(parsed_error, response.status_code, response.headers)
Expand All @@ -47,6 +49,7 @@ def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]:
) from exc
return (response_data, response.headers)


def _build_query_params(base_url: str, query_params: dict = None) -> str:
query_param_parts = []
for key, value in query_params.items():
Expand Down Expand Up @@ -107,7 +110,9 @@ def _execute(
if serialized_json_body is not None and data is None:
json_data = serialized_json_body
elif request_body is not None and data is None:
json_data = json.dumps(request_body, ensure_ascii=False, allow_nan=True).encode("utf-8")
json_data = json.dumps(
request_body, ensure_ascii=False, allow_nan=True
).encode("utf-8")
try:
response = requests.request(
request["method"],
Expand All @@ -128,7 +133,7 @@ def _execute_download_request(
query_params=None,
stream=False,
overrides=None,
) -> Union[bytes, Response,dict]:
) -> Union[bytes, Response, dict]:
request = self._build_request("GET", path, headers, query_params, overrides)

timeout = self.timeout
Expand Down Expand Up @@ -204,8 +209,9 @@ def _build_headers(
headers = {
"X-Nylas-API-Wrapper": "python",
"User-Agent": user_agent_header,
"Authorization": f"Bearer {api_key}",
}
if not (overrides and overrides.get("skip_auth")):
headers["Authorization"] = f"Bearer {api_key}"
if data is not None and data.content_type is not None:
headers["Content-type"] = data.content_type
elif response_body is not None:
Expand Down
148 changes: 143 additions & 5 deletions nylas/models/application_details.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from dataclasses import dataclass, field
from typing import Literal, Optional, List
from typing import Optional, List

from dataclasses_json import dataclass_json
from typing_extensions import TypedDict, NotRequired

from nylas.models.redirect_uri import RedirectUri

Region = Literal["us", "eu"]
""" Literal representing the available Nylas API regions. """
Region = str
""" The Nylas API region (free-form string, e.g. ``us``, ``eu``). """

Environment = Literal["production", "staging", "development", "sandbox"]
""" Literal representing the different Nylas API environments. """
Environment = str
""" The Nylas API environment (free-form string, e.g. ``sandbox``). """


@dataclass_json
Expand Down Expand Up @@ -46,6 +47,8 @@ class HostedAuthentication:
subtitle: Subtitle for the hosted authentication page.
background_color: Background color of the hosted authentication page.
spacing: CSS spacing attribute in px.
terms_of_service_url: URL pointing to the terms of service.
privacy_policy_url: URL pointing to the privacy policy.
"""

background_image_url: Optional[str] = None
Expand All @@ -56,6 +59,23 @@ class HostedAuthentication:
subtitle: Optional[str] = None
background_color: Optional[str] = None
spacing: Optional[int] = None
terms_of_service_url: Optional[str] = None
privacy_policy_url: Optional[str] = None


@dataclass_json
@dataclass
class IdpSettings:
"""
Class representation of identity provider settings for the application.

Attributes:
origins: Comma-separated list of allowed origins.
issuers: Comma-separated list of allowed issuers.
"""

origins: Optional[str] = None
issuers: Optional[str] = None


@dataclass_json
Expand All @@ -70,14 +90,132 @@ class ApplicationDetails:
region: Region identifier.
environment: Environment identifier.
branding: Branding details for the application.
domain: The white-label domain associated with the application, if any.
hosted_authentication: Hosted authentication branding details.
idp_settings: Identity provider settings.
callback_uris: List of redirect URIs.
created_at: Unix timestamp (seconds) when the application was created.
updated_at: Unix timestamp (seconds) when the application was last updated.
blocked: Whether the application is blocked.
"""

application_id: str
organization_id: str
region: Region
environment: Environment
branding: Branding
domain: Optional[str] = None
hosted_authentication: Optional[HostedAuthentication] = None
idp_settings: Optional[IdpSettings] = None
callback_uris: List[RedirectUri] = field(default_factory=list)
created_at: Optional[int] = None
updated_at: Optional[int] = None
blocked: Optional[bool] = None


class WritableBranding(TypedDict):
"""
Class representing branding details for a create/update application call.

Attributes:
name: Name of the application.
icon_url: URL pointing to the application icon.
website_url: Application/publisher website URL.
description: Description of the application.
"""

name: NotRequired[str]
icon_url: NotRequired[str]
website_url: NotRequired[str]
description: NotRequired[str]


class WritableHostedAuthentication(TypedDict):
"""
Class representing hosted authentication details for a create/update application call.

Attributes:
background_image_url: URL pointing to the background image.
alignment: Alignment of the background image.
color_primary: Primary color of the hosted authentication page.
color_secondary: Secondary color of the hosted authentication page.
title: Title of the hosted authentication page.
subtitle: Subtitle for the hosted authentication page.
background_color: Background color of the hosted authentication page.
spacing: CSS spacing attribute in px.
terms_of_service_url: URL pointing to the terms of service.
privacy_policy_url: URL pointing to the privacy policy.
"""

background_image_url: NotRequired[str]
alignment: NotRequired[str]
color_primary: NotRequired[str]
color_secondary: NotRequired[str]
title: NotRequired[str]
subtitle: NotRequired[str]
background_color: NotRequired[str]
spacing: NotRequired[int]
terms_of_service_url: NotRequired[str]
privacy_policy_url: NotRequired[str]


class WritableIdpSettings(TypedDict):
"""
Class representing identity provider settings for a create/update application call.

Attributes:
origins: Comma-separated list of allowed origins.
issuers: Comma-separated list of allowed issuers.
"""

origins: NotRequired[str]
issuers: NotRequired[str]


class WritableAdditionalSettings(TypedDict):
"""
Class representing additional application settings for an update call.

These settings are write-only: they can be set via the update call but are
stripped from every response and are not bound on the application model.

Attributes:
login_url: The login URL.
logout_url: The logout URL.
refresh_token_expiration_absolute: Absolute refresh token expiration.
refresh_token_expiration_idle: Idle refresh token expiration.
rotate_refresh_token: Whether to rotate the refresh token.
allow_query_param_in_redirect_uri: Whether query params are allowed in redirect URIs.
"""

login_url: NotRequired[str]
logout_url: NotRequired[str]
refresh_token_expiration_absolute: NotRequired[int]
refresh_token_expiration_idle: NotRequired[int]
rotate_refresh_token: NotRequired[bool]
allow_query_param_in_redirect_uri: NotRequired[bool]


class UpdateApplicationRequest(TypedDict):
"""
Class representing a request to update a Nylas application.

Note:
``callback_uris`` / ``redirect_uris`` cannot be set via this request; the
server silently ignores them. Manage callback URIs via the dedicated
redirect-uris endpoints. ``additional_settings`` is write-only and is
stripped from the response.

Attributes:
branding: Branding details for the application.
hosted_authentication: Hosted authentication branding details.
idp_settings: Identity provider settings.
domain: The white-label domain associated with the application.
additional_settings: Additional (write-only) application settings.
"""

branding: NotRequired[WritableBranding]
hosted_authentication: NotRequired[WritableHostedAuthentication]
idp_settings: NotRequired[WritableIdpSettings]
domain: NotRequired[str]
additional_settings: NotRequired[WritableAdditionalSettings]
19 changes: 15 additions & 4 deletions nylas/models/domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
from typing import Any, Literal, Optional

from dataclasses_json import config, dataclass_json
from typing_extensions import TypedDict
from typing_extensions import NotRequired, TypedDict

from nylas.models.list_query_params import ListQueryParams

DomainVerificationType = Literal["ownership", "dkim", "spf", "feedback", "mx"]
DomainVerificationRequestType = Literal["ownership", "dkim", "spf", "feedback", "mx"]
DomainVerificationType = Literal[
"ownership", "dkim", "spf", "feedback", "mx", "dmarc", "arc"
]


class DomainVerificationOptions(TypedDict, total=False):
"""Options for domain verification operations."""

key_length: int


class ListDomainsQueryParams(ListQueryParams):
Expand Down Expand Up @@ -37,13 +46,15 @@ class UpdateDomainRequest(TypedDict, total=False):
class GetDomainInfoRequest(TypedDict):
"""Request body for retrieving DNS records for a verification type."""

type: DomainVerificationType
type: DomainVerificationRequestType
options: NotRequired[DomainVerificationOptions]


class VerifyDomainRequest(TypedDict):
"""Request body for triggering DNS verification."""

type: DomainVerificationType
type: DomainVerificationRequestType
options: NotRequired[DomainVerificationOptions]


@dataclass_json
Expand Down
2 changes: 1 addition & 1 deletion nylas/models/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class NylasList:
id: Optional[str] = None
name: Optional[str] = None
description: Optional[str] = None
type: Optional[str] = None
type: Optional[ListType] = None
items_count: Optional[int] = None
application_id: Optional[str] = None
organization_id: Optional[str] = None
Expand Down
6 changes: 4 additions & 2 deletions nylas/models/redirect_uri.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ class RedirectUri:
url: Redirect URL.
platform: Platform identifier.
settings: Configuration settings.
deleted_at: Soft-delete timestamp (Unix seconds); omitted when not deleted.
"""

id: str
url: str
platform: str
settings: Optional[RedirectUriSettings] = None
deleted_at: Optional[int] = None


class WritableRedirectUriSettings(TypedDict):
Expand Down Expand Up @@ -74,12 +76,12 @@ class CreateRedirectUriRequest(TypedDict):

Attributes:
url: Redirect URL.
platform: Platform identifier.
platform: Platform identifier. Optional; defaults to "web" server-side.
settings: Optional settings for the redirect uri.
"""

url: str
platform: str
platform: NotRequired[str]
settings: NotRequired[WritableRedirectUriSettings]


Expand Down
Loading
Loading