diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c5b6474..752303a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.18.2" + ".": "7.19.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index fa82f66..04c1236 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 136 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-d2b3bdb6747f4acc997702dbd9f24340a3d85f22c00e1f17af2bd6621358bc77.yml -openapi_spec_hash: b6791652567d04fd87c6117090de7635 -config_hash: 627f8592c19dfb77f91ffbec2a057c3a +configured_endpoints: 138 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-cba470b0c221bcfbe2e8bf51276292285a60a05a4c0b47f5f741706adf1205fc.yml +openapi_spec_hash: c9f1b73fb32f39243d9d95a667ea3341 +config_hash: 6025fdbfe31c3b2d57a71f9885d37294 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc41b5..8f4e4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 7.19.0 (2026-07-09) + +Full Changelog: [v7.18.2...v7.19.0](https://github.com/trycourier/courier-python/compare/v7.18.2...v7.19.0) + +### Features + +* Add bulk user preference upsert and replace endpoints ([53d28d9](https://github.com/trycourier/courier-python/commit/53d28d9260952cdbaecf774fe28bfbf05b688395)) +* Preferences v3 REST fields: section/topic description + publish body ([8f33229](https://github.com/trycourier/courier-python/commit/8f3322973866ce8060729bd3b03b4f7bbf95766e)) + ## 7.18.2 (2026-07-09) Full Changelog: [v7.18.1...v7.18.2](https://github.com/trycourier/courier-python/compare/v7.18.1...v7.18.2) diff --git a/api.md b/api.md index 7aee335..c491bc1 100644 --- a/api.md +++ b/api.md @@ -494,6 +494,7 @@ Types: ```python from courier.types import ( + PublishPreferencesRequest, PublishPreferencesResponse, WorkspacePreferenceCreateRequest, WorkspacePreferenceGetResponse, @@ -512,7 +513,7 @@ Methods: - client.workspace_preferences.retrieve(section_id) -> WorkspacePreferenceGetResponse - client.workspace_preferences.list() -> WorkspacePreferenceListResponse - client.workspace_preferences.archive(section_id) -> None -- client.workspace_preferences.publish() -> PublishPreferencesResponse +- client.workspace_preferences.publish(\*\*params) -> PublishPreferencesResponse - client.workspace_preferences.replace(section_id, \*\*params) -> WorkspacePreferenceGetResponse ## Topics @@ -641,8 +642,11 @@ Types: ```python from courier.types.users import ( + BulkPreferenceTopic, TopicPreference, PreferenceRetrieveResponse, + PreferenceBulkReplaceResponse, + PreferenceBulkUpdateResponse, PreferenceRetrieveTopicResponse, PreferenceUpdateOrCreateTopicResponse, ) @@ -651,6 +655,8 @@ from courier.types.users import ( Methods: - client.users.preferences.retrieve(user_id, \*\*params) -> PreferenceRetrieveResponse +- client.users.preferences.bulk_replace(user_id, \*\*params) -> PreferenceBulkReplaceResponse +- client.users.preferences.bulk_update(user_id, \*\*params) -> PreferenceBulkUpdateResponse - client.users.preferences.delete_topic(topic_id, \*, user_id, \*\*params) -> None - client.users.preferences.retrieve_topic(topic_id, \*, user_id, \*\*params) -> PreferenceRetrieveTopicResponse - client.users.preferences.update_or_create_topic(topic_id, \*, user_id, \*\*params) -> PreferenceUpdateOrCreateTopicResponse diff --git a/pyproject.toml b/pyproject.toml index d9f4536..378feb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "trycourier" -version = "7.18.2" +version = "7.19.0" description = "The official Python library for the Courier API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/courier/_version.py b/src/courier/_version.py index 1dabad9..72ab75e 100644 --- a/src/courier/_version.py +++ b/src/courier/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "courier" -__version__ = "7.18.2" # x-release-please-version +__version__ = "7.19.0" # x-release-please-version diff --git a/src/courier/resources/users/preferences.py b/src/courier/resources/users/preferences.py index 3083999..809b250 100644 --- a/src/courier/resources/users/preferences.py +++ b/src/courier/resources/users/preferences.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Iterable, Optional import httpx @@ -18,12 +18,16 @@ ) from ...types.users import ( preference_retrieve_params, + preference_bulk_update_params, + preference_bulk_replace_params, preference_delete_topic_params, preference_retrieve_topic_params, preference_update_or_create_topic_params, ) from ..._base_client import make_request_options from ...types.users.preference_retrieve_response import PreferenceRetrieveResponse +from ...types.users.preference_bulk_update_response import PreferenceBulkUpdateResponse +from ...types.users.preference_bulk_replace_response import PreferenceBulkReplaceResponse from ...types.users.preference_retrieve_topic_response import PreferenceRetrieveTopicResponse from ...types.users.preference_update_or_create_topic_response import PreferenceUpdateOrCreateTopicResponse @@ -90,6 +94,134 @@ def retrieve( cast_to=PreferenceRetrieveResponse, ) + def bulk_replace( + self, + user_id: str, + *, + topics: Iterable[preference_bulk_replace_params.Topic], + tenant_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PreferenceBulkReplaceResponse: + """Replace a user's complete set of preference overrides in a single request. + + The + topics in the request body become the recipient's entire set of overrides: + listed topics are created or updated, and every existing override that is not + included in the body is reset to its topic default. Submitting an empty `topics` + array is a valid clear-all that resets every existing override. + + This operation is validation-atomic (all-or-nothing): structural validation + fails fast with a single `400`, and if any topic is semantically invalid (an + unknown topic, a `REQUIRED` topic that cannot be opted out, or a custom routing + request that is not available on the workspace's plan) the request returns a + single `400` aggregating every failure in `errors` and writes nothing. On + success it returns `200` with `items` (the complete resulting override set) and + `deleted` (the ids of the overrides that were reset to default). + + Every `topic_id` in the response — in `items`, `deleted`, and any `errors` — is + returned in Courier's canonical topic id form, regardless of the form supplied + in the request. + + Args: + topics: The complete set of topic overrides for the user. Up to 50 topics may be + provided. Any existing override not listed here is reset to its topic default; + an empty array resets every existing override. + + tenant_id: Update the preferences of a user for this specific tenant context. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._put( + path_template("/users/{user_id}/preferences", user_id=user_id), + body=maybe_transform({"topics": topics}, preference_bulk_replace_params.PreferenceBulkReplaceParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"tenant_id": tenant_id}, preference_bulk_replace_params.PreferenceBulkReplaceParams + ), + ), + cast_to=PreferenceBulkReplaceResponse, + ) + + def bulk_update( + self, + user_id: str, + *, + topics: Iterable[preference_bulk_update_params.Topic], + tenant_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PreferenceBulkUpdateResponse: + """ + Additively create or update a user's preferences for one or more subscription + topics in a single request. Only the topics included in the request body are + created or updated; any existing overrides for topics not listed are left + untouched. + + Structural validation of the request body fails fast with a single `400`. Beyond + that, each topic is processed independently (partial-success, not + all-or-nothing): valid topics are written and returned in `items`, while topics + that cannot be applied are collected in `errors` with a per-topic `reason` (for + example an unknown topic, a `REQUIRED` topic that cannot be opted out, a custom + routing request that is not available on the workspace's plan, or a write + failure). The request therefore returns `200` with both lists whenever the body + is structurally valid. + + Every `topic_id` in the response — in both `items` and `errors` — is returned in + Courier's canonical topic id form, regardless of the form supplied in the + request. + + Args: + topics: The topics to create or update. Between 1 and 50 topics may be provided in a + single request. + + tenant_id: Update the preferences of a user for this specific tenant context. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._post( + path_template("/users/{user_id}/preferences", user_id=user_id), + body=maybe_transform({"topics": topics}, preference_bulk_update_params.PreferenceBulkUpdateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"tenant_id": tenant_id}, preference_bulk_update_params.PreferenceBulkUpdateParams + ), + ), + cast_to=PreferenceBulkUpdateResponse, + ) + def delete_topic( self, topic_id: str, @@ -296,6 +428,138 @@ async def retrieve( cast_to=PreferenceRetrieveResponse, ) + async def bulk_replace( + self, + user_id: str, + *, + topics: Iterable[preference_bulk_replace_params.Topic], + tenant_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PreferenceBulkReplaceResponse: + """Replace a user's complete set of preference overrides in a single request. + + The + topics in the request body become the recipient's entire set of overrides: + listed topics are created or updated, and every existing override that is not + included in the body is reset to its topic default. Submitting an empty `topics` + array is a valid clear-all that resets every existing override. + + This operation is validation-atomic (all-or-nothing): structural validation + fails fast with a single `400`, and if any topic is semantically invalid (an + unknown topic, a `REQUIRED` topic that cannot be opted out, or a custom routing + request that is not available on the workspace's plan) the request returns a + single `400` aggregating every failure in `errors` and writes nothing. On + success it returns `200` with `items` (the complete resulting override set) and + `deleted` (the ids of the overrides that were reset to default). + + Every `topic_id` in the response — in `items`, `deleted`, and any `errors` — is + returned in Courier's canonical topic id form, regardless of the form supplied + in the request. + + Args: + topics: The complete set of topic overrides for the user. Up to 50 topics may be + provided. Any existing override not listed here is reset to its topic default; + an empty array resets every existing override. + + tenant_id: Update the preferences of a user for this specific tenant context. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._put( + path_template("/users/{user_id}/preferences", user_id=user_id), + body=await async_maybe_transform( + {"topics": topics}, preference_bulk_replace_params.PreferenceBulkReplaceParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"tenant_id": tenant_id}, preference_bulk_replace_params.PreferenceBulkReplaceParams + ), + ), + cast_to=PreferenceBulkReplaceResponse, + ) + + async def bulk_update( + self, + user_id: str, + *, + topics: Iterable[preference_bulk_update_params.Topic], + tenant_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PreferenceBulkUpdateResponse: + """ + Additively create or update a user's preferences for one or more subscription + topics in a single request. Only the topics included in the request body are + created or updated; any existing overrides for topics not listed are left + untouched. + + Structural validation of the request body fails fast with a single `400`. Beyond + that, each topic is processed independently (partial-success, not + all-or-nothing): valid topics are written and returned in `items`, while topics + that cannot be applied are collected in `errors` with a per-topic `reason` (for + example an unknown topic, a `REQUIRED` topic that cannot be opted out, a custom + routing request that is not available on the workspace's plan, or a write + failure). The request therefore returns `200` with both lists whenever the body + is structurally valid. + + Every `topic_id` in the response — in both `items` and `errors` — is returned in + Courier's canonical topic id form, regardless of the form supplied in the + request. + + Args: + topics: The topics to create or update. Between 1 and 50 topics may be provided in a + single request. + + tenant_id: Update the preferences of a user for this specific tenant context. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._post( + path_template("/users/{user_id}/preferences", user_id=user_id), + body=await async_maybe_transform( + {"topics": topics}, preference_bulk_update_params.PreferenceBulkUpdateParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"tenant_id": tenant_id}, preference_bulk_update_params.PreferenceBulkUpdateParams + ), + ), + cast_to=PreferenceBulkUpdateResponse, + ) + async def delete_topic( self, topic_id: str, @@ -447,6 +711,12 @@ def __init__(self, preferences: PreferencesResource) -> None: self.retrieve = to_raw_response_wrapper( preferences.retrieve, ) + self.bulk_replace = to_raw_response_wrapper( + preferences.bulk_replace, + ) + self.bulk_update = to_raw_response_wrapper( + preferences.bulk_update, + ) self.delete_topic = to_raw_response_wrapper( preferences.delete_topic, ) @@ -465,6 +735,12 @@ def __init__(self, preferences: AsyncPreferencesResource) -> None: self.retrieve = async_to_raw_response_wrapper( preferences.retrieve, ) + self.bulk_replace = async_to_raw_response_wrapper( + preferences.bulk_replace, + ) + self.bulk_update = async_to_raw_response_wrapper( + preferences.bulk_update, + ) self.delete_topic = async_to_raw_response_wrapper( preferences.delete_topic, ) @@ -483,6 +759,12 @@ def __init__(self, preferences: PreferencesResource) -> None: self.retrieve = to_streamed_response_wrapper( preferences.retrieve, ) + self.bulk_replace = to_streamed_response_wrapper( + preferences.bulk_replace, + ) + self.bulk_update = to_streamed_response_wrapper( + preferences.bulk_update, + ) self.delete_topic = to_streamed_response_wrapper( preferences.delete_topic, ) @@ -501,6 +783,12 @@ def __init__(self, preferences: AsyncPreferencesResource) -> None: self.retrieve = async_to_streamed_response_wrapper( preferences.retrieve, ) + self.bulk_replace = async_to_streamed_response_wrapper( + preferences.bulk_replace, + ) + self.bulk_update = async_to_streamed_response_wrapper( + preferences.bulk_update, + ) self.delete_topic = async_to_streamed_response_wrapper( preferences.delete_topic, ) diff --git a/src/courier/resources/workspace_preferences/topics.py b/src/courier/resources/workspace_preferences/topics.py index 6b06915..7c2ca93 100644 --- a/src/courier/resources/workspace_preferences/topics.py +++ b/src/courier/resources/workspace_preferences/topics.py @@ -53,6 +53,7 @@ def create( default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"], name: str, allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit, + description: Optional[str] | Omit = omit, include_unsubscribe_header: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, topic_data: Optional[Dict[str, object]] | Omit = omit, @@ -77,6 +78,8 @@ def create( allowed_preferences: Preference controls a recipient may customize for this topic. Defaults to empty if omitted. + description: Optional description shown under the topic on the hosted preferences page. + include_unsubscribe_header: Whether to include a list-unsubscribe header on emails for this topic. routing_options: Default channels delivered for this topic. Defaults to empty if omitted. @@ -100,6 +103,7 @@ def create( "default_status": default_status, "name": name, "allowed_preferences": allowed_preferences, + "description": description, "include_unsubscribe_header": include_unsubscribe_header, "routing_options": routing_options, "topic_data": topic_data, @@ -235,6 +239,7 @@ def replace( default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"], name: str, allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit, + description: Optional[str] | Omit = omit, include_unsubscribe_header: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, topic_data: Optional[Dict[str, object]] | Omit = omit, @@ -257,6 +262,9 @@ def replace( allowed_preferences: Preference controls a recipient may customize. Omit to clear. + description: Optional description shown under the topic on the hosted preferences page. Omit + to clear. + include_unsubscribe_header: Whether to include a list-unsubscribe header on emails for this topic. routing_options: Default channels delivered for this topic. Omit to clear. @@ -284,6 +292,7 @@ def replace( "default_status": default_status, "name": name, "allowed_preferences": allowed_preferences, + "description": description, "include_unsubscribe_header": include_unsubscribe_header, "routing_options": routing_options, "topic_data": topic_data, @@ -324,6 +333,7 @@ async def create( default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"], name: str, allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit, + description: Optional[str] | Omit = omit, include_unsubscribe_header: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, topic_data: Optional[Dict[str, object]] | Omit = omit, @@ -348,6 +358,8 @@ async def create( allowed_preferences: Preference controls a recipient may customize for this topic. Defaults to empty if omitted. + description: Optional description shown under the topic on the hosted preferences page. + include_unsubscribe_header: Whether to include a list-unsubscribe header on emails for this topic. routing_options: Default channels delivered for this topic. Defaults to empty if omitted. @@ -371,6 +383,7 @@ async def create( "default_status": default_status, "name": name, "allowed_preferences": allowed_preferences, + "description": description, "include_unsubscribe_header": include_unsubscribe_header, "routing_options": routing_options, "topic_data": topic_data, @@ -506,6 +519,7 @@ async def replace( default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"], name: str, allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit, + description: Optional[str] | Omit = omit, include_unsubscribe_header: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, topic_data: Optional[Dict[str, object]] | Omit = omit, @@ -528,6 +542,9 @@ async def replace( allowed_preferences: Preference controls a recipient may customize. Omit to clear. + description: Optional description shown under the topic on the hosted preferences page. Omit + to clear. + include_unsubscribe_header: Whether to include a list-unsubscribe header on emails for this topic. routing_options: Default channels delivered for this topic. Omit to clear. @@ -555,6 +572,7 @@ async def replace( "default_status": default_status, "name": name, "allowed_preferences": allowed_preferences, + "description": description, "include_unsubscribe_header": include_unsubscribe_header, "routing_options": routing_options, "topic_data": topic_data, diff --git a/src/courier/resources/workspace_preferences/workspace_preferences.py b/src/courier/resources/workspace_preferences/workspace_preferences.py index 98dc816..361f955 100644 --- a/src/courier/resources/workspace_preferences/workspace_preferences.py +++ b/src/courier/resources/workspace_preferences/workspace_preferences.py @@ -14,7 +14,11 @@ TopicsResourceWithStreamingResponse, AsyncTopicsResourceWithStreamingResponse, ) -from ...types import workspace_preference_create_params, workspace_preference_replace_params +from ...types import ( + workspace_preference_create_params, + workspace_preference_publish_params, + workspace_preference_replace_params, +) from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property @@ -62,6 +66,7 @@ def create( self, *, name: str, + description: Optional[str] | Omit = omit, has_custom_routing: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -80,6 +85,8 @@ def create( Args: name: Human-readable name for the workspace preference. + description: Optional description shown under the section on the hosted preferences page. + has_custom_routing: Whether the workspace preference defines custom routing for its topics. routing_options: Default channels for the workspace preference. Defaults to empty if omitted. @@ -97,6 +104,7 @@ def create( body=maybe_transform( { "name": name, + "description": description, "has_custom_routing": has_custom_routing, "routing_options": routing_options, }, @@ -203,6 +211,9 @@ def archive( def publish( self, *, + brand_id: Optional[str] | Omit = omit, + description: Optional[str] | Omit = omit, + heading: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -215,9 +226,33 @@ def publish( Takes a snapshot of every workspace preference with its topics under a new published version, making the current state visible on the hosted preferences page (non-draft). + + Args: + brand_id: Brand for the hosted page - "default" (workspace default brand), "none" (no + brand), or a specific brand id. Defaults to "default". + + description: Description shown under the heading on the hosted preferences page. + + heading: Heading shown at the top of the hosted preferences page. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds """ return self._post( "/preferences/publish", + body=maybe_transform( + { + "brand_id": brand_id, + "description": description, + "heading": heading, + }, + workspace_preference_publish_params.WorkspacePreferencePublishParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -229,6 +264,7 @@ def replace( section_id: str, *, name: str, + description: Optional[str] | Omit = omit, has_custom_routing: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -246,6 +282,9 @@ def replace( Args: name: Human-readable name for the workspace preference. + description: Optional description shown under the section on the hosted preferences page. + Omit to clear. + has_custom_routing: Whether the workspace preference defines custom routing for its topics. routing_options: Default channels for the workspace preference. Omit to clear. @@ -265,6 +304,7 @@ def replace( body=maybe_transform( { "name": name, + "description": description, "has_custom_routing": has_custom_routing, "routing_options": routing_options, }, @@ -305,6 +345,7 @@ async def create( self, *, name: str, + description: Optional[str] | Omit = omit, has_custom_routing: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -323,6 +364,8 @@ async def create( Args: name: Human-readable name for the workspace preference. + description: Optional description shown under the section on the hosted preferences page. + has_custom_routing: Whether the workspace preference defines custom routing for its topics. routing_options: Default channels for the workspace preference. Defaults to empty if omitted. @@ -340,6 +383,7 @@ async def create( body=await async_maybe_transform( { "name": name, + "description": description, "has_custom_routing": has_custom_routing, "routing_options": routing_options, }, @@ -446,6 +490,9 @@ async def archive( async def publish( self, *, + brand_id: Optional[str] | Omit = omit, + description: Optional[str] | Omit = omit, + heading: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -458,9 +505,33 @@ async def publish( Takes a snapshot of every workspace preference with its topics under a new published version, making the current state visible on the hosted preferences page (non-draft). + + Args: + brand_id: Brand for the hosted page - "default" (workspace default brand), "none" (no + brand), or a specific brand id. Defaults to "default". + + description: Description shown under the heading on the hosted preferences page. + + heading: Heading shown at the top of the hosted preferences page. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( "/preferences/publish", + body=await async_maybe_transform( + { + "brand_id": brand_id, + "description": description, + "heading": heading, + }, + workspace_preference_publish_params.WorkspacePreferencePublishParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -472,6 +543,7 @@ async def replace( section_id: str, *, name: str, + description: Optional[str] | Omit = omit, has_custom_routing: Optional[bool] | Omit = omit, routing_options: Optional[List[ChannelClassification]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -489,6 +561,9 @@ async def replace( Args: name: Human-readable name for the workspace preference. + description: Optional description shown under the section on the hosted preferences page. + Omit to clear. + has_custom_routing: Whether the workspace preference defines custom routing for its topics. routing_options: Default channels for the workspace preference. Omit to clear. @@ -508,6 +583,7 @@ async def replace( body=await async_maybe_transform( { "name": name, + "description": description, "has_custom_routing": has_custom_routing, "routing_options": routing_options, }, diff --git a/src/courier/types/__init__.py b/src/courier/types/__init__.py index 8861c80..9c9ec09 100644 --- a/src/courier/types/__init__.py +++ b/src/courier/types/__init__.py @@ -286,6 +286,7 @@ from .journey_fetch_get_delete_node_param import JourneyFetchGetDeleteNodeParam as JourneyFetchGetDeleteNodeParam from .journey_throttle_dynamic_node_param import JourneyThrottleDynamicNodeParam as JourneyThrottleDynamicNodeParam from .notification_template_payload_param import NotificationTemplatePayloadParam as NotificationTemplatePayloadParam +from .workspace_preference_publish_params import WorkspacePreferencePublishParams as WorkspacePreferencePublishParams from .workspace_preference_replace_params import WorkspacePreferenceReplaceParams as WorkspacePreferenceReplaceParams from .journey_condition_nested_group_param import JourneyConditionNestedGroupParam as JourneyConditionNestedGroupParam from .notification_retrieve_content_params import NotificationRetrieveContentParams as NotificationRetrieveContentParams diff --git a/src/courier/types/users/__init__.py b/src/courier/types/users/__init__.py index 3a23aaf..94bf6ed 100644 --- a/src/courier/types/users/__init__.py +++ b/src/courier/types/users/__init__.py @@ -8,13 +8,18 @@ from .token_list_response import TokenListResponse as TokenListResponse from .token_update_params import TokenUpdateParams as TokenUpdateParams from .tenant_list_response import TenantListResponse as TenantListResponse +from .bulk_preference_topic import BulkPreferenceTopic as BulkPreferenceTopic from .token_add_single_params import TokenAddSingleParams as TokenAddSingleParams from .token_retrieve_response import TokenRetrieveResponse as TokenRetrieveResponse from .tenant_add_single_params import TenantAddSingleParams as TenantAddSingleParams from .preference_retrieve_params import PreferenceRetrieveParams as PreferenceRetrieveParams from .tenant_add_multiple_params import TenantAddMultipleParams as TenantAddMultipleParams from .preference_retrieve_response import PreferenceRetrieveResponse as PreferenceRetrieveResponse +from .preference_bulk_update_params import PreferenceBulkUpdateParams as PreferenceBulkUpdateParams +from .preference_bulk_replace_params import PreferenceBulkReplaceParams as PreferenceBulkReplaceParams from .preference_delete_topic_params import PreferenceDeleteTopicParams as PreferenceDeleteTopicParams +from .preference_bulk_update_response import PreferenceBulkUpdateResponse as PreferenceBulkUpdateResponse +from .preference_bulk_replace_response import PreferenceBulkReplaceResponse as PreferenceBulkReplaceResponse from .preference_retrieve_topic_params import PreferenceRetrieveTopicParams as PreferenceRetrieveTopicParams from .preference_retrieve_topic_response import PreferenceRetrieveTopicResponse as PreferenceRetrieveTopicResponse from .preference_update_or_create_topic_params import ( diff --git a/src/courier/types/users/bulk_preference_topic.py b/src/courier/types/users/bulk_preference_topic.py new file mode 100644 index 0000000..b36d109 --- /dev/null +++ b/src/courier/types/users/bulk_preference_topic.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import Literal + +from ..._models import BaseModel +from ..shared.channel_classification import ChannelClassification + +__all__ = ["BulkPreferenceTopic"] + + +class BulkPreferenceTopic(BaseModel): + """A single topic override echoed in a bulk preference response.""" + + custom_routing: List[ChannelClassification] + + has_custom_routing: bool + + status: Literal["OPTED_IN", "OPTED_OUT"] + """The applied subscription status. + + Echoes the requested value, so it is always OPTED_IN or OPTED_OUT. + """ + + topic_id: str diff --git a/src/courier/types/users/preference_bulk_replace_params.py b/src/courier/types/users/preference_bulk_replace_params.py new file mode 100644 index 0000000..b0ab115 --- /dev/null +++ b/src/courier/types/users/preference_bulk_replace_params.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from ..shared.channel_classification import ChannelClassification + +__all__ = ["PreferenceBulkReplaceParams", "Topic"] + + +class PreferenceBulkReplaceParams(TypedDict, total=False): + topics: Required[Iterable[Topic]] + """The complete set of topic overrides for the user. + + Up to 50 topics may be provided. Any existing override not listed here is reset + to its topic default; an empty array resets every existing override. + """ + + tenant_id: Optional[str] + """Update the preferences of a user for this specific tenant context.""" + + +class Topic(TypedDict, total=False): + status: Required[Literal["OPTED_IN", "OPTED_OUT"]] + """The subscription status to apply for this topic.""" + + topic_id: Required[str] + """A unique identifier associated with a subscription topic.""" + + custom_routing: List[ChannelClassification] + """The channels a user has chosen to receive notifications through for this topic.""" + + has_custom_routing: bool + """Whether the recipient has chosen specific delivery channels for this topic.""" diff --git a/src/courier/types/users/preference_bulk_replace_response.py b/src/courier/types/users/preference_bulk_replace_response.py new file mode 100644 index 0000000..2b3c294 --- /dev/null +++ b/src/courier/types/users/preference_bulk_replace_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from ..._models import BaseModel +from .bulk_preference_topic import BulkPreferenceTopic + +__all__ = ["PreferenceBulkReplaceResponse"] + + +class PreferenceBulkReplaceResponse(BaseModel): + deleted: List[str] + """The ids of the overrides that were reset to their topic default.""" + + items: List[BulkPreferenceTopic] + """The complete resulting set of topic overrides for the user.""" diff --git a/src/courier/types/users/preference_bulk_update_params.py b/src/courier/types/users/preference_bulk_update_params.py new file mode 100644 index 0000000..70f7964 --- /dev/null +++ b/src/courier/types/users/preference_bulk_update_params.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from ..shared.channel_classification import ChannelClassification + +__all__ = ["PreferenceBulkUpdateParams", "Topic"] + + +class PreferenceBulkUpdateParams(TypedDict, total=False): + topics: Required[Iterable[Topic]] + """The topics to create or update. + + Between 1 and 50 topics may be provided in a single request. + """ + + tenant_id: Optional[str] + """Update the preferences of a user for this specific tenant context.""" + + +class Topic(TypedDict, total=False): + status: Required[Literal["OPTED_IN", "OPTED_OUT"]] + """The subscription status to apply for this topic.""" + + topic_id: Required[str] + """A unique identifier associated with a subscription topic.""" + + custom_routing: List[ChannelClassification] + """The channels a user has chosen to receive notifications through for this topic.""" + + has_custom_routing: bool + """Whether the recipient has chosen specific delivery channels for this topic.""" diff --git a/src/courier/types/users/preference_bulk_update_response.py b/src/courier/types/users/preference_bulk_update_response.py new file mode 100644 index 0000000..d5eaa39 --- /dev/null +++ b/src/courier/types/users/preference_bulk_update_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from ..._models import BaseModel +from .bulk_preference_topic import BulkPreferenceTopic + +__all__ = ["PreferenceBulkUpdateResponse", "Error"] + + +class Error(BaseModel): + """A single topic that could not be applied in a bulk preference request.""" + + reason: str + """A human-readable explanation of why the topic could not be applied.""" + + topic_id: str + + +class PreferenceBulkUpdateResponse(BaseModel): + errors: List[Error] + """The topics that could not be applied, each with a reason.""" + + items: List[BulkPreferenceTopic] + """The topics that were successfully created or updated.""" diff --git a/src/courier/types/workspace_preference_create_params.py b/src/courier/types/workspace_preference_create_params.py index b4057cf..8f3367c 100644 --- a/src/courier/types/workspace_preference_create_params.py +++ b/src/courier/types/workspace_preference_create_params.py @@ -14,6 +14,9 @@ class WorkspacePreferenceCreateParams(TypedDict, total=False): name: Required[str] """Human-readable name for the workspace preference.""" + description: Optional[str] + """Optional description shown under the section on the hosted preferences page.""" + has_custom_routing: Optional[bool] """Whether the workspace preference defines custom routing for its topics.""" diff --git a/src/courier/types/workspace_preference_get_response.py b/src/courier/types/workspace_preference_get_response.py index d702830..d9d0de7 100644 --- a/src/courier/types/workspace_preference_get_response.py +++ b/src/courier/types/workspace_preference_get_response.py @@ -33,6 +33,9 @@ class WorkspacePreferenceGetResponse(BaseModel): creator: Optional[str] = None """Id of the creator.""" + description: Optional[str] = None + """Optional description shown under the section on the hosted preferences page.""" + updated: Optional[str] = None """ISO-8601 timestamp of the last update.""" diff --git a/src/courier/types/workspace_preference_publish_params.py b/src/courier/types/workspace_preference_publish_params.py new file mode 100644 index 0000000..180318c --- /dev/null +++ b/src/courier/types/workspace_preference_publish_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["WorkspacePreferencePublishParams"] + + +class WorkspacePreferencePublishParams(TypedDict, total=False): + brand_id: Optional[str] + """ + Brand for the hosted page - "default" (workspace default brand), "none" (no + brand), or a specific brand id. Defaults to "default". + """ + + description: Optional[str] + """Description shown under the heading on the hosted preferences page.""" + + heading: Optional[str] + """Heading shown at the top of the hosted preferences page.""" diff --git a/src/courier/types/workspace_preference_replace_params.py b/src/courier/types/workspace_preference_replace_params.py index 9376c2a..79f6092 100644 --- a/src/courier/types/workspace_preference_replace_params.py +++ b/src/courier/types/workspace_preference_replace_params.py @@ -14,6 +14,12 @@ class WorkspacePreferenceReplaceParams(TypedDict, total=False): name: Required[str] """Human-readable name for the workspace preference.""" + description: Optional[str] + """Optional description shown under the section on the hosted preferences page. + + Omit to clear. + """ + has_custom_routing: Optional[bool] """Whether the workspace preference defines custom routing for its topics.""" diff --git a/src/courier/types/workspace_preference_topic_get_response.py b/src/courier/types/workspace_preference_topic_get_response.py index d76c638..bf10ef4 100644 --- a/src/courier/types/workspace_preference_topic_get_response.py +++ b/src/courier/types/workspace_preference_topic_get_response.py @@ -42,5 +42,8 @@ class WorkspacePreferenceTopicGetResponse(BaseModel): creator: Optional[str] = None """Id of the creator.""" + description: Optional[str] = None + """Optional description shown under the topic on the hosted preferences page.""" + updater: Optional[str] = None """Id of the last updater.""" diff --git a/src/courier/types/workspace_preferences/topic_create_params.py b/src/courier/types/workspace_preferences/topic_create_params.py index 4fdd262..698c942 100644 --- a/src/courier/types/workspace_preferences/topic_create_params.py +++ b/src/courier/types/workspace_preferences/topic_create_params.py @@ -23,6 +23,9 @@ class TopicCreateParams(TypedDict, total=False): Defaults to empty if omitted. """ + description: Optional[str] + """Optional description shown under the topic on the hosted preferences page.""" + include_unsubscribe_header: Optional[bool] """Whether to include a list-unsubscribe header on emails for this topic.""" diff --git a/src/courier/types/workspace_preferences/topic_replace_params.py b/src/courier/types/workspace_preferences/topic_replace_params.py index 7e6c593..7f2e324 100644 --- a/src/courier/types/workspace_preferences/topic_replace_params.py +++ b/src/courier/types/workspace_preferences/topic_replace_params.py @@ -22,6 +22,12 @@ class TopicReplaceParams(TypedDict, total=False): allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] """Preference controls a recipient may customize. Omit to clear.""" + description: Optional[str] + """Optional description shown under the topic on the hosted preferences page. + + Omit to clear. + """ + include_unsubscribe_header: Optional[bool] """Whether to include a list-unsubscribe header on emails for this topic.""" diff --git a/tests/api_resources/test_workspace_preferences.py b/tests/api_resources/test_workspace_preferences.py index b22c930..791e166 100644 --- a/tests/api_resources/test_workspace_preferences.py +++ b/tests/api_resources/test_workspace_preferences.py @@ -34,6 +34,7 @@ def test_method_create(self, client: Courier) -> None: def test_method_create_with_all_params(self, client: Courier) -> None: workspace_preference = client.workspace_preferences.create( name="Account Notifications", + description="description", has_custom_routing=True, routing_options=["direct_message"], ) @@ -183,6 +184,16 @@ def test_method_publish(self, client: Courier) -> None: workspace_preference = client.workspace_preferences.publish() assert_matches_type(PublishPreferencesResponse, workspace_preference, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_publish_with_all_params(self, client: Courier) -> None: + workspace_preference = client.workspace_preferences.publish( + brand_id="brand_id", + description="description", + heading="heading", + ) + assert_matches_type(PublishPreferencesResponse, workspace_preference, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_publish(self, client: Courier) -> None: @@ -220,6 +231,7 @@ def test_method_replace_with_all_params(self, client: Courier) -> None: workspace_preference = client.workspace_preferences.replace( section_id="section_id", name="name", + description="description", has_custom_routing=True, routing_options=["direct_message"], ) @@ -281,6 +293,7 @@ async def test_method_create(self, async_client: AsyncCourier) -> None: async def test_method_create_with_all_params(self, async_client: AsyncCourier) -> None: workspace_preference = await async_client.workspace_preferences.create( name="Account Notifications", + description="description", has_custom_routing=True, routing_options=["direct_message"], ) @@ -430,6 +443,16 @@ async def test_method_publish(self, async_client: AsyncCourier) -> None: workspace_preference = await async_client.workspace_preferences.publish() assert_matches_type(PublishPreferencesResponse, workspace_preference, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_publish_with_all_params(self, async_client: AsyncCourier) -> None: + workspace_preference = await async_client.workspace_preferences.publish( + brand_id="brand_id", + description="description", + heading="heading", + ) + assert_matches_type(PublishPreferencesResponse, workspace_preference, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_publish(self, async_client: AsyncCourier) -> None: @@ -467,6 +490,7 @@ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) workspace_preference = await async_client.workspace_preferences.replace( section_id="section_id", name="name", + description="description", has_custom_routing=True, routing_options=["direct_message"], ) diff --git a/tests/api_resources/users/test_preferences.py b/tests/api_resources/users/test_preferences.py index a5ee71e..3bc7848 100644 --- a/tests/api_resources/users/test_preferences.py +++ b/tests/api_resources/users/test_preferences.py @@ -11,6 +11,8 @@ from tests.utils import assert_matches_type from courier.types.users import ( PreferenceRetrieveResponse, + PreferenceBulkUpdateResponse, + PreferenceBulkReplaceResponse, PreferenceRetrieveTopicResponse, PreferenceUpdateOrCreateTopicResponse, ) @@ -72,6 +74,194 @@ def test_path_params_retrieve(self, client: Courier) -> None: user_id="", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_bulk_replace(self, client: Courier) -> None: + preference = client.users.preferences.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_bulk_replace_with_all_params(self, client: Courier) -> None: + preference = client.users.preferences.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + "custom_routing": ["inbox", "email"], + "has_custom_routing": True, + } + ], + tenant_id="tenant_id", + ) + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_bulk_replace(self, client: Courier) -> None: + response = client.users.preferences.with_raw_response.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + preference = response.parse() + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_bulk_replace(self, client: Courier) -> None: + with client.users.preferences.with_streaming_response.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + preference = response.parse() + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_bulk_replace(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + client.users.preferences.with_raw_response.bulk_replace( + user_id="", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_bulk_update(self, client: Courier) -> None: + preference = client.users.preferences.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_bulk_update_with_all_params(self, client: Courier) -> None: + preference = client.users.preferences.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + "custom_routing": ["inbox", "email"], + "has_custom_routing": True, + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + "custom_routing": ["direct_message"], + "has_custom_routing": True, + }, + ], + tenant_id="tenant_id", + ) + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_bulk_update(self, client: Courier) -> None: + response = client.users.preferences.with_raw_response.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + preference = response.parse() + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_bulk_update(self, client: Courier) -> None: + with client.users.preferences.with_streaming_response.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + preference = response.parse() + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_bulk_update(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + client.users.preferences.with_raw_response.bulk_update( + user_id="", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_topic(self, client: Courier) -> None: @@ -325,6 +515,194 @@ async def test_path_params_retrieve(self, async_client: AsyncCourier) -> None: user_id="", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_bulk_replace(self, async_client: AsyncCourier) -> None: + preference = await async_client.users.preferences.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_bulk_replace_with_all_params(self, async_client: AsyncCourier) -> None: + preference = await async_client.users.preferences.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + "custom_routing": ["inbox", "email"], + "has_custom_routing": True, + } + ], + tenant_id="tenant_id", + ) + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_bulk_replace(self, async_client: AsyncCourier) -> None: + response = await async_client.users.preferences.with_raw_response.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + preference = await response.parse() + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_bulk_replace(self, async_client: AsyncCourier) -> None: + async with async_client.users.preferences.with_streaming_response.bulk_replace( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + preference = await response.parse() + assert_matches_type(PreferenceBulkReplaceResponse, preference, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_bulk_replace(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + await async_client.users.preferences.with_raw_response.bulk_replace( + user_id="", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + } + ], + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_bulk_update(self, async_client: AsyncCourier) -> None: + preference = await async_client.users.preferences.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_bulk_update_with_all_params(self, async_client: AsyncCourier) -> None: + preference = await async_client.users.preferences.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + "custom_routing": ["inbox", "email"], + "has_custom_routing": True, + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + "custom_routing": ["direct_message"], + "has_custom_routing": True, + }, + ], + tenant_id="tenant_id", + ) + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_bulk_update(self, async_client: AsyncCourier) -> None: + response = await async_client.users.preferences.with_raw_response.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + preference = await response.parse() + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_bulk_update(self, async_client: AsyncCourier) -> None: + async with async_client.users.preferences.with_streaming_response.bulk_update( + user_id="user_id", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + preference = await response.parse() + assert_matches_type(PreferenceBulkUpdateResponse, preference, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_bulk_update(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + await async_client.users.preferences.with_raw_response.bulk_update( + user_id="", + topics=[ + { + "status": "OPTED_IN", + "topic_id": "74Q4QGFBEX481DP6JRPMV751H4XT", + }, + { + "status": "OPTED_OUT", + "topic_id": "5Q4QGFBEX481DP6JRPMV751H4YU", + }, + ], + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_topic(self, async_client: AsyncCourier) -> None: diff --git a/tests/api_resources/workspace_preferences/test_topics.py b/tests/api_resources/workspace_preferences/test_topics.py index 5a94444..5647167 100644 --- a/tests/api_resources/workspace_preferences/test_topics.py +++ b/tests/api_resources/workspace_preferences/test_topics.py @@ -35,6 +35,7 @@ def test_method_create_with_all_params(self, client: Courier) -> None: default_status="OPTED_OUT", name="Marketing", allowed_preferences=["snooze"], + description="description", include_unsubscribe_header=True, routing_options=["direct_message"], topic_data={"foo": "bar"}, @@ -247,6 +248,7 @@ def test_method_replace_with_all_params(self, client: Courier) -> None: default_status="OPTED_OUT", name="name", allowed_preferences=["snooze"], + description="description", include_unsubscribe_header=True, routing_options=["direct_message"], topic_data={"foo": "bar"}, @@ -328,6 +330,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncCourier) - default_status="OPTED_OUT", name="Marketing", allowed_preferences=["snooze"], + description="description", include_unsubscribe_header=True, routing_options=["direct_message"], topic_data={"foo": "bar"}, @@ -540,6 +543,7 @@ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) default_status="OPTED_OUT", name="name", allowed_preferences=["snooze"], + description="description", include_unsubscribe_header=True, routing_options=["direct_message"], topic_data={"foo": "bar"},