diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a746d..efece91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Unreleased ---------- * 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 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` v6.14.3 ---------- diff --git a/nylas/client.py b/nylas/client.py index 1af8d53..a5e3f41 100644 --- a/nylas/client.py +++ b/nylas/client.py @@ -15,6 +15,7 @@ from nylas.resources.drafts import Drafts from nylas.resources.domains import Domains from nylas.resources.grants import Grants +from nylas.resources.policies import Policies from nylas.resources.scheduler import Scheduler from nylas.resources.notetakers import Notetakers @@ -154,6 +155,16 @@ def grants(self) -> Grants: """ return Grants(self.http_client) + @property + def policies(self) -> Policies: + """ + Access the Policies API. + + Returns: + The Policies API. + """ + return Policies(self.http_client) + @property def messages(self) -> Messages: """ diff --git a/nylas/models/auth.py b/nylas/models/auth.py index 6c83628..97481e8 100644 --- a/nylas/models/auth.py +++ b/nylas/models/auth.py @@ -7,7 +7,7 @@ AccessType = Literal["online", "offline"] """ Literal for the access type of the authentication URL. """ -Provider = Literal["google", "imap", "microsoft", "icloud", "virtual-calendar", "yahoo", "ews", "zoom"] +Provider = Literal["google", "imap", "microsoft", "icloud", "virtual-calendar", "yahoo", "ews", "zoom", "nylas"] """ Literal for the different authentication providers. """ Prompt = Literal[ diff --git a/nylas/models/policies.py b/nylas/models/policies.py new file mode 100644 index 0000000..48dbafe --- /dev/null +++ b/nylas/models/policies.py @@ -0,0 +1,118 @@ +from dataclasses import dataclass +from typing import List, Optional + +from dataclasses_json import dataclass_json +from typing_extensions import NotRequired, TypedDict + +from nylas.models.list_query_params import ListQueryParams + + +class ListPoliciesQueryParams(ListQueryParams): + """ + Query parameters for listing policies. + + Attributes: + limit: Maximum number of objects to return. + page_token: Cursor for the next page (from ``next_cursor`` on the previous response). + """ + + pass + + +class PolicyOptionsRequest(TypedDict, total=False): + """Request shape for policy options.""" + + additional_folders: NotRequired[List[str]] + use_cidr_aliasing: NotRequired[bool] + + +class PolicyLimitsRequest(TypedDict, total=False): + """Request shape for policy limits.""" + + limit_attachment_size_limit: NotRequired[int] + limit_attachment_count_limit: NotRequired[int] + limit_attachment_allowed_types: NotRequired[List[str]] + limit_size_total_mime: NotRequired[int] + limit_storage_total: NotRequired[int] + limit_count_daily_message_per_grant: NotRequired[int] + limit_inbox_retention_period: NotRequired[int] + limit_spam_retention_period: NotRequired[int] + + +class PolicySpamDetectionRequest(TypedDict, total=False): + """Request shape for policy spam detection settings.""" + + use_list_dnsbl: NotRequired[bool] + use_header_anomaly_detection: NotRequired[bool] + spam_sensitivity: NotRequired[float] + + +class CreatePolicyRequest(TypedDict): + """Request body for creating a policy.""" + + name: str + options: NotRequired[PolicyOptionsRequest] + limits: NotRequired[PolicyLimitsRequest] + rules: NotRequired[List[str]] + spam_detection: NotRequired[PolicySpamDetectionRequest] + + +class UpdatePolicyRequest(TypedDict, total=False): + """Request body for updating a policy.""" + + name: NotRequired[str] + options: NotRequired[PolicyOptionsRequest] + limits: NotRequired[PolicyLimitsRequest] + rules: NotRequired[List[str]] + spam_detection: NotRequired[PolicySpamDetectionRequest] + + +@dataclass_json +@dataclass +class PolicyOptions: + """Policy options applied to inboxes that use this policy.""" + + additional_folders: Optional[List[str]] = None + use_cidr_aliasing: Optional[bool] = None + + +@dataclass_json +@dataclass +class PolicyLimits: + """Operational limits applied to inboxes that use this policy.""" + + limit_attachment_size_limit: Optional[int] = None + limit_attachment_count_limit: Optional[int] = None + limit_attachment_allowed_types: Optional[List[str]] = None + limit_size_total_mime: Optional[int] = None + limit_storage_total: Optional[int] = None + limit_count_daily_message_per_grant: Optional[int] = None + limit_inbox_retention_period: Optional[int] = None + limit_spam_retention_period: Optional[int] = None + + +@dataclass_json +@dataclass +class PolicySpamDetection: + """Spam detection settings applied to inboxes that use this policy.""" + + use_list_dnsbl: Optional[bool] = None + use_header_anomaly_detection: Optional[bool] = None + spam_sensitivity: Optional[float] = None + + +@dataclass_json +@dataclass +class Policy: + """A policy for Nylas Agent Accounts.""" + + id: Optional[str] = None + name: Optional[str] = None + application_id: Optional[str] = None + organization_id: Optional[str] = None + options: Optional[PolicyOptions] = None + limits: Optional[PolicyLimits] = None + rules: Optional[List[str]] = None + spam_detection: Optional[PolicySpamDetection] = None + created_at: Optional[int] = None + updated_at: Optional[int] = None diff --git a/nylas/resources/policies.py b/nylas/resources/policies.py new file mode 100644 index 0000000..88c8ed2 --- /dev/null +++ b/nylas/resources/policies.py @@ -0,0 +1,81 @@ +from nylas.config import RequestOverrides +from nylas.handler.api_resources import ( + CreatableApiResource, + DestroyableApiResource, + FindableApiResource, + ListableApiResource, + UpdatableApiResource, +) +from nylas.models.policies import ( + CreatePolicyRequest, + ListPoliciesQueryParams, + Policy, + UpdatePolicyRequest, +) +from nylas.models.response import DeleteResponse, ListResponse, Response + + +class Policies( + ListableApiResource, + FindableApiResource, + CreatableApiResource, + UpdatableApiResource, + DestroyableApiResource, +): + """ + Nylas Policies API. + + Policies define operational configuration for Nylas Agent Accounts. + """ + + def list( + self, + query_params: ListPoliciesQueryParams = None, + overrides: RequestOverrides = None, + ) -> ListResponse[Policy]: + return super().list( + path="/v3/policies", + response_type=Policy, + query_params=query_params, + overrides=overrides, + ) + + def create( + self, + request_body: CreatePolicyRequest, + overrides: RequestOverrides = None, + ) -> Response[Policy]: + return super().create( + path="/v3/policies", + request_body=request_body, + response_type=Policy, + overrides=overrides, + ) + + def find( + self, policy_id: str, overrides: RequestOverrides = None + ) -> Response[Policy]: + return super().find( + path=f"/v3/policies/{policy_id}", + response_type=Policy, + overrides=overrides, + ) + + def update( + self, + policy_id: str, + request_body: UpdatePolicyRequest, + overrides: RequestOverrides = None, + ) -> Response[Policy]: + return super().update( + path=f"/v3/policies/{policy_id}", + response_type=Policy, + request_body=request_body, + method="PUT", + overrides=overrides, + ) + + def destroy( + self, policy_id: str, overrides: RequestOverrides = None + ) -> DeleteResponse: + return super().destroy(path=f"/v3/policies/{policy_id}", overrides=overrides) diff --git a/tests/resources/test_policies.py b/tests/resources/test_policies.py new file mode 100644 index 0000000..bd45171 --- /dev/null +++ b/tests/resources/test_policies.py @@ -0,0 +1,238 @@ +from nylas.models.policies import Policy +from nylas.resources.policies import Policies + + +class TestPolicies: + def test_policy_deserialization(self, http_client): + policy_json = { + "id": "policy-123", + "name": "Standard Agent Account Policy", + "application_id": "app-123", + "organization_id": "org-123", + "options": { + "additional_folders": ["processed", "spam-review"], + "use_cidr_aliasing": True, + }, + "limits": { + "limit_attachment_size_limit": 26214400, + "limit_attachment_count_limit": 50, + "limit_attachment_allowed_types": ["image/png", "application/pdf"], + "limit_size_total_mime": 52428800, + "limit_storage_total": 1073741824, + "limit_count_daily_message_per_grant": 1000, + "limit_inbox_retention_period": 365, + "limit_spam_retention_period": 30, + }, + "rules": ["rule-1", "rule-2"], + "spam_detection": { + "use_list_dnsbl": True, + "use_header_anomaly_detection": True, + "spam_sensitivity": 1.5, + }, + "created_at": 1712450952, + "updated_at": 1712450952, + } + + policy = Policy.from_dict(policy_json) + + assert policy.id == "policy-123" + assert policy.name == "Standard Agent Account Policy" + assert policy.application_id == "app-123" + assert policy.organization_id == "org-123" + assert policy.options is not None + assert policy.options.additional_folders == ["processed", "spam-review"] + assert policy.options.use_cidr_aliasing is True + assert policy.limits is not None + assert policy.limits.limit_attachment_size_limit == 26214400 + assert policy.limits.limit_attachment_count_limit == 50 + assert policy.limits.limit_attachment_allowed_types == [ + "image/png", + "application/pdf", + ] + assert policy.limits.limit_size_total_mime == 52428800 + assert policy.limits.limit_storage_total == 1073741824 + assert policy.limits.limit_count_daily_message_per_grant == 1000 + assert policy.limits.limit_inbox_retention_period == 365 + assert policy.limits.limit_spam_retention_period == 30 + assert policy.rules == ["rule-1", "rule-2"] + assert policy.spam_detection is not None + assert policy.spam_detection.use_list_dnsbl is True + assert policy.spam_detection.use_header_anomaly_detection is True + assert policy.spam_detection.spam_sensitivity == 1.5 + assert policy.created_at == 1712450952 + assert policy.updated_at == 1712450952 + + def test_policy_deserialization_with_minimal_fields(self, http_client): + policy_json = { + "id": "policy-123", + "name": "Minimal Policy", + "application_id": "app-123", + "organization_id": "org-123", + } + + policy = Policy.from_dict(policy_json, infer_missing=True) + + assert policy.id == "policy-123" + assert policy.name == "Minimal Policy" + assert policy.application_id == "app-123" + assert policy.organization_id == "org-123" + assert policy.options is None + assert policy.limits is None + assert policy.rules is None + assert policy.spam_detection is None + assert policy.created_at is None + assert policy.updated_at is None + + def test_list_policies(self, http_client_list_response): + policies = Policies(http_client_list_response) + + policies.list() + + http_client_list_response._execute.assert_called_once_with( + "GET", "/v3/policies", None, None, None, overrides=None + ) + + def test_list_policies_with_query_params(self, http_client_list_response): + policies = Policies(http_client_list_response) + + policies.list(query_params={"limit": 10, "page_token": "next-page-token"}) + + http_client_list_response._execute.assert_called_once_with( + "GET", + "/v3/policies", + None, + {"limit": 10, "page_token": "next-page-token"}, + None, + overrides=None, + ) + + def test_list_policies_with_overrides(self, http_client_list_response): + policies = Policies(http_client_list_response) + overrides = {"headers": {"X-Test": "value"}, "timeout": 42} + + policies.list(overrides=overrides) + + http_client_list_response._execute.assert_called_once_with( + "GET", + "/v3/policies", + None, + None, + None, + overrides=overrides, + ) + + def test_create_policy(self, http_client_response): + policies = Policies(http_client_response) + request_body = { + "name": "Standard Agent Account Policy", + "spam_detection": { + "use_list_dnsbl": True, + "use_header_anomaly_detection": True, + "spam_sensitivity": 1.5, + }, + "limits": { + "limit_attachment_size_limit": 26214400, + "limit_attachment_count_limit": 50, + "limit_inbox_retention_period": 365, + "limit_spam_retention_period": 30, + }, + } + + policies.create(request_body) + + http_client_response._execute.assert_called_once_with( + "POST", "/v3/policies", None, None, request_body, overrides=None + ) + + def test_create_policy_with_overrides(self, http_client_response): + policies = Policies(http_client_response) + request_body = {"name": "Standard Agent Account Policy"} + overrides = {"headers": {"X-Test": "value"}} + + policies.create(request_body, overrides=overrides) + + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/policies", + None, + None, + request_body, + overrides=overrides, + ) + + def test_find_policy(self, http_client_response): + policies = Policies(http_client_response) + + policies.find("policy-123") + + http_client_response._execute.assert_called_once_with( + "GET", "/v3/policies/policy-123", None, None, None, overrides=None + ) + + def test_find_policy_with_overrides(self, http_client_response): + policies = Policies(http_client_response) + overrides = {"headers": {"X-Test": "value"}} + + policies.find("policy-123", overrides=overrides) + + http_client_response._execute.assert_called_once_with( + "GET", + "/v3/policies/policy-123", + None, + None, + None, + overrides=overrides, + ) + + def test_update_policy(self, http_client_response): + policies = Policies(http_client_response) + request_body = { + "name": "Updated Agent Policy", + "rules": ["rule-1", "rule-2"], + } + + policies.update("policy-123", request_body) + + http_client_response._execute.assert_called_once_with( + "PUT", "/v3/policies/policy-123", None, None, request_body, overrides=None + ) + + def test_update_policy_with_overrides(self, http_client_response): + policies = Policies(http_client_response) + request_body = {"rules": ["rule-1"]} + overrides = {"headers": {"X-Test": "value"}} + + policies.update("policy-123", request_body, overrides=overrides) + + http_client_response._execute.assert_called_once_with( + "PUT", + "/v3/policies/policy-123", + None, + None, + request_body, + overrides=overrides, + ) + + def test_destroy_policy(self, http_client_delete_response): + policies = Policies(http_client_delete_response) + + policies.destroy("policy-123") + + http_client_delete_response._execute.assert_called_once_with( + "DELETE", "/v3/policies/policy-123", None, None, None, overrides=None + ) + + def test_destroy_policy_with_overrides(self, http_client_delete_response): + policies = Policies(http_client_delete_response) + overrides = {"headers": {"X-Test": "value"}} + + policies.destroy("policy-123", overrides=overrides) + + http_client_delete_response._execute.assert_called_once_with( + "DELETE", + "/v3/policies/policy-123", + None, + None, + None, + overrides=overrides, + ) diff --git a/tests/test_client.py b/tests/test_client.py index 44b3a11..dd01c93 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -11,6 +11,7 @@ from nylas.resources.folders import Folders from nylas.resources.grants import Grants from nylas.resources.messages import Messages +from nylas.resources.policies import Policies from nylas.resources.threads import Threads from nylas.resources.transactional_send import TransactionalSend from nylas.resources.webhooks import Webhooks @@ -81,6 +82,10 @@ def test_client_grants_property(self, client): assert client.grants is not None assert type(client.grants) is Grants + def test_client_policies_property(self, client): + assert client.policies is not None + assert type(client.policies) is Policies + def test_client_messages_property(self, client): assert client.messages is not None assert type(client.messages) is Messages