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"},