From 362609ad7f9f9416c9b3bfabb846128c595dcda6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 24 Jun 2026 22:15:21 +0000
Subject: [PATCH 1/2] feat(preferences): workspace preference sections & nested
topics endpoints
---
.stats.yml | 8 +-
api.md | 37 +
src/courier/_client.py | 41 ++
src/courier/resources/__init__.py | 14 +
.../resources/preference_sections/__init__.py | 33 +
.../preference_sections.py | 632 +++++++++++++++++
.../resources/preference_sections/topics.py | 646 ++++++++++++++++++
src/courier/types/__init__.py | 7 +
.../types/preference_section_create_params.py | 21 +
.../types/preference_section_get_response.py | 40 ++
.../types/preference_section_list_response.py | 14 +
.../preference_section_replace_params.py | 21 +
.../types/preference_sections/__init__.py | 6 +
.../topic_create_params.py | 33 +
.../topic_replace_params.py | 32 +
.../types/preference_topic_get_response.py | 46 ++
.../types/preference_topic_list_response.py | 14 +
.../types/publish_preferences_response.py | 26 +
.../preference_sections/__init__.py | 1 +
.../preference_sections/test_topics.py | 598 ++++++++++++++++
.../api_resources/test_preference_sections.py | 510 ++++++++++++++
21 files changed, 2776 insertions(+), 4 deletions(-)
create mode 100644 src/courier/resources/preference_sections/__init__.py
create mode 100644 src/courier/resources/preference_sections/preference_sections.py
create mode 100644 src/courier/resources/preference_sections/topics.py
create mode 100644 src/courier/types/preference_section_create_params.py
create mode 100644 src/courier/types/preference_section_get_response.py
create mode 100644 src/courier/types/preference_section_list_response.py
create mode 100644 src/courier/types/preference_section_replace_params.py
create mode 100644 src/courier/types/preference_sections/__init__.py
create mode 100644 src/courier/types/preference_sections/topic_create_params.py
create mode 100644 src/courier/types/preference_sections/topic_replace_params.py
create mode 100644 src/courier/types/preference_topic_get_response.py
create mode 100644 src/courier/types/preference_topic_list_response.py
create mode 100644 src/courier/types/publish_preferences_response.py
create mode 100644 tests/api_resources/preference_sections/__init__.py
create mode 100644 tests/api_resources/preference_sections/test_topics.py
create mode 100644 tests/api_resources/test_preference_sections.py
diff --git a/.stats.yml b/.stats.yml
index 0dd47099..9be8c110 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 123
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-f54bd32a07b6d85cfda75812728f86aa295b0f3fb662ead7670179f809195896.yml
-openapi_spec_hash: 0a283754d97445ef84d50720ba47081a
-config_hash: 3536872b17998fc451577505e15afb3d
+configured_endpoints: 134
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-e0c54fd2a28d7beec2c18a6c7abb833e0dd9b14e99c48cf1cfd126f664f1dc62.yml
+openapi_spec_hash: 713a396d0875e2000de918032b499b2e
+config_hash: 83c79a6ad0a0b5dcce3b85208026343b
diff --git a/api.md b/api.md
index eee01fab..07acc284 100644
--- a/api.md
+++ b/api.md
@@ -481,6 +481,43 @@ Methods:
- client.routing_strategies.list_notifications(id, \*\*params) -> AssociatedNotificationListResponse
- client.routing_strategies.replace(id, \*\*params) -> RoutingStrategyGetResponse
+# PreferenceSections
+
+Types:
+
+```python
+from courier.types import (
+ PreferenceSectionCreateRequest,
+ PreferenceSectionGetResponse,
+ PreferenceSectionListResponse,
+ PreferenceSectionReplaceRequest,
+ PreferenceTopicCreateRequest,
+ PreferenceTopicGetResponse,
+ PreferenceTopicListResponse,
+ PreferenceTopicReplaceRequest,
+ PublishPreferencesResponse,
+)
+```
+
+Methods:
+
+- client.preference_sections.create(\*\*params) -> PreferenceSectionGetResponse
+- client.preference_sections.retrieve(section_id) -> PreferenceSectionGetResponse
+- client.preference_sections.list() -> PreferenceSectionListResponse
+- client.preference_sections.archive(section_id) -> None
+- client.preference_sections.publish() -> PublishPreferencesResponse
+- client.preference_sections.replace(section_id, \*\*params) -> PreferenceSectionGetResponse
+
+## Topics
+
+Methods:
+
+- client.preference_sections.topics.create(section_id, \*\*params) -> PreferenceTopicGetResponse
+- client.preference_sections.topics.retrieve(topic_id, \*, section_id) -> PreferenceTopicGetResponse
+- client.preference_sections.topics.list(section_id) -> PreferenceTopicListResponse
+- client.preference_sections.topics.archive(topic_id, \*, section_id) -> None
+- client.preference_sections.topics.replace(topic_id, \*, section_id, \*\*params) -> PreferenceTopicGetResponse
+
# Profiles
Types:
diff --git a/src/courier/_client.py b/src/courier/_client.py
index 64fa1369..b79512a8 100644
--- a/src/courier/_client.py
+++ b/src/courier/_client.py
@@ -56,6 +56,7 @@
translations,
notifications,
routing_strategies,
+ preference_sections,
)
from .resources.auth import AuthResource, AsyncAuthResource
from .resources.bulk import BulkResource, AsyncBulkResource
@@ -77,6 +78,10 @@
from .resources.providers.providers import ProvidersResource, AsyncProvidersResource
from .resources.automations.automations import AutomationsResource, AsyncAutomationsResource
from .resources.notifications.notifications import NotificationsResource, AsyncNotificationsResource
+ from .resources.preference_sections.preference_sections import (
+ PreferenceSectionsResource,
+ AsyncPreferenceSectionsResource,
+ )
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Courier", "AsyncCourier", "Client", "AsyncClient"]
@@ -241,6 +246,12 @@ def routing_strategies(self) -> RoutingStrategiesResource:
return RoutingStrategiesResource(self)
+ @cached_property
+ def preference_sections(self) -> PreferenceSectionsResource:
+ from .resources.preference_sections import PreferenceSectionsResource
+
+ return PreferenceSectionsResource(self)
+
@cached_property
def profiles(self) -> ProfilesResource:
from .resources.profiles import ProfilesResource
@@ -538,6 +549,12 @@ def routing_strategies(self) -> AsyncRoutingStrategiesResource:
return AsyncRoutingStrategiesResource(self)
+ @cached_property
+ def preference_sections(self) -> AsyncPreferenceSectionsResource:
+ from .resources.preference_sections import AsyncPreferenceSectionsResource
+
+ return AsyncPreferenceSectionsResource(self)
+
@cached_property
def profiles(self) -> AsyncProfilesResource:
from .resources.profiles import AsyncProfilesResource
@@ -777,6 +794,12 @@ def routing_strategies(self) -> routing_strategies.RoutingStrategiesResourceWith
return RoutingStrategiesResourceWithRawResponse(self._client.routing_strategies)
+ @cached_property
+ def preference_sections(self) -> preference_sections.PreferenceSectionsResourceWithRawResponse:
+ from .resources.preference_sections import PreferenceSectionsResourceWithRawResponse
+
+ return PreferenceSectionsResourceWithRawResponse(self._client.preference_sections)
+
@cached_property
def profiles(self) -> profiles.ProfilesResourceWithRawResponse:
from .resources.profiles import ProfilesResourceWithRawResponse
@@ -904,6 +927,12 @@ def routing_strategies(self) -> routing_strategies.AsyncRoutingStrategiesResourc
return AsyncRoutingStrategiesResourceWithRawResponse(self._client.routing_strategies)
+ @cached_property
+ def preference_sections(self) -> preference_sections.AsyncPreferenceSectionsResourceWithRawResponse:
+ from .resources.preference_sections import AsyncPreferenceSectionsResourceWithRawResponse
+
+ return AsyncPreferenceSectionsResourceWithRawResponse(self._client.preference_sections)
+
@cached_property
def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse:
from .resources.profiles import AsyncProfilesResourceWithRawResponse
@@ -1031,6 +1060,12 @@ def routing_strategies(self) -> routing_strategies.RoutingStrategiesResourceWith
return RoutingStrategiesResourceWithStreamingResponse(self._client.routing_strategies)
+ @cached_property
+ def preference_sections(self) -> preference_sections.PreferenceSectionsResourceWithStreamingResponse:
+ from .resources.preference_sections import PreferenceSectionsResourceWithStreamingResponse
+
+ return PreferenceSectionsResourceWithStreamingResponse(self._client.preference_sections)
+
@cached_property
def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse:
from .resources.profiles import ProfilesResourceWithStreamingResponse
@@ -1158,6 +1193,12 @@ def routing_strategies(self) -> routing_strategies.AsyncRoutingStrategiesResourc
return AsyncRoutingStrategiesResourceWithStreamingResponse(self._client.routing_strategies)
+ @cached_property
+ def preference_sections(self) -> preference_sections.AsyncPreferenceSectionsResourceWithStreamingResponse:
+ from .resources.preference_sections import AsyncPreferenceSectionsResourceWithStreamingResponse
+
+ return AsyncPreferenceSectionsResourceWithStreamingResponse(self._client.preference_sections)
+
@cached_property
def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse:
from .resources.profiles import AsyncProfilesResourceWithStreamingResponse
diff --git a/src/courier/resources/__init__.py b/src/courier/resources/__init__.py
index 3933d0ab..20439e32 100644
--- a/src/courier/resources/__init__.py
+++ b/src/courier/resources/__init__.py
@@ -160,6 +160,14 @@
RoutingStrategiesResourceWithStreamingResponse,
AsyncRoutingStrategiesResourceWithStreamingResponse,
)
+from .preference_sections import (
+ PreferenceSectionsResource,
+ AsyncPreferenceSectionsResource,
+ PreferenceSectionsResourceWithRawResponse,
+ AsyncPreferenceSectionsResourceWithRawResponse,
+ PreferenceSectionsResourceWithStreamingResponse,
+ AsyncPreferenceSectionsResourceWithStreamingResponse,
+)
__all__ = [
"SendResource",
@@ -258,6 +266,12 @@
"AsyncRoutingStrategiesResourceWithRawResponse",
"RoutingStrategiesResourceWithStreamingResponse",
"AsyncRoutingStrategiesResourceWithStreamingResponse",
+ "PreferenceSectionsResource",
+ "AsyncPreferenceSectionsResource",
+ "PreferenceSectionsResourceWithRawResponse",
+ "AsyncPreferenceSectionsResourceWithRawResponse",
+ "PreferenceSectionsResourceWithStreamingResponse",
+ "AsyncPreferenceSectionsResourceWithStreamingResponse",
"ProfilesResource",
"AsyncProfilesResource",
"ProfilesResourceWithRawResponse",
diff --git a/src/courier/resources/preference_sections/__init__.py b/src/courier/resources/preference_sections/__init__.py
new file mode 100644
index 00000000..2ce5b7b2
--- /dev/null
+++ b/src/courier/resources/preference_sections/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .topics import (
+ TopicsResource,
+ AsyncTopicsResource,
+ TopicsResourceWithRawResponse,
+ AsyncTopicsResourceWithRawResponse,
+ TopicsResourceWithStreamingResponse,
+ AsyncTopicsResourceWithStreamingResponse,
+)
+from .preference_sections import (
+ PreferenceSectionsResource,
+ AsyncPreferenceSectionsResource,
+ PreferenceSectionsResourceWithRawResponse,
+ AsyncPreferenceSectionsResourceWithRawResponse,
+ PreferenceSectionsResourceWithStreamingResponse,
+ AsyncPreferenceSectionsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "TopicsResource",
+ "AsyncTopicsResource",
+ "TopicsResourceWithRawResponse",
+ "AsyncTopicsResourceWithRawResponse",
+ "TopicsResourceWithStreamingResponse",
+ "AsyncTopicsResourceWithStreamingResponse",
+ "PreferenceSectionsResource",
+ "AsyncPreferenceSectionsResource",
+ "PreferenceSectionsResourceWithRawResponse",
+ "AsyncPreferenceSectionsResourceWithRawResponse",
+ "PreferenceSectionsResourceWithStreamingResponse",
+ "AsyncPreferenceSectionsResourceWithStreamingResponse",
+]
diff --git a/src/courier/resources/preference_sections/preference_sections.py b/src/courier/resources/preference_sections/preference_sections.py
new file mode 100644
index 00000000..47267cf6
--- /dev/null
+++ b/src/courier/resources/preference_sections/preference_sections.py
@@ -0,0 +1,632 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+
+import httpx
+
+from .topics import (
+ TopicsResource,
+ AsyncTopicsResource,
+ TopicsResourceWithRawResponse,
+ AsyncTopicsResourceWithRawResponse,
+ TopicsResourceWithStreamingResponse,
+ AsyncTopicsResourceWithStreamingResponse,
+)
+from ...types import preference_section_create_params, preference_section_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
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.publish_preferences_response import PublishPreferencesResponse
+from ...types.shared.channel_classification import ChannelClassification
+from ...types.preference_section_get_response import PreferenceSectionGetResponse
+from ...types.preference_section_list_response import PreferenceSectionListResponse
+
+__all__ = ["PreferenceSectionsResource", "AsyncPreferenceSectionsResource"]
+
+
+class PreferenceSectionsResource(SyncAPIResource):
+ @cached_property
+ def topics(self) -> TopicsResource:
+ return TopicsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> PreferenceSectionsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/trycourier/courier-python#accessing-raw-response-data-eg-headers
+ """
+ return PreferenceSectionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> PreferenceSectionsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/trycourier/courier-python#with_streaming_response
+ """
+ return PreferenceSectionsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ name: str,
+ 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.
+ # 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,
+ ) -> PreferenceSectionGetResponse:
+ """Create a preference section in your workspace.
+
+ The section id is generated and
+ returned. Topics are created inside a section via POST
+ /preferences/sections/{section_id}/topics.
+
+ Args:
+ name: Human-readable name for the section.
+
+ has_custom_routing: Whether the section defines custom routing for its topics.
+
+ routing_options: Default channels for the section. Defaults to empty if omitted.
+
+ 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/sections",
+ body=maybe_transform(
+ {
+ "name": name,
+ "has_custom_routing": has_custom_routing,
+ "routing_options": routing_options,
+ },
+ preference_section_create_params.PreferenceSectionCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionGetResponse,
+ )
+
+ def retrieve(
+ self,
+ section_id: str,
+ *,
+ # 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,
+ ) -> PreferenceSectionGetResponse:
+ """
+ Retrieve a preference section by id, including its topics.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return self._get(
+ path_template("/preferences/sections/{section_id}", section_id=section_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionGetResponse,
+ )
+
+ def list(
+ self,
+ *,
+ # 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,
+ ) -> PreferenceSectionListResponse:
+ """List the workspace's preference sections.
+
+ Each section embeds its topics. Scoped
+ to the workspace of the API key.
+ """
+ return self._get(
+ "/preferences/sections",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionListResponse,
+ )
+
+ def archive(
+ self,
+ section_id: str,
+ *,
+ # 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,
+ ) -> None:
+ """Archive a preference section.
+
+ The section must be empty: delete its topics
+ first, otherwise the request fails with 409.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/preferences/sections/{section_id}", section_id=section_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def publish(
+ self,
+ *,
+ # 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,
+ ) -> PublishPreferencesResponse:
+ """Publish the workspace's preferences page.
+
+ Takes a snapshot of every section with
+ its topics under a new published version, making the current state visible on
+ the hosted preferences page (non-draft).
+ """
+ return self._post(
+ "/preferences/publish",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PublishPreferencesResponse,
+ )
+
+ def replace(
+ self,
+ section_id: str,
+ *,
+ name: str,
+ 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.
+ # 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,
+ ) -> PreferenceSectionGetResponse:
+ """Replace a preference section.
+
+ Full document replacement; missing optional fields
+ are cleared. Topics attached to the section are unaffected.
+
+ Args:
+ name: Human-readable name for the section.
+
+ has_custom_routing: Whether the section defines custom routing for its topics.
+
+ routing_options: Default channels for the section. Omit to clear.
+
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return self._put(
+ path_template("/preferences/sections/{section_id}", section_id=section_id),
+ body=maybe_transform(
+ {
+ "name": name,
+ "has_custom_routing": has_custom_routing,
+ "routing_options": routing_options,
+ },
+ preference_section_replace_params.PreferenceSectionReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionGetResponse,
+ )
+
+
+class AsyncPreferenceSectionsResource(AsyncAPIResource):
+ @cached_property
+ def topics(self) -> AsyncTopicsResource:
+ return AsyncTopicsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncPreferenceSectionsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/trycourier/courier-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncPreferenceSectionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncPreferenceSectionsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/trycourier/courier-python#with_streaming_response
+ """
+ return AsyncPreferenceSectionsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ name: str,
+ 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.
+ # 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,
+ ) -> PreferenceSectionGetResponse:
+ """Create a preference section in your workspace.
+
+ The section id is generated and
+ returned. Topics are created inside a section via POST
+ /preferences/sections/{section_id}/topics.
+
+ Args:
+ name: Human-readable name for the section.
+
+ has_custom_routing: Whether the section defines custom routing for its topics.
+
+ routing_options: Default channels for the section. Defaults to empty if omitted.
+
+ 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/sections",
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "has_custom_routing": has_custom_routing,
+ "routing_options": routing_options,
+ },
+ preference_section_create_params.PreferenceSectionCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionGetResponse,
+ )
+
+ async def retrieve(
+ self,
+ section_id: str,
+ *,
+ # 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,
+ ) -> PreferenceSectionGetResponse:
+ """
+ Retrieve a preference section by id, including its topics.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return await self._get(
+ path_template("/preferences/sections/{section_id}", section_id=section_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionGetResponse,
+ )
+
+ async def list(
+ self,
+ *,
+ # 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,
+ ) -> PreferenceSectionListResponse:
+ """List the workspace's preference sections.
+
+ Each section embeds its topics. Scoped
+ to the workspace of the API key.
+ """
+ return await self._get(
+ "/preferences/sections",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionListResponse,
+ )
+
+ async def archive(
+ self,
+ section_id: str,
+ *,
+ # 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,
+ ) -> None:
+ """Archive a preference section.
+
+ The section must be empty: delete its topics
+ first, otherwise the request fails with 409.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/preferences/sections/{section_id}", section_id=section_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def publish(
+ self,
+ *,
+ # 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,
+ ) -> PublishPreferencesResponse:
+ """Publish the workspace's preferences page.
+
+ Takes a snapshot of every section with
+ its topics under a new published version, making the current state visible on
+ the hosted preferences page (non-draft).
+ """
+ return await self._post(
+ "/preferences/publish",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PublishPreferencesResponse,
+ )
+
+ async def replace(
+ self,
+ section_id: str,
+ *,
+ name: str,
+ 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.
+ # 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,
+ ) -> PreferenceSectionGetResponse:
+ """Replace a preference section.
+
+ Full document replacement; missing optional fields
+ are cleared. Topics attached to the section are unaffected.
+
+ Args:
+ name: Human-readable name for the section.
+
+ has_custom_routing: Whether the section defines custom routing for its topics.
+
+ routing_options: Default channels for the section. Omit to clear.
+
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return await self._put(
+ path_template("/preferences/sections/{section_id}", section_id=section_id),
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "has_custom_routing": has_custom_routing,
+ "routing_options": routing_options,
+ },
+ preference_section_replace_params.PreferenceSectionReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceSectionGetResponse,
+ )
+
+
+class PreferenceSectionsResourceWithRawResponse:
+ def __init__(self, preference_sections: PreferenceSectionsResource) -> None:
+ self._preference_sections = preference_sections
+
+ self.create = to_raw_response_wrapper(
+ preference_sections.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ preference_sections.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ preference_sections.list,
+ )
+ self.archive = to_raw_response_wrapper(
+ preference_sections.archive,
+ )
+ self.publish = to_raw_response_wrapper(
+ preference_sections.publish,
+ )
+ self.replace = to_raw_response_wrapper(
+ preference_sections.replace,
+ )
+
+ @cached_property
+ def topics(self) -> TopicsResourceWithRawResponse:
+ return TopicsResourceWithRawResponse(self._preference_sections.topics)
+
+
+class AsyncPreferenceSectionsResourceWithRawResponse:
+ def __init__(self, preference_sections: AsyncPreferenceSectionsResource) -> None:
+ self._preference_sections = preference_sections
+
+ self.create = async_to_raw_response_wrapper(
+ preference_sections.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ preference_sections.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ preference_sections.list,
+ )
+ self.archive = async_to_raw_response_wrapper(
+ preference_sections.archive,
+ )
+ self.publish = async_to_raw_response_wrapper(
+ preference_sections.publish,
+ )
+ self.replace = async_to_raw_response_wrapper(
+ preference_sections.replace,
+ )
+
+ @cached_property
+ def topics(self) -> AsyncTopicsResourceWithRawResponse:
+ return AsyncTopicsResourceWithRawResponse(self._preference_sections.topics)
+
+
+class PreferenceSectionsResourceWithStreamingResponse:
+ def __init__(self, preference_sections: PreferenceSectionsResource) -> None:
+ self._preference_sections = preference_sections
+
+ self.create = to_streamed_response_wrapper(
+ preference_sections.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ preference_sections.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ preference_sections.list,
+ )
+ self.archive = to_streamed_response_wrapper(
+ preference_sections.archive,
+ )
+ self.publish = to_streamed_response_wrapper(
+ preference_sections.publish,
+ )
+ self.replace = to_streamed_response_wrapper(
+ preference_sections.replace,
+ )
+
+ @cached_property
+ def topics(self) -> TopicsResourceWithStreamingResponse:
+ return TopicsResourceWithStreamingResponse(self._preference_sections.topics)
+
+
+class AsyncPreferenceSectionsResourceWithStreamingResponse:
+ def __init__(self, preference_sections: AsyncPreferenceSectionsResource) -> None:
+ self._preference_sections = preference_sections
+
+ self.create = async_to_streamed_response_wrapper(
+ preference_sections.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ preference_sections.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ preference_sections.list,
+ )
+ self.archive = async_to_streamed_response_wrapper(
+ preference_sections.archive,
+ )
+ self.publish = async_to_streamed_response_wrapper(
+ preference_sections.publish,
+ )
+ self.replace = async_to_streamed_response_wrapper(
+ preference_sections.replace,
+ )
+
+ @cached_property
+ def topics(self) -> AsyncTopicsResourceWithStreamingResponse:
+ return AsyncTopicsResourceWithStreamingResponse(self._preference_sections.topics)
diff --git a/src/courier/resources/preference_sections/topics.py b/src/courier/resources/preference_sections/topics.py
new file mode 100644
index 00000000..4ea2c176
--- /dev/null
+++ b/src/courier/resources/preference_sections/topics.py
@@ -0,0 +1,646 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, List, Optional
+from typing_extensions import Literal
+
+import httpx
+
+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
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.preference_sections import topic_create_params, topic_replace_params
+from ...types.preference_topic_get_response import PreferenceTopicGetResponse
+from ...types.shared.channel_classification import ChannelClassification
+from ...types.preference_topic_list_response import PreferenceTopicListResponse
+
+__all__ = ["TopicsResource", "AsyncTopicsResource"]
+
+
+class TopicsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> TopicsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/trycourier/courier-python#accessing-raw-response-data-eg-headers
+ """
+ return TopicsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> TopicsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/trycourier/courier-python#with_streaming_response
+ """
+ return TopicsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ section_id: str,
+ *,
+ default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"],
+ name: str,
+ allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit,
+ include_unsubscribe_header: Optional[bool] | Omit = omit,
+ routing_options: Optional[List[ChannelClassification]] | Omit = omit,
+ topic_data: Optional[Dict[str, object]] | 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,
+ ) -> PreferenceTopicGetResponse:
+ """Create a subscription preference topic inside a section.
+
+ Fails with 404 if the
+ section does not exist. The topic id is generated and returned.
+
+ Args:
+ default_status: The default subscription status applied when a recipient has not set their own.
+
+ name: Human-readable name for the preference topic.
+
+ allowed_preferences: Preference controls a recipient may customize for this topic. Defaults to empty
+ if omitted.
+
+ 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.
+
+ topic_data: Arbitrary metadata associated with the topic.
+
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return self._post(
+ path_template("/preferences/sections/{section_id}/topics", section_id=section_id),
+ body=maybe_transform(
+ {
+ "default_status": default_status,
+ "name": name,
+ "allowed_preferences": allowed_preferences,
+ "include_unsubscribe_header": include_unsubscribe_header,
+ "routing_options": routing_options,
+ "topic_data": topic_data,
+ },
+ topic_create_params.TopicCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicGetResponse,
+ )
+
+ def retrieve(
+ self,
+ topic_id: str,
+ *,
+ section_id: str,
+ # 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,
+ ) -> PreferenceTopicGetResponse:
+ """Retrieve a topic within a section.
+
+ Returns 404 if the section does not exist,
+ the topic does not exist, or the topic belongs to a different section.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ if not topic_id:
+ raise ValueError(f"Expected a non-empty value for `topic_id` but received {topic_id!r}")
+ return self._get(
+ path_template(
+ "/preferences/sections/{section_id}/topics/{topic_id}", section_id=section_id, topic_id=topic_id
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicGetResponse,
+ )
+
+ def list(
+ self,
+ section_id: str,
+ *,
+ # 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,
+ ) -> PreferenceTopicListResponse:
+ """
+ List the topics in a preference section.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return self._get(
+ path_template("/preferences/sections/{section_id}/topics", section_id=section_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicListResponse,
+ )
+
+ def archive(
+ self,
+ topic_id: str,
+ *,
+ section_id: str,
+ # 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,
+ ) -> None:
+ """Archive a topic and remove it from its section.
+
+ Same 404 rules as GET.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ if not topic_id:
+ raise ValueError(f"Expected a non-empty value for `topic_id` but received {topic_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template(
+ "/preferences/sections/{section_id}/topics/{topic_id}", section_id=section_id, topic_id=topic_id
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def replace(
+ self,
+ topic_id: str,
+ *,
+ section_id: str,
+ default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"],
+ name: str,
+ allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit,
+ include_unsubscribe_header: Optional[bool] | Omit = omit,
+ routing_options: Optional[List[ChannelClassification]] | Omit = omit,
+ topic_data: Optional[Dict[str, object]] | 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,
+ ) -> PreferenceTopicGetResponse:
+ """Replace a topic within a section.
+
+ Full document replacement; missing optional
+ fields are cleared. Same 404 rules as GET.
+
+ Args:
+ default_status: The default subscription status applied when a recipient has not set their own.
+
+ name: Human-readable name for the preference topic.
+
+ allowed_preferences: Preference controls a recipient may customize. 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.
+
+ topic_data: Arbitrary metadata associated with the topic. Omit to clear.
+
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ if not topic_id:
+ raise ValueError(f"Expected a non-empty value for `topic_id` but received {topic_id!r}")
+ return self._put(
+ path_template(
+ "/preferences/sections/{section_id}/topics/{topic_id}", section_id=section_id, topic_id=topic_id
+ ),
+ body=maybe_transform(
+ {
+ "default_status": default_status,
+ "name": name,
+ "allowed_preferences": allowed_preferences,
+ "include_unsubscribe_header": include_unsubscribe_header,
+ "routing_options": routing_options,
+ "topic_data": topic_data,
+ },
+ topic_replace_params.TopicReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicGetResponse,
+ )
+
+
+class AsyncTopicsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncTopicsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/trycourier/courier-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncTopicsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncTopicsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/trycourier/courier-python#with_streaming_response
+ """
+ return AsyncTopicsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ section_id: str,
+ *,
+ default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"],
+ name: str,
+ allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit,
+ include_unsubscribe_header: Optional[bool] | Omit = omit,
+ routing_options: Optional[List[ChannelClassification]] | Omit = omit,
+ topic_data: Optional[Dict[str, object]] | 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,
+ ) -> PreferenceTopicGetResponse:
+ """Create a subscription preference topic inside a section.
+
+ Fails with 404 if the
+ section does not exist. The topic id is generated and returned.
+
+ Args:
+ default_status: The default subscription status applied when a recipient has not set their own.
+
+ name: Human-readable name for the preference topic.
+
+ allowed_preferences: Preference controls a recipient may customize for this topic. Defaults to empty
+ if omitted.
+
+ 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.
+
+ topic_data: Arbitrary metadata associated with the topic.
+
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return await self._post(
+ path_template("/preferences/sections/{section_id}/topics", section_id=section_id),
+ body=await async_maybe_transform(
+ {
+ "default_status": default_status,
+ "name": name,
+ "allowed_preferences": allowed_preferences,
+ "include_unsubscribe_header": include_unsubscribe_header,
+ "routing_options": routing_options,
+ "topic_data": topic_data,
+ },
+ topic_create_params.TopicCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicGetResponse,
+ )
+
+ async def retrieve(
+ self,
+ topic_id: str,
+ *,
+ section_id: str,
+ # 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,
+ ) -> PreferenceTopicGetResponse:
+ """Retrieve a topic within a section.
+
+ Returns 404 if the section does not exist,
+ the topic does not exist, or the topic belongs to a different section.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ if not topic_id:
+ raise ValueError(f"Expected a non-empty value for `topic_id` but received {topic_id!r}")
+ return await self._get(
+ path_template(
+ "/preferences/sections/{section_id}/topics/{topic_id}", section_id=section_id, topic_id=topic_id
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicGetResponse,
+ )
+
+ async def list(
+ self,
+ section_id: str,
+ *,
+ # 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,
+ ) -> PreferenceTopicListResponse:
+ """
+ List the topics in a preference section.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ return await self._get(
+ path_template("/preferences/sections/{section_id}/topics", section_id=section_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicListResponse,
+ )
+
+ async def archive(
+ self,
+ topic_id: str,
+ *,
+ section_id: str,
+ # 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,
+ ) -> None:
+ """Archive a topic and remove it from its section.
+
+ Same 404 rules as GET.
+
+ Args:
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ if not topic_id:
+ raise ValueError(f"Expected a non-empty value for `topic_id` but received {topic_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template(
+ "/preferences/sections/{section_id}/topics/{topic_id}", section_id=section_id, topic_id=topic_id
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def replace(
+ self,
+ topic_id: str,
+ *,
+ section_id: str,
+ default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"],
+ name: str,
+ allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]] | Omit = omit,
+ include_unsubscribe_header: Optional[bool] | Omit = omit,
+ routing_options: Optional[List[ChannelClassification]] | Omit = omit,
+ topic_data: Optional[Dict[str, object]] | 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,
+ ) -> PreferenceTopicGetResponse:
+ """Replace a topic within a section.
+
+ Full document replacement; missing optional
+ fields are cleared. Same 404 rules as GET.
+
+ Args:
+ default_status: The default subscription status applied when a recipient has not set their own.
+
+ name: Human-readable name for the preference topic.
+
+ allowed_preferences: Preference controls a recipient may customize. 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.
+
+ topic_data: Arbitrary metadata associated with the topic. Omit to clear.
+
+ 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 section_id:
+ raise ValueError(f"Expected a non-empty value for `section_id` but received {section_id!r}")
+ if not topic_id:
+ raise ValueError(f"Expected a non-empty value for `topic_id` but received {topic_id!r}")
+ return await self._put(
+ path_template(
+ "/preferences/sections/{section_id}/topics/{topic_id}", section_id=section_id, topic_id=topic_id
+ ),
+ body=await async_maybe_transform(
+ {
+ "default_status": default_status,
+ "name": name,
+ "allowed_preferences": allowed_preferences,
+ "include_unsubscribe_header": include_unsubscribe_header,
+ "routing_options": routing_options,
+ "topic_data": topic_data,
+ },
+ topic_replace_params.TopicReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreferenceTopicGetResponse,
+ )
+
+
+class TopicsResourceWithRawResponse:
+ def __init__(self, topics: TopicsResource) -> None:
+ self._topics = topics
+
+ self.create = to_raw_response_wrapper(
+ topics.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ topics.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ topics.list,
+ )
+ self.archive = to_raw_response_wrapper(
+ topics.archive,
+ )
+ self.replace = to_raw_response_wrapper(
+ topics.replace,
+ )
+
+
+class AsyncTopicsResourceWithRawResponse:
+ def __init__(self, topics: AsyncTopicsResource) -> None:
+ self._topics = topics
+
+ self.create = async_to_raw_response_wrapper(
+ topics.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ topics.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ topics.list,
+ )
+ self.archive = async_to_raw_response_wrapper(
+ topics.archive,
+ )
+ self.replace = async_to_raw_response_wrapper(
+ topics.replace,
+ )
+
+
+class TopicsResourceWithStreamingResponse:
+ def __init__(self, topics: TopicsResource) -> None:
+ self._topics = topics
+
+ self.create = to_streamed_response_wrapper(
+ topics.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ topics.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ topics.list,
+ )
+ self.archive = to_streamed_response_wrapper(
+ topics.archive,
+ )
+ self.replace = to_streamed_response_wrapper(
+ topics.replace,
+ )
+
+
+class AsyncTopicsResourceWithStreamingResponse:
+ def __init__(self, topics: AsyncTopicsResource) -> None:
+ self._topics = topics
+
+ self.create = async_to_streamed_response_wrapper(
+ topics.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ topics.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ topics.list,
+ )
+ self.archive = async_to_streamed_response_wrapper(
+ topics.archive,
+ )
+ self.replace = async_to_streamed_response_wrapper(
+ topics.replace,
+ )
diff --git a/src/courier/types/__init__.py b/src/courier/types/__init__.py
index 22ef85dd..bd8105f0 100644
--- a/src/courier/types/__init__.py
+++ b/src/courier/types/__init__.py
@@ -235,6 +235,7 @@
from .journey_segment_trigger_node import JourneySegmentTriggerNode as JourneySegmentTriggerNode
from .journey_throttle_static_node import JourneyThrottleStaticNode as JourneyThrottleStaticNode
from .notification_retrieve_params import NotificationRetrieveParams as NotificationRetrieveParams
+from .publish_preferences_response import PublishPreferencesResponse as PublishPreferencesResponse
from .put_tenant_template_response import PutTenantTemplateResponse as PutTenantTemplateResponse
from .routing_strategy_list_params import RoutingStrategyListParams as RoutingStrategyListParams
from .subscription_topic_new_param import SubscriptionTopicNewParam as SubscriptionTopicNewParam
@@ -245,6 +246,7 @@
from .journey_throttle_dynamic_node import JourneyThrottleDynamicNode as JourneyThrottleDynamicNode
from .notification_template_payload import NotificationTemplatePayload as NotificationTemplatePayload
from .notification_template_summary import NotificationTemplateSummary as NotificationTemplateSummary
+from .preference_topic_get_response import PreferenceTopicGetResponse as PreferenceTopicGetResponse
from .routing_strategy_get_response import RoutingStrategyGetResponse as RoutingStrategyGetResponse
from .translation_retrieve_response import TranslationRetrieveResponse as TranslationRetrieveResponse
from .audience_list_members_response import AudienceListMembersResponse as AudienceListMembersResponse
@@ -255,19 +257,24 @@
from .journey_versions_list_response import JourneyVersionsListResponse as JourneyVersionsListResponse
from .notification_put_locale_params import NotificationPutLocaleParams as NotificationPutLocaleParams
from .notification_template_response import NotificationTemplateResponse as NotificationTemplateResponse
+from .preference_topic_list_response import PreferenceTopicListResponse as PreferenceTopicListResponse
from .routing_strategy_create_params import RoutingStrategyCreateParams as RoutingStrategyCreateParams
from .routing_strategy_list_response import RoutingStrategyListResponse as RoutingStrategyListResponse
from .inbound_bulk_message_user_param import InboundBulkMessageUserParam as InboundBulkMessageUserParam
from .journey_api_invoke_trigger_node import JourneyAPIInvokeTriggerNode as JourneyAPIInvokeTriggerNode
from .notification_put_content_params import NotificationPutContentParams as NotificationPutContentParams
from .notification_put_element_params import NotificationPutElementParams as NotificationPutElementParams
+from .preference_section_get_response import PreferenceSectionGetResponse as PreferenceSectionGetResponse
from .routing_strategy_replace_params import RoutingStrategyReplaceParams as RoutingStrategyReplaceParams
from .base_template_tenant_association import BaseTemplateTenantAssociation as BaseTemplateTenantAssociation
+from .preference_section_create_params import PreferenceSectionCreateParams as PreferenceSectionCreateParams
+from .preference_section_list_response import PreferenceSectionListResponse as PreferenceSectionListResponse
from .automation_template_list_response import AutomationTemplateListResponse as AutomationTemplateListResponse
from .journey_delay_duration_node_param import JourneyDelayDurationNodeParam as JourneyDelayDurationNodeParam
from .journey_fetch_post_put_node_param import JourneyFetchPostPutNodeParam as JourneyFetchPostPutNodeParam
from .notification_content_get_response import NotificationContentGetResponse as NotificationContentGetResponse
from .notification_list_versions_params import NotificationListVersionsParams as NotificationListVersionsParams
+from .preference_section_replace_params import PreferenceSectionReplaceParams as PreferenceSectionReplaceParams
from .put_subscriptions_recipient_param import PutSubscriptionsRecipientParam as PutSubscriptionsRecipientParam
from .journey_segment_trigger_node_param import JourneySegmentTriggerNodeParam as JourneySegmentTriggerNodeParam
from .journey_throttle_static_node_param import JourneyThrottleStaticNodeParam as JourneyThrottleStaticNodeParam
diff --git a/src/courier/types/preference_section_create_params.py b/src/courier/types/preference_section_create_params.py
new file mode 100644
index 00000000..a9d0e770
--- /dev/null
+++ b/src/courier/types/preference_section_create_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Required, TypedDict
+
+from .shared.channel_classification import ChannelClassification
+
+__all__ = ["PreferenceSectionCreateParams"]
+
+
+class PreferenceSectionCreateParams(TypedDict, total=False):
+ name: Required[str]
+ """Human-readable name for the section."""
+
+ has_custom_routing: Optional[bool]
+ """Whether the section defines custom routing for its topics."""
+
+ routing_options: Optional[List[ChannelClassification]]
+ """Default channels for the section. Defaults to empty if omitted."""
diff --git a/src/courier/types/preference_section_get_response.py b/src/courier/types/preference_section_get_response.py
new file mode 100644
index 00000000..1e853c20
--- /dev/null
+++ b/src/courier/types/preference_section_get_response.py
@@ -0,0 +1,40 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+from .preference_topic_get_response import PreferenceTopicGetResponse
+from .shared.channel_classification import ChannelClassification
+
+__all__ = ["PreferenceSectionGetResponse"]
+
+
+class PreferenceSectionGetResponse(BaseModel):
+ """A preference section in your workspace, including its topics."""
+
+ id: str
+ """The preference section id."""
+
+ created: str
+ """ISO-8601 timestamp of when the section was created."""
+
+ has_custom_routing: bool
+ """Whether the section defines custom routing for its topics."""
+
+ name: str
+ """Human-readable name."""
+
+ routing_options: List[ChannelClassification]
+ """Default channels for the section. May be empty."""
+
+ topics: List[PreferenceTopicGetResponse]
+ """The topics contained in this section."""
+
+ creator: Optional[str] = None
+ """Id of the creator."""
+
+ updated: Optional[str] = None
+ """ISO-8601 timestamp of the last update."""
+
+ updater: Optional[str] = None
+ """Id of the last updater."""
diff --git a/src/courier/types/preference_section_list_response.py b/src/courier/types/preference_section_list_response.py
new file mode 100644
index 00000000..c8255146
--- /dev/null
+++ b/src/courier/types/preference_section_list_response.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .._models import BaseModel
+from .preference_section_get_response import PreferenceSectionGetResponse
+
+__all__ = ["PreferenceSectionListResponse"]
+
+
+class PreferenceSectionListResponse(BaseModel):
+ """The workspace's preference sections, each with its topics."""
+
+ results: List[PreferenceSectionGetResponse]
diff --git a/src/courier/types/preference_section_replace_params.py b/src/courier/types/preference_section_replace_params.py
new file mode 100644
index 00000000..87c2e1a6
--- /dev/null
+++ b/src/courier/types/preference_section_replace_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Required, TypedDict
+
+from .shared.channel_classification import ChannelClassification
+
+__all__ = ["PreferenceSectionReplaceParams"]
+
+
+class PreferenceSectionReplaceParams(TypedDict, total=False):
+ name: Required[str]
+ """Human-readable name for the section."""
+
+ has_custom_routing: Optional[bool]
+ """Whether the section defines custom routing for its topics."""
+
+ routing_options: Optional[List[ChannelClassification]]
+ """Default channels for the section. Omit to clear."""
diff --git a/src/courier/types/preference_sections/__init__.py b/src/courier/types/preference_sections/__init__.py
new file mode 100644
index 00000000..43664cc4
--- /dev/null
+++ b/src/courier/types/preference_sections/__init__.py
@@ -0,0 +1,6 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .topic_create_params import TopicCreateParams as TopicCreateParams
+from .topic_replace_params import TopicReplaceParams as TopicReplaceParams
diff --git a/src/courier/types/preference_sections/topic_create_params.py b/src/courier/types/preference_sections/topic_create_params.py
new file mode 100644
index 00000000..4fdd262f
--- /dev/null
+++ b/src/courier/types/preference_sections/topic_create_params.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, List, Optional
+from typing_extensions import Literal, Required, TypedDict
+
+from ..shared.channel_classification import ChannelClassification
+
+__all__ = ["TopicCreateParams"]
+
+
+class TopicCreateParams(TypedDict, total=False):
+ default_status: Required[Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"]]
+ """The default subscription status applied when a recipient has not set their own."""
+
+ name: Required[str]
+ """Human-readable name for the preference topic."""
+
+ allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]]
+ """Preference controls a recipient may customize for this topic.
+
+ Defaults to empty if omitted.
+ """
+
+ include_unsubscribe_header: Optional[bool]
+ """Whether to include a list-unsubscribe header on emails for this topic."""
+
+ routing_options: Optional[List[ChannelClassification]]
+ """Default channels delivered for this topic. Defaults to empty if omitted."""
+
+ topic_data: Optional[Dict[str, object]]
+ """Arbitrary metadata associated with the topic."""
diff --git a/src/courier/types/preference_sections/topic_replace_params.py b/src/courier/types/preference_sections/topic_replace_params.py
new file mode 100644
index 00000000..7e6c593e
--- /dev/null
+++ b/src/courier/types/preference_sections/topic_replace_params.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, List, Optional
+from typing_extensions import Literal, Required, TypedDict
+
+from ..shared.channel_classification import ChannelClassification
+
+__all__ = ["TopicReplaceParams"]
+
+
+class TopicReplaceParams(TypedDict, total=False):
+ section_id: Required[str]
+
+ default_status: Required[Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"]]
+ """The default subscription status applied when a recipient has not set their own."""
+
+ name: Required[str]
+ """Human-readable name for the preference topic."""
+
+ allowed_preferences: Optional[List[Literal["snooze", "channel_preferences"]]]
+ """Preference controls a recipient may customize. Omit to clear."""
+
+ include_unsubscribe_header: Optional[bool]
+ """Whether to include a list-unsubscribe header on emails for this topic."""
+
+ routing_options: Optional[List[ChannelClassification]]
+ """Default channels delivered for this topic. Omit to clear."""
+
+ topic_data: Optional[Dict[str, object]]
+ """Arbitrary metadata associated with the topic. Omit to clear."""
diff --git a/src/courier/types/preference_topic_get_response.py b/src/courier/types/preference_topic_get_response.py
new file mode 100644
index 00000000..1ad846aa
--- /dev/null
+++ b/src/courier/types/preference_topic_get_response.py
@@ -0,0 +1,46 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.channel_classification import ChannelClassification
+
+__all__ = ["PreferenceTopicGetResponse"]
+
+
+class PreferenceTopicGetResponse(BaseModel):
+ """A subscription preference topic in your workspace."""
+
+ id: str
+ """The preference topic id."""
+
+ allowed_preferences: List[Literal["snooze", "channel_preferences"]]
+ """Preference controls a recipient may customize. May be empty."""
+
+ created: str
+ """ISO-8601 timestamp of when the topic was created."""
+
+ default_status: Literal["OPTED_OUT", "OPTED_IN", "REQUIRED"]
+ """The default subscription status applied when a recipient has not set their own."""
+
+ include_unsubscribe_header: bool
+ """Whether a list-unsubscribe header is included on emails for this topic."""
+
+ name: str
+ """Human-readable name."""
+
+ routing_options: List[ChannelClassification]
+ """Default channels delivered for this topic. May be empty."""
+
+ topic_data: Dict[str, object]
+ """Arbitrary metadata associated with the topic."""
+
+ updated: str
+ """ISO-8601 timestamp of the last update."""
+
+ creator: Optional[str] = None
+ """Id of the creator."""
+
+ updater: Optional[str] = None
+ """Id of the last updater."""
diff --git a/src/courier/types/preference_topic_list_response.py b/src/courier/types/preference_topic_list_response.py
new file mode 100644
index 00000000..47c22c4c
--- /dev/null
+++ b/src/courier/types/preference_topic_list_response.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .._models import BaseModel
+from .preference_topic_get_response import PreferenceTopicGetResponse
+
+__all__ = ["PreferenceTopicListResponse"]
+
+
+class PreferenceTopicListResponse(BaseModel):
+ """Topics contained in a preference section."""
+
+ results: List[PreferenceTopicGetResponse]
diff --git a/src/courier/types/publish_preferences_response.py b/src/courier/types/publish_preferences_response.py
new file mode 100644
index 00000000..c179b5ca
--- /dev/null
+++ b/src/courier/types/publish_preferences_response.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["PublishPreferencesResponse"]
+
+
+class PublishPreferencesResponse(BaseModel):
+ """Result of publishing the workspace's preferences page."""
+
+ page_id: str
+ """Id of the published page snapshot."""
+
+ published_at: str
+ """ISO-8601 timestamp of the publish."""
+
+ published_version: float
+ """Monotonic published version (epoch milliseconds)."""
+
+ preview_url: Optional[str] = None
+ """Draft-mode hosted preferences page URL for previewing."""
+
+ published_by: Optional[str] = None
+ """Id of the publisher."""
diff --git a/tests/api_resources/preference_sections/__init__.py b/tests/api_resources/preference_sections/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/preference_sections/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/preference_sections/test_topics.py b/tests/api_resources/preference_sections/test_topics.py
new file mode 100644
index 00000000..1317fd4d
--- /dev/null
+++ b/tests/api_resources/preference_sections/test_topics.py
@@ -0,0 +1,598 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from courier import Courier, AsyncCourier
+from tests.utils import assert_matches_type
+from courier.types import PreferenceTopicGetResponse, PreferenceTopicListResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestTopics:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ allowed_preferences=["snooze"],
+ include_unsubscribe_header=True,
+ routing_options=["direct_message"],
+ topic_data={"foo": "bar"},
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Courier) -> None:
+ response = client.preference_sections.topics.with_raw_response.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Courier) -> None:
+ with client.preference_sections.topics.with_streaming_response.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_create(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.create(
+ section_id="",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.retrieve(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Courier) -> None:
+ response = client.preference_sections.topics.with_raw_response.retrieve(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Courier) -> None:
+ with client.preference_sections.topics.with_streaming_response.retrieve(
+ topic_id="topic_id",
+ section_id="section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.retrieve(
+ topic_id="topic_id",
+ section_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `topic_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.retrieve(
+ topic_id="",
+ section_id="section_id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.list(
+ "section_id",
+ )
+ assert_matches_type(PreferenceTopicListResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Courier) -> None:
+ response = client.preference_sections.topics.with_raw_response.list(
+ "section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicListResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Courier) -> None:
+ with client.preference_sections.topics.with_streaming_response.list(
+ "section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicListResponse, topic, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_archive(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.archive(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+ assert topic is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_archive(self, client: Courier) -> None:
+ response = client.preference_sections.topics.with_raw_response.archive(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = response.parse()
+ assert topic is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_archive(self, client: Courier) -> None:
+ with client.preference_sections.topics.with_streaming_response.archive(
+ topic_id="topic_id",
+ section_id="section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = response.parse()
+ assert topic is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_archive(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.archive(
+ topic_id="topic_id",
+ section_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `topic_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.archive(
+ topic_id="",
+ section_id="section_id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace_with_all_params(self, client: Courier) -> None:
+ topic = client.preference_sections.topics.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ allowed_preferences=["snooze"],
+ include_unsubscribe_header=True,
+ routing_options=["direct_message"],
+ topic_data={"foo": "bar"},
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_replace(self, client: Courier) -> None:
+ response = client.preference_sections.topics.with_raw_response.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_replace(self, client: Courier) -> None:
+ with client.preference_sections.topics.with_streaming_response.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_replace(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.replace(
+ topic_id="topic_id",
+ section_id="",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `topic_id` but received ''"):
+ client.preference_sections.topics.with_raw_response.replace(
+ topic_id="",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+
+
+class TestAsyncTopics:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ allowed_preferences=["snooze"],
+ include_unsubscribe_header=True,
+ routing_options=["direct_message"],
+ topic_data={"foo": "bar"},
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.topics.with_raw_response.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.topics.with_streaming_response.create(
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, 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_create(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.create(
+ section_id="",
+ default_status="OPTED_OUT",
+ name="Marketing",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.retrieve(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.topics.with_raw_response.retrieve(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.topics.with_streaming_response.retrieve(
+ topic_id="topic_id",
+ section_id="section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, 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_retrieve(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.retrieve(
+ topic_id="topic_id",
+ section_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `topic_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.retrieve(
+ topic_id="",
+ section_id="section_id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.list(
+ "section_id",
+ )
+ assert_matches_type(PreferenceTopicListResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.topics.with_raw_response.list(
+ "section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicListResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.topics.with_streaming_response.list(
+ "section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicListResponse, topic, 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_list(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_archive(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.archive(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+ assert topic is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_archive(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.topics.with_raw_response.archive(
+ topic_id="topic_id",
+ section_id="section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = await response.parse()
+ assert topic is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_archive(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.topics.with_streaming_response.archive(
+ topic_id="topic_id",
+ section_id="section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = await response.parse()
+ assert topic is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_archive(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.archive(
+ topic_id="topic_id",
+ section_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `topic_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.archive(
+ topic_id="",
+ section_id="section_id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) -> None:
+ topic = await async_client.preference_sections.topics.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ allowed_preferences=["snooze"],
+ include_unsubscribe_header=True,
+ routing_options=["direct_message"],
+ topic_data={"foo": "bar"},
+ )
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_replace(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.topics.with_raw_response.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_replace(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.topics.with_streaming_response.replace(
+ topic_id="topic_id",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ topic = await response.parse()
+ assert_matches_type(PreferenceTopicGetResponse, topic, 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_replace(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.replace(
+ topic_id="topic_id",
+ section_id="",
+ default_status="OPTED_OUT",
+ name="name",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `topic_id` but received ''"):
+ await async_client.preference_sections.topics.with_raw_response.replace(
+ topic_id="",
+ section_id="section_id",
+ default_status="OPTED_OUT",
+ name="name",
+ )
diff --git a/tests/api_resources/test_preference_sections.py b/tests/api_resources/test_preference_sections.py
new file mode 100644
index 00000000..fc02cc3a
--- /dev/null
+++ b/tests/api_resources/test_preference_sections.py
@@ -0,0 +1,510 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from courier import Courier, AsyncCourier
+from tests.utils import assert_matches_type
+from courier.types import (
+ PublishPreferencesResponse,
+ PreferenceSectionGetResponse,
+ PreferenceSectionListResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestPreferenceSections:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Courier) -> None:
+ preference_section = client.preference_sections.create(
+ name="Account Notifications",
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Courier) -> None:
+ preference_section = client.preference_sections.create(
+ name="Account Notifications",
+ has_custom_routing=True,
+ routing_options=["direct_message"],
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Courier) -> None:
+ response = client.preference_sections.with_raw_response.create(
+ name="Account Notifications",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Courier) -> None:
+ with client.preference_sections.with_streaming_response.create(
+ name="Account Notifications",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Courier) -> None:
+ preference_section = client.preference_sections.retrieve(
+ "section_id",
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Courier) -> None:
+ response = client.preference_sections.with_raw_response.retrieve(
+ "section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Courier) -> None:
+ with client.preference_sections.with_streaming_response.retrieve(
+ "section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Courier) -> None:
+ preference_section = client.preference_sections.list()
+ assert_matches_type(PreferenceSectionListResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Courier) -> None:
+ response = client.preference_sections.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionListResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Courier) -> None:
+ with client.preference_sections.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionListResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_archive(self, client: Courier) -> None:
+ preference_section = client.preference_sections.archive(
+ "section_id",
+ )
+ assert preference_section is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_archive(self, client: Courier) -> None:
+ response = client.preference_sections.with_raw_response.archive(
+ "section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = response.parse()
+ assert preference_section is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_archive(self, client: Courier) -> None:
+ with client.preference_sections.with_streaming_response.archive(
+ "section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = response.parse()
+ assert preference_section is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_archive(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.with_raw_response.archive(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_publish(self, client: Courier) -> None:
+ preference_section = client.preference_sections.publish()
+ assert_matches_type(PublishPreferencesResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_publish(self, client: Courier) -> None:
+ response = client.preference_sections.with_raw_response.publish()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = response.parse()
+ assert_matches_type(PublishPreferencesResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_publish(self, client: Courier) -> None:
+ with client.preference_sections.with_streaming_response.publish() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = response.parse()
+ assert_matches_type(PublishPreferencesResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace(self, client: Courier) -> None:
+ preference_section = client.preference_sections.replace(
+ section_id="section_id",
+ name="name",
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace_with_all_params(self, client: Courier) -> None:
+ preference_section = client.preference_sections.replace(
+ section_id="section_id",
+ name="name",
+ has_custom_routing=True,
+ routing_options=["direct_message"],
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_replace(self, client: Courier) -> None:
+ response = client.preference_sections.with_raw_response.replace(
+ section_id="section_id",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_replace(self, client: Courier) -> None:
+ with client.preference_sections.with_streaming_response.replace(
+ section_id="section_id",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_replace(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ client.preference_sections.with_raw_response.replace(
+ section_id="",
+ name="name",
+ )
+
+
+class TestAsyncPreferenceSections:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.create(
+ name="Account Notifications",
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.create(
+ name="Account Notifications",
+ has_custom_routing=True,
+ routing_options=["direct_message"],
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.with_raw_response.create(
+ name="Account Notifications",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.with_streaming_response.create(
+ name="Account Notifications",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.retrieve(
+ "section_id",
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.with_raw_response.retrieve(
+ "section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.with_streaming_response.retrieve(
+ "section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, 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_retrieve(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.list()
+ assert_matches_type(PreferenceSectionListResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionListResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionListResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_archive(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.archive(
+ "section_id",
+ )
+ assert preference_section is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_archive(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.with_raw_response.archive(
+ "section_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = await response.parse()
+ assert preference_section is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_archive(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.with_streaming_response.archive(
+ "section_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = await response.parse()
+ assert preference_section is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_archive(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.with_raw_response.archive(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_publish(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.publish()
+ assert_matches_type(PublishPreferencesResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_publish(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.with_raw_response.publish()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = await response.parse()
+ assert_matches_type(PublishPreferencesResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_publish(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.with_streaming_response.publish() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = await response.parse()
+ assert_matches_type(PublishPreferencesResponse, preference_section, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.replace(
+ section_id="section_id",
+ name="name",
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) -> None:
+ preference_section = await async_client.preference_sections.replace(
+ section_id="section_id",
+ name="name",
+ has_custom_routing=True,
+ routing_options=["direct_message"],
+ )
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_replace(self, async_client: AsyncCourier) -> None:
+ response = await async_client.preference_sections.with_raw_response.replace(
+ section_id="section_id",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_replace(self, async_client: AsyncCourier) -> None:
+ async with async_client.preference_sections.with_streaming_response.replace(
+ section_id="section_id",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preference_section = await response.parse()
+ assert_matches_type(PreferenceSectionGetResponse, preference_section, 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_replace(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `section_id` but received ''"):
+ await async_client.preference_sections.with_raw_response.replace(
+ section_id="",
+ name="name",
+ )
From 40486cfa67bf9e4a45a3e067748120001e4b4e3d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 24 Jun 2026 22:15:40 +0000
Subject: [PATCH 2/2] release: 7.17.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/courier/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 811e1841..f8521830 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "7.16.0"
+ ".": "7.17.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08776047..fea178a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 7.17.0 (2026-06-24)
+
+Full Changelog: [v7.16.0...v7.17.0](https://github.com/trycourier/courier-python/compare/v7.16.0...v7.17.0)
+
+### Features
+
+* **preferences:** workspace preference sections & nested topics endpoints ([362609a](https://github.com/trycourier/courier-python/commit/362609ad7f9f9416c9b3bfabb846128c595dcda6))
+
## 7.16.0 (2026-06-23)
Full Changelog: [v7.15.0...v7.16.0](https://github.com/trycourier/courier-python/compare/v7.15.0...v7.16.0)
diff --git a/pyproject.toml b/pyproject.toml
index 64dfbeeb..340c3095 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "trycourier"
-version = "7.16.0"
+version = "7.17.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 6fb1f77c..313a1059 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.16.0" # x-release-please-version
+__version__ = "7.17.0" # x-release-please-version