From b641cb6819d5727a7ec7f1c3cf240a91a28ffd42 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 30 Jan 2026 04:18:11 +0000
Subject: [PATCH 1/4] feat(client): add custom JSON encoder for extended type
support
---
src/courier/_base_client.py | 7 +-
src/courier/_compat.py | 6 +-
src/courier/_utils/_json.py | 35 ++++++++++
tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++
4 files changed, 169 insertions(+), 5 deletions(-)
create mode 100644 src/courier/_utils/_json.py
create mode 100644 tests/test_utils/test_json.py
diff --git a/src/courier/_base_client.py b/src/courier/_base_client.py
index 724a399..eb7f840 100644
--- a/src/courier/_base_client.py
+++ b/src/courier/_base_client.py
@@ -86,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -554,8 +555,10 @@ def _build_request(
kwargs["content"] = options.content
elif isinstance(json_data, bytes):
kwargs["content"] = json_data
- else:
- kwargs["json"] = json_data if is_given(json_data) else None
+ elif not files:
+ # Don't set content when JSON is sent as multipart/form-data,
+ # since httpx's content param overrides other body arguments
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/courier/_compat.py b/src/courier/_compat.py
index bdef67f..786ff42 100644
--- a/src/courier/_compat.py
+++ b/src/courier/_compat.py
@@ -139,6 +139,7 @@ def model_dump(
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
+ by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
+ by_alias=by_alias,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
- exclude=exclude,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)
diff --git a/src/courier/_utils/_json.py b/src/courier/_utils/_json.py
new file mode 100644
index 0000000..6058421
--- /dev/null
+++ b/src/courier/_utils/_json.py
@@ -0,0 +1,35 @@
+import json
+from typing import Any
+from datetime import datetime
+from typing_extensions import override
+
+import pydantic
+
+from .._compat import model_dump
+
+
+def openapi_dumps(obj: Any) -> bytes:
+ """
+ Serialize an object to UTF-8 encoded JSON bytes.
+
+ Extends the standard json.dumps with support for additional types
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
+ """
+ return json.dumps(
+ obj,
+ cls=_CustomEncoder,
+ # Uses the same defaults as httpx's JSON serialization
+ ensure_ascii=False,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode()
+
+
+class _CustomEncoder(json.JSONEncoder):
+ @override
+ def default(self, o: Any) -> Any:
+ if isinstance(o, datetime):
+ return o.isoformat()
+ if isinstance(o, pydantic.BaseModel):
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
+ return super().default(o)
diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py
new file mode 100644
index 0000000..3f40e78
--- /dev/null
+++ b/tests/test_utils/test_json.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import datetime
+from typing import Union
+
+import pydantic
+
+from courier import _compat
+from courier._utils._json import openapi_dumps
+
+
+class TestOpenapiDumps:
+ def test_basic(self) -> None:
+ data = {"key": "value", "number": 42}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"key":"value","number":42}'
+
+ def test_datetime_serialization(self) -> None:
+ dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
+ data = {"datetime": dt}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}'
+
+ def test_pydantic_model_serialization(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str
+ last_name: str
+ age: int
+
+ model_instance = User(first_name="John", last_name="Kramer", age=83)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}'
+
+ def test_pydantic_model_with_default_values(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+ score: int = 0
+
+ model_instance = User(name="Alice")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Alice"}}'
+
+ def test_pydantic_model_with_default_values_overridden(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+
+ model_instance = User(name="Bob", role="admin", active=False)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}'
+
+ def test_pydantic_model_with_alias(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str = pydantic.Field(alias="firstName")
+ last_name: str = pydantic.Field(alias="lastName")
+
+ model_instance = User(firstName="John", lastName="Doe")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}'
+
+ def test_pydantic_model_with_alias_and_default(self) -> None:
+ class User(pydantic.BaseModel):
+ user_name: str = pydantic.Field(alias="userName")
+ user_role: str = pydantic.Field(default="member", alias="userRole")
+ is_active: bool = pydantic.Field(default=True, alias="isActive")
+
+ model_instance = User(userName="charlie")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"charlie"}}'
+
+ model_with_overrides = User(userName="diana", userRole="admin", isActive=False)
+ data = {"model": model_with_overrides}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}'
+
+ def test_pydantic_model_with_nested_models_and_defaults(self) -> None:
+ class Address(pydantic.BaseModel):
+ street: str
+ city: str = "Unknown"
+
+ class User(pydantic.BaseModel):
+ name: str
+ address: Address
+ verified: bool = False
+
+ if _compat.PYDANTIC_V1:
+ # to handle forward references in Pydantic v1
+ User.update_forward_refs(**locals()) # type: ignore[reportDeprecated]
+
+ address = Address(street="123 Main St")
+ user = User(name="Diana", address=address)
+ data = {"user": user}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}'
+
+ address_with_city = Address(street="456 Oak Ave", city="Boston")
+ user_verified = User(name="Eve", address=address_with_city, verified=True)
+ data = {"user": user_verified}
+ json_bytes = openapi_dumps(data)
+ assert (
+ json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}'
+ )
+
+ def test_pydantic_model_with_optional_fields(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ email: Union[str, None]
+ phone: Union[str, None]
+
+ model_with_none = User(name="Eve", email=None, phone=None)
+ data = {"model": model_with_none}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}'
+
+ model_with_values = User(name="Frank", email="frank@example.com", phone=None)
+ data = {"model": model_with_values}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'
From 76ccdfd9aee812c97cbad0d10b13b5746db4b24e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 5 Feb 2026 17:28:45 +0000
Subject: [PATCH 2/4] feat(api): support list of recipients in send message
---
.stats.yml | 4 ++--
src/courier/types/send_message_params.py | 15 ++++++++++++++-
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index edd99bd..e1e808f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 78
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-4469c7d243ac17a71d48187ede11d7f6fd178d1006f2542c973259c5c37007fb.yml
-openapi_spec_hash: 2036a46b6fa7ac8eae981583bd452458
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-2cde866450022e180983325c70421aa17a47ae9dbf6a7dbd935f3279d61a0172.yml
+openapi_spec_hash: d805377811f69d0b37c578ebf9d6bada
config_hash: 93eb861d9572cea4d66edeab309e08c6
diff --git a/src/courier/types/send_message_params.py b/src/courier/types/send_message_params.py
index d51083a..1114143 100644
--- a/src/courier/types/send_message_params.py
+++ b/src/courier/types/send_message_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Dict, Union, Optional
+from typing import Dict, Union, Iterable, Optional
from typing_extensions import Literal, Required, TypeAlias, TypedDict
from .._types import SequenceNotStr
@@ -35,6 +35,7 @@
"MessageRouting",
"MessageTimeout",
"MessageTo",
+ "MessageToUnionMember8",
]
@@ -167,6 +168,17 @@ class MessageTimeout(TypedDict, total=False):
provider: Optional[Dict[str, int]]
+MessageToUnionMember8: TypeAlias = Union[
+ UserRecipient,
+ AudienceRecipient,
+ ListRecipient,
+ ListPatternRecipient,
+ SlackRecipient,
+ MsTeamsRecipient,
+ PagerdutyRecipient,
+ WebhookRecipient,
+]
+
MessageTo: TypeAlias = Union[
UserRecipient,
AudienceRecipient,
@@ -176,6 +188,7 @@ class MessageTimeout(TypedDict, total=False):
MsTeamsRecipient,
PagerdutyRecipient,
WebhookRecipient,
+ Iterable[MessageToUnionMember8],
]
From 2b397816d696929cf5ad38b137b3c624f897ee26 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 18:58:41 +0000
Subject: [PATCH 3/4] feat(api): add publish/replace methods, versions resource
to tenants.templates
---
.stats.yml | 8 +-
api.md | 17 +-
src/courier/resources/tenants/templates.py | 284 ---------
.../resources/tenants/templates/__init__.py | 33 ++
.../resources/tenants/templates/templates.py | 545 ++++++++++++++++++
.../resources/tenants/templates/versions.py | 187 ++++++
src/courier/resources/tenants/tenants.py | 16 +-
src/courier/types/__init__.py | 5 +
.../post_tenant_template_publish_response.py | 18 +
.../types/put_tenant_template_response.py | 23 +
.../types/tenant_template_input_param.py | 102 ++++
src/courier/types/tenants/__init__.py | 2 +
.../types/tenants/template_publish_params.py | 17 +
.../types/tenants/template_replace_params.py | 24 +
.../types/tenants/templates/__init__.py | 3 +
.../tenants/templates/__init__.py | 1 +
.../tenants/templates/test_versions.py | 152 +++++
tests/api_resources/tenants/test_templates.py | 432 +++++++++++++-
18 files changed, 1569 insertions(+), 300 deletions(-)
delete mode 100644 src/courier/resources/tenants/templates.py
create mode 100644 src/courier/resources/tenants/templates/__init__.py
create mode 100644 src/courier/resources/tenants/templates/templates.py
create mode 100644 src/courier/resources/tenants/templates/versions.py
create mode 100644 src/courier/types/post_tenant_template_publish_response.py
create mode 100644 src/courier/types/put_tenant_template_response.py
create mode 100644 src/courier/types/tenant_template_input_param.py
create mode 100644 src/courier/types/tenants/template_publish_params.py
create mode 100644 src/courier/types/tenants/template_replace_params.py
create mode 100644 src/courier/types/tenants/templates/__init__.py
create mode 100644 tests/api_resources/tenants/templates/__init__.py
create mode 100644 tests/api_resources/tenants/templates/test_versions.py
diff --git a/.stats.yml b/.stats.yml
index e1e808f..17749e2 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 78
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-2cde866450022e180983325c70421aa17a47ae9dbf6a7dbd935f3279d61a0172.yml
-openapi_spec_hash: d805377811f69d0b37c578ebf9d6bada
-config_hash: 93eb861d9572cea4d66edeab309e08c6
+configured_endpoints: 81
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-3fc1c86b4a83a16393aaf17d1fb3ac6098d30dd057ba872973b57285a7a3f0d0.yml
+openapi_spec_hash: 02a545d217b13399f311e99561f9de1d
+config_hash: 0789c3cddc625bb9712b3bded274ab6c
diff --git a/api.md b/api.md
index b0e8a70..5cbd3da 100644
--- a/api.md
+++ b/api.md
@@ -357,9 +357,14 @@ Types:
from courier.types import (
BaseTemplateTenantAssociation,
DefaultPreferences,
+ PostTenantTemplatePublishRequest,
+ PostTenantTemplatePublishResponse,
+ PutTenantTemplateRequest,
+ PutTenantTemplateResponse,
SubscriptionTopicNew,
Tenant,
TenantAssociation,
+ TenantTemplateInput,
TenantListResponse,
TenantListUsersResponse,
)
@@ -392,8 +397,16 @@ from courier.types.tenants import TemplateListResponse
Methods:
-- client.tenants.templates.retrieve(template_id, \*, tenant_id) -> BaseTemplateTenantAssociation
-- client.tenants.templates.list(tenant_id, \*\*params) -> TemplateListResponse
+- client.tenants.templates.retrieve(template_id, \*, tenant_id) -> BaseTemplateTenantAssociation
+- client.tenants.templates.list(tenant_id, \*\*params) -> TemplateListResponse
+- client.tenants.templates.publish(template_id, \*, tenant_id, \*\*params) -> PostTenantTemplatePublishResponse
+- client.tenants.templates.replace(template_id, \*, tenant_id, \*\*params) -> PutTenantTemplateResponse
+
+### Versions
+
+Methods:
+
+- client.tenants.templates.versions.retrieve(version, \*, tenant_id, template_id) -> BaseTemplateTenantAssociation
# Translations
diff --git a/src/courier/resources/tenants/templates.py b/src/courier/resources/tenants/templates.py
deleted file mode 100644
index c26db8d..0000000
--- a/src/courier/resources/tenants/templates.py
+++ /dev/null
@@ -1,284 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Optional
-
-import httpx
-
-from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import 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.tenants import template_list_params
-from ...types.tenants.template_list_response import TemplateListResponse
-from ...types.base_template_tenant_association import BaseTemplateTenantAssociation
-
-__all__ = ["TemplatesResource", "AsyncTemplatesResource"]
-
-
-class TemplatesResource(SyncAPIResource):
- @cached_property
- def with_raw_response(self) -> TemplatesResourceWithRawResponse:
- """
- 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 TemplatesResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> TemplatesResourceWithStreamingResponse:
- """
- 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 TemplatesResourceWithStreamingResponse(self)
-
- def retrieve(
- self,
- template_id: str,
- *,
- tenant_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,
- ) -> BaseTemplateTenantAssociation:
- """
- Get a Template in Tenant
-
- 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 tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- if not template_id:
- raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
- return self._get(
- f"/tenants/{tenant_id}/templates/{template_id}",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=BaseTemplateTenantAssociation,
- )
-
- def list(
- self,
- tenant_id: str,
- *,
- cursor: Optional[str] | Omit = omit,
- limit: Optional[int] | 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,
- ) -> TemplateListResponse:
- """
- List Templates in Tenant
-
- Args:
- cursor: Continue the pagination with the next cursor
-
- limit: The number of templates to return (defaults to 20, maximum value of 100)
-
- 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 tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- return self._get(
- f"/tenants/{tenant_id}/templates",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "cursor": cursor,
- "limit": limit,
- },
- template_list_params.TemplateListParams,
- ),
- ),
- cast_to=TemplateListResponse,
- )
-
-
-class AsyncTemplatesResource(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncTemplatesResourceWithRawResponse:
- """
- 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 AsyncTemplatesResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncTemplatesResourceWithStreamingResponse:
- """
- 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 AsyncTemplatesResourceWithStreamingResponse(self)
-
- async def retrieve(
- self,
- template_id: str,
- *,
- tenant_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,
- ) -> BaseTemplateTenantAssociation:
- """
- Get a Template in Tenant
-
- 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 tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- if not template_id:
- raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
- return await self._get(
- f"/tenants/{tenant_id}/templates/{template_id}",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=BaseTemplateTenantAssociation,
- )
-
- async def list(
- self,
- tenant_id: str,
- *,
- cursor: Optional[str] | Omit = omit,
- limit: Optional[int] | 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,
- ) -> TemplateListResponse:
- """
- List Templates in Tenant
-
- Args:
- cursor: Continue the pagination with the next cursor
-
- limit: The number of templates to return (defaults to 20, maximum value of 100)
-
- 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 tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- return await self._get(
- f"/tenants/{tenant_id}/templates",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform(
- {
- "cursor": cursor,
- "limit": limit,
- },
- template_list_params.TemplateListParams,
- ),
- ),
- cast_to=TemplateListResponse,
- )
-
-
-class TemplatesResourceWithRawResponse:
- def __init__(self, templates: TemplatesResource) -> None:
- self._templates = templates
-
- self.retrieve = to_raw_response_wrapper(
- templates.retrieve,
- )
- self.list = to_raw_response_wrapper(
- templates.list,
- )
-
-
-class AsyncTemplatesResourceWithRawResponse:
- def __init__(self, templates: AsyncTemplatesResource) -> None:
- self._templates = templates
-
- self.retrieve = async_to_raw_response_wrapper(
- templates.retrieve,
- )
- self.list = async_to_raw_response_wrapper(
- templates.list,
- )
-
-
-class TemplatesResourceWithStreamingResponse:
- def __init__(self, templates: TemplatesResource) -> None:
- self._templates = templates
-
- self.retrieve = to_streamed_response_wrapper(
- templates.retrieve,
- )
- self.list = to_streamed_response_wrapper(
- templates.list,
- )
-
-
-class AsyncTemplatesResourceWithStreamingResponse:
- def __init__(self, templates: AsyncTemplatesResource) -> None:
- self._templates = templates
-
- self.retrieve = async_to_streamed_response_wrapper(
- templates.retrieve,
- )
- self.list = async_to_streamed_response_wrapper(
- templates.list,
- )
diff --git a/src/courier/resources/tenants/templates/__init__.py b/src/courier/resources/tenants/templates/__init__.py
new file mode 100644
index 0000000..fdb7495
--- /dev/null
+++ b/src/courier/resources/tenants/templates/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .versions import (
+ VersionsResource,
+ AsyncVersionsResource,
+ VersionsResourceWithRawResponse,
+ AsyncVersionsResourceWithRawResponse,
+ VersionsResourceWithStreamingResponse,
+ AsyncVersionsResourceWithStreamingResponse,
+)
+from .templates import (
+ TemplatesResource,
+ AsyncTemplatesResource,
+ TemplatesResourceWithRawResponse,
+ AsyncTemplatesResourceWithRawResponse,
+ TemplatesResourceWithStreamingResponse,
+ AsyncTemplatesResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "VersionsResource",
+ "AsyncVersionsResource",
+ "VersionsResourceWithRawResponse",
+ "AsyncVersionsResourceWithRawResponse",
+ "VersionsResourceWithStreamingResponse",
+ "AsyncVersionsResourceWithStreamingResponse",
+ "TemplatesResource",
+ "AsyncTemplatesResource",
+ "TemplatesResourceWithRawResponse",
+ "AsyncTemplatesResourceWithRawResponse",
+ "TemplatesResourceWithStreamingResponse",
+ "AsyncTemplatesResourceWithStreamingResponse",
+]
diff --git a/src/courier/resources/tenants/templates/templates.py b/src/courier/resources/tenants/templates/templates.py
new file mode 100644
index 0000000..87ad2d6
--- /dev/null
+++ b/src/courier/resources/tenants/templates/templates.py
@@ -0,0 +1,545 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+
+import httpx
+
+from .versions import (
+ VersionsResource,
+ AsyncVersionsResource,
+ VersionsResourceWithRawResponse,
+ AsyncVersionsResourceWithRawResponse,
+ VersionsResourceWithStreamingResponse,
+ AsyncVersionsResourceWithStreamingResponse,
+)
+from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ...._utils import 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.tenants import template_list_params, template_publish_params, template_replace_params
+from ....types.tenant_template_input_param import TenantTemplateInputParam
+from ....types.put_tenant_template_response import PutTenantTemplateResponse
+from ....types.tenants.template_list_response import TemplateListResponse
+from ....types.base_template_tenant_association import BaseTemplateTenantAssociation
+from ....types.post_tenant_template_publish_response import PostTenantTemplatePublishResponse
+
+__all__ = ["TemplatesResource", "AsyncTemplatesResource"]
+
+
+class TemplatesResource(SyncAPIResource):
+ @cached_property
+ def versions(self) -> VersionsResource:
+ return VersionsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> TemplatesResourceWithRawResponse:
+ """
+ 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 TemplatesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> TemplatesResourceWithStreamingResponse:
+ """
+ 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 TemplatesResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ template_id: str,
+ *,
+ tenant_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,
+ ) -> BaseTemplateTenantAssociation:
+ """
+ Get a Template in Tenant
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/templates/{template_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseTemplateTenantAssociation,
+ )
+
+ def list(
+ self,
+ tenant_id: str,
+ *,
+ cursor: Optional[str] | Omit = omit,
+ limit: Optional[int] | 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,
+ ) -> TemplateListResponse:
+ """
+ List Templates in Tenant
+
+ Args:
+ cursor: Continue the pagination with the next cursor
+
+ limit: The number of templates to return (defaults to 20, maximum value of 100)
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/templates",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "cursor": cursor,
+ "limit": limit,
+ },
+ template_list_params.TemplateListParams,
+ ),
+ ),
+ cast_to=TemplateListResponse,
+ )
+
+ def publish(
+ self,
+ template_id: str,
+ *,
+ tenant_id: str,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> PostTenantTemplatePublishResponse:
+ """
+ Publishes a specific version of a notification template for a tenant.
+
+ The template must already exist in the tenant's notification map. If no version
+ is specified, defaults to publishing the "latest" version.
+
+ Args:
+ version: The version of the template to publish (e.g., "v1", "v2", "latest"). If not
+ provided, defaults to "latest".
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._post(
+ f"/tenants/{tenant_id}/templates/{template_id}/publish",
+ body=maybe_transform({"version": version}, template_publish_params.TemplatePublishParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PostTenantTemplatePublishResponse,
+ )
+
+ def replace(
+ self,
+ template_id: str,
+ *,
+ tenant_id: str,
+ template: TenantTemplateInputParam,
+ published: bool | 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,
+ ) -> PutTenantTemplateResponse:
+ """
+ Creates or updates a notification template for a tenant.
+
+ If the template already exists for the tenant, it will be updated (200).
+ Otherwise, a new template is created (201).
+
+ Optionally publishes the template immediately if the `published` flag is set to
+ true.
+
+ Args:
+ template: Template configuration for creating or updating a tenant notification template
+
+ published: Whether to publish the template immediately after saving. When true, the
+ template becomes the active/published version. When false (default), the
+ template is saved as a draft.
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._put(
+ f"/tenants/{tenant_id}/templates/{template_id}",
+ body=maybe_transform(
+ {
+ "template": template,
+ "published": published,
+ },
+ template_replace_params.TemplateReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PutTenantTemplateResponse,
+ )
+
+
+class AsyncTemplatesResource(AsyncAPIResource):
+ @cached_property
+ def versions(self) -> AsyncVersionsResource:
+ return AsyncVersionsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncTemplatesResourceWithRawResponse:
+ """
+ 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 AsyncTemplatesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncTemplatesResourceWithStreamingResponse:
+ """
+ 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 AsyncTemplatesResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ template_id: str,
+ *,
+ tenant_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,
+ ) -> BaseTemplateTenantAssociation:
+ """
+ Get a Template in Tenant
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/templates/{template_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseTemplateTenantAssociation,
+ )
+
+ async def list(
+ self,
+ tenant_id: str,
+ *,
+ cursor: Optional[str] | Omit = omit,
+ limit: Optional[int] | 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,
+ ) -> TemplateListResponse:
+ """
+ List Templates in Tenant
+
+ Args:
+ cursor: Continue the pagination with the next cursor
+
+ limit: The number of templates to return (defaults to 20, maximum value of 100)
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/templates",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "cursor": cursor,
+ "limit": limit,
+ },
+ template_list_params.TemplateListParams,
+ ),
+ ),
+ cast_to=TemplateListResponse,
+ )
+
+ async def publish(
+ self,
+ template_id: str,
+ *,
+ tenant_id: str,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> PostTenantTemplatePublishResponse:
+ """
+ Publishes a specific version of a notification template for a tenant.
+
+ The template must already exist in the tenant's notification map. If no version
+ is specified, defaults to publishing the "latest" version.
+
+ Args:
+ version: The version of the template to publish (e.g., "v1", "v2", "latest"). If not
+ provided, defaults to "latest".
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._post(
+ f"/tenants/{tenant_id}/templates/{template_id}/publish",
+ body=await async_maybe_transform({"version": version}, template_publish_params.TemplatePublishParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PostTenantTemplatePublishResponse,
+ )
+
+ async def replace(
+ self,
+ template_id: str,
+ *,
+ tenant_id: str,
+ template: TenantTemplateInputParam,
+ published: bool | 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,
+ ) -> PutTenantTemplateResponse:
+ """
+ Creates or updates a notification template for a tenant.
+
+ If the template already exists for the tenant, it will be updated (200).
+ Otherwise, a new template is created (201).
+
+ Optionally publishes the template immediately if the `published` flag is set to
+ true.
+
+ Args:
+ template: Template configuration for creating or updating a tenant notification template
+
+ published: Whether to publish the template immediately after saving. When true, the
+ template becomes the active/published version. When false (default), the
+ template is saved as a draft.
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._put(
+ f"/tenants/{tenant_id}/templates/{template_id}",
+ body=await async_maybe_transform(
+ {
+ "template": template,
+ "published": published,
+ },
+ template_replace_params.TemplateReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PutTenantTemplateResponse,
+ )
+
+
+class TemplatesResourceWithRawResponse:
+ def __init__(self, templates: TemplatesResource) -> None:
+ self._templates = templates
+
+ self.retrieve = to_raw_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ templates.list,
+ )
+ self.publish = to_raw_response_wrapper(
+ templates.publish,
+ )
+ self.replace = to_raw_response_wrapper(
+ templates.replace,
+ )
+
+ @cached_property
+ def versions(self) -> VersionsResourceWithRawResponse:
+ return VersionsResourceWithRawResponse(self._templates.versions)
+
+
+class AsyncTemplatesResourceWithRawResponse:
+ def __init__(self, templates: AsyncTemplatesResource) -> None:
+ self._templates = templates
+
+ self.retrieve = async_to_raw_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ templates.list,
+ )
+ self.publish = async_to_raw_response_wrapper(
+ templates.publish,
+ )
+ self.replace = async_to_raw_response_wrapper(
+ templates.replace,
+ )
+
+ @cached_property
+ def versions(self) -> AsyncVersionsResourceWithRawResponse:
+ return AsyncVersionsResourceWithRawResponse(self._templates.versions)
+
+
+class TemplatesResourceWithStreamingResponse:
+ def __init__(self, templates: TemplatesResource) -> None:
+ self._templates = templates
+
+ self.retrieve = to_streamed_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ templates.list,
+ )
+ self.publish = to_streamed_response_wrapper(
+ templates.publish,
+ )
+ self.replace = to_streamed_response_wrapper(
+ templates.replace,
+ )
+
+ @cached_property
+ def versions(self) -> VersionsResourceWithStreamingResponse:
+ return VersionsResourceWithStreamingResponse(self._templates.versions)
+
+
+class AsyncTemplatesResourceWithStreamingResponse:
+ def __init__(self, templates: AsyncTemplatesResource) -> None:
+ self._templates = templates
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ templates.list,
+ )
+ self.publish = async_to_streamed_response_wrapper(
+ templates.publish,
+ )
+ self.replace = async_to_streamed_response_wrapper(
+ templates.replace,
+ )
+
+ @cached_property
+ def versions(self) -> AsyncVersionsResourceWithStreamingResponse:
+ return AsyncVersionsResourceWithStreamingResponse(self._templates.versions)
diff --git a/src/courier/resources/tenants/templates/versions.py b/src/courier/resources/tenants/templates/versions.py
new file mode 100644
index 0000000..bcbef61
--- /dev/null
+++ b/src/courier/resources/tenants/templates/versions.py
@@ -0,0 +1,187 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ...._types import Body, Query, Headers, NotGiven, not_given
+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.base_template_tenant_association import BaseTemplateTenantAssociation
+
+__all__ = ["VersionsResource", "AsyncVersionsResource"]
+
+
+class VersionsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> VersionsResourceWithRawResponse:
+ """
+ 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 VersionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> VersionsResourceWithStreamingResponse:
+ """
+ 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 VersionsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ version: str,
+ *,
+ tenant_id: str,
+ template_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,
+ ) -> BaseTemplateTenantAssociation:
+ """
+ Fetches a specific version of a tenant template.
+
+ Supports the following version formats:
+
+ - `latest` - The most recent version of the template
+ - `published` - The currently published version
+ - `v{version}` - A specific version (e.g., "v1", "v2", "v1.0.0")
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not version:
+ raise ValueError(f"Expected a non-empty value for `version` but received {version!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/templates/{template_id}/versions/{version}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseTemplateTenantAssociation,
+ )
+
+
+class AsyncVersionsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncVersionsResourceWithRawResponse:
+ """
+ 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 AsyncVersionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncVersionsResourceWithStreamingResponse:
+ """
+ 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 AsyncVersionsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ version: str,
+ *,
+ tenant_id: str,
+ template_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,
+ ) -> BaseTemplateTenantAssociation:
+ """
+ Fetches a specific version of a tenant template.
+
+ Supports the following version formats:
+
+ - `latest` - The most recent version of the template
+ - `published` - The currently published version
+ - `v{version}` - A specific version (e.g., "v1", "v2", "v1.0.0")
+
+ 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 tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not version:
+ raise ValueError(f"Expected a non-empty value for `version` but received {version!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/templates/{template_id}/versions/{version}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BaseTemplateTenantAssociation,
+ )
+
+
+class VersionsResourceWithRawResponse:
+ def __init__(self, versions: VersionsResource) -> None:
+ self._versions = versions
+
+ self.retrieve = to_raw_response_wrapper(
+ versions.retrieve,
+ )
+
+
+class AsyncVersionsResourceWithRawResponse:
+ def __init__(self, versions: AsyncVersionsResource) -> None:
+ self._versions = versions
+
+ self.retrieve = async_to_raw_response_wrapper(
+ versions.retrieve,
+ )
+
+
+class VersionsResourceWithStreamingResponse:
+ def __init__(self, versions: VersionsResource) -> None:
+ self._versions = versions
+
+ self.retrieve = to_streamed_response_wrapper(
+ versions.retrieve,
+ )
+
+
+class AsyncVersionsResourceWithStreamingResponse:
+ def __init__(self, versions: AsyncVersionsResource) -> None:
+ self._versions = versions
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ versions.retrieve,
+ )
diff --git a/src/courier/resources/tenants/tenants.py b/src/courier/resources/tenants/tenants.py
index 33feb88..c1fd3ea 100644
--- a/src/courier/resources/tenants/tenants.py
+++ b/src/courier/resources/tenants/tenants.py
@@ -10,14 +10,6 @@
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
-from .templates import (
- TemplatesResource,
- AsyncTemplatesResource,
- TemplatesResourceWithRawResponse,
- AsyncTemplatesResourceWithRawResponse,
- TemplatesResourceWithStreamingResponse,
- AsyncTemplatesResourceWithStreamingResponse,
-)
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
to_raw_response_wrapper,
@@ -27,6 +19,14 @@
)
from ..._base_client import make_request_options
from ...types.tenant import Tenant
+from .templates.templates import (
+ TemplatesResource,
+ AsyncTemplatesResource,
+ TemplatesResourceWithRawResponse,
+ AsyncTemplatesResourceWithRawResponse,
+ TemplatesResourceWithStreamingResponse,
+ AsyncTemplatesResourceWithStreamingResponse,
+)
from .preferences.preferences import (
PreferencesResource,
AsyncPreferencesResource,
diff --git a/src/courier/types/__init__.py b/src/courier/types/__init__.py
index 128f19b..8db3805 100644
--- a/src/courier/types/__init__.py
+++ b/src/courier/types/__init__.py
@@ -166,8 +166,10 @@
from .notification_list_response import NotificationListResponse as NotificationListResponse
from .tenant_list_users_response import TenantListUsersResponse as TenantListUsersResponse
from .brand_settings_in_app_param import BrandSettingsInAppParam as BrandSettingsInAppParam
+from .tenant_template_input_param import TenantTemplateInputParam as TenantTemplateInputParam
from .audience_list_members_params import AudienceListMembersParams as AudienceListMembersParams
from .inbound_track_event_response import InboundTrackEventResponse as InboundTrackEventResponse
+from .put_tenant_template_response import PutTenantTemplateResponse as PutTenantTemplateResponse
from .subscription_topic_new_param import SubscriptionTopicNewParam as SubscriptionTopicNewParam
from .translation_retrieve_response import TranslationRetrieveResponse as TranslationRetrieveResponse
from .audience_list_members_response import AudienceListMembersResponse as AudienceListMembersResponse
@@ -175,6 +177,9 @@
from .base_template_tenant_association import BaseTemplateTenantAssociation as BaseTemplateTenantAssociation
from .automation_template_list_response import AutomationTemplateListResponse as AutomationTemplateListResponse
from .put_subscriptions_recipient_param import PutSubscriptionsRecipientParam as PutSubscriptionsRecipientParam
+from .post_tenant_template_publish_response import (
+ PostTenantTemplatePublishResponse as PostTenantTemplatePublishResponse,
+)
from .subscribe_to_lists_request_item_param import SubscribeToListsRequestItemParam as SubscribeToListsRequestItemParam
# Rebuild cyclical models only after all modules are imported.
diff --git a/src/courier/types/post_tenant_template_publish_response.py b/src/courier/types/post_tenant_template_publish_response.py
new file mode 100644
index 0000000..5cc5615
--- /dev/null
+++ b/src/courier/types/post_tenant_template_publish_response.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["PostTenantTemplatePublishResponse"]
+
+
+class PostTenantTemplatePublishResponse(BaseModel):
+ """Response from publishing a tenant template"""
+
+ id: str
+ """The template ID"""
+
+ published_at: str
+ """The timestamp when the template was published"""
+
+ version: str
+ """The published version of the template"""
diff --git a/src/courier/types/put_tenant_template_response.py b/src/courier/types/put_tenant_template_response.py
new file mode 100644
index 0000000..3f5dd52
--- /dev/null
+++ b/src/courier/types/put_tenant_template_response.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["PutTenantTemplateResponse"]
+
+
+class PutTenantTemplateResponse(BaseModel):
+ """Response from creating or updating a tenant notification template"""
+
+ id: str
+ """The template ID"""
+
+ version: str
+ """The version of the saved template"""
+
+ published_at: Optional[str] = None
+ """The timestamp when the template was published.
+
+ Only present if the template was published as part of this request.
+ """
diff --git a/src/courier/types/tenant_template_input_param.py b/src/courier/types/tenant_template_input_param.py
new file mode 100644
index 0000000..d3604fe
--- /dev/null
+++ b/src/courier/types/tenant_template_input_param.py
@@ -0,0 +1,102 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Optional
+from typing_extensions import Literal, Required, TypedDict
+
+from .._types import SequenceNotStr
+from .shared_params.utm import Utm
+from .shared_params.elemental_content import ElementalContent
+
+__all__ = [
+ "TenantTemplateInputParam",
+ "Channels",
+ "ChannelsMetadata",
+ "ChannelsTimeouts",
+ "Providers",
+ "ProvidersMetadata",
+]
+
+
+class ChannelsMetadata(TypedDict, total=False):
+ utm: Optional[Utm]
+
+
+class ChannelsTimeouts(TypedDict, total=False):
+ channel: Optional[int]
+
+ provider: Optional[int]
+
+
+_ChannelsReservedKeywords = TypedDict(
+ "_ChannelsReservedKeywords",
+ {
+ "if": Optional[str],
+ },
+ total=False,
+)
+
+
+class Channels(_ChannelsReservedKeywords, total=False):
+ brand_id: Optional[str]
+ """Brand id used for rendering."""
+
+ metadata: Optional[ChannelsMetadata]
+
+ override: Optional[Dict[str, object]]
+ """Channel specific overrides."""
+
+ providers: Optional[SequenceNotStr[str]]
+ """Providers enabled for this channel."""
+
+ routing_method: Optional[Literal["all", "single"]]
+ """Defaults to `single`."""
+
+ timeouts: Optional[ChannelsTimeouts]
+
+
+class ProvidersMetadata(TypedDict, total=False):
+ utm: Optional[Utm]
+
+
+_ProvidersReservedKeywords = TypedDict(
+ "_ProvidersReservedKeywords",
+ {
+ "if": Optional[str],
+ },
+ total=False,
+)
+
+
+class Providers(_ProvidersReservedKeywords, total=False):
+ metadata: Optional[ProvidersMetadata]
+
+ override: Optional[Dict[str, object]]
+ """Provider-specific overrides."""
+
+ timeouts: Optional[int]
+
+
+class TenantTemplateInputParam(TypedDict, total=False):
+ """Template configuration for creating or updating a tenant notification template"""
+
+ content: Required[ElementalContent]
+ """
+ Template content configuration including blocks, elements, and message structure
+ """
+
+ channels: Dict[str, Channels]
+ """Channel-specific delivery configuration (email, SMS, push, etc.)"""
+
+ providers: Dict[str, Providers]
+ """
+ Provider-specific delivery configuration for routing to specific email/SMS
+ providers
+ """
+
+ routing: "MessageRouting"
+ """Message routing configuration for multi-channel delivery strategies"""
+
+
+from .shared_params.message_routing import MessageRouting
diff --git a/src/courier/types/tenants/__init__.py b/src/courier/types/tenants/__init__.py
index dad61d1..bd05988 100644
--- a/src/courier/types/tenants/__init__.py
+++ b/src/courier/types/tenants/__init__.py
@@ -4,3 +4,5 @@
from .template_list_params import TemplateListParams as TemplateListParams
from .template_list_response import TemplateListResponse as TemplateListResponse
+from .template_publish_params import TemplatePublishParams as TemplatePublishParams
+from .template_replace_params import TemplateReplaceParams as TemplateReplaceParams
diff --git a/src/courier/types/tenants/template_publish_params.py b/src/courier/types/tenants/template_publish_params.py
new file mode 100644
index 0000000..75f3570
--- /dev/null
+++ b/src/courier/types/tenants/template_publish_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["TemplatePublishParams"]
+
+
+class TemplatePublishParams(TypedDict, total=False):
+ tenant_id: Required[str]
+
+ version: str
+ """The version of the template to publish (e.g., "v1", "v2", "latest").
+
+ If not provided, defaults to "latest".
+ """
diff --git a/src/courier/types/tenants/template_replace_params.py b/src/courier/types/tenants/template_replace_params.py
new file mode 100644
index 0000000..69a92b1
--- /dev/null
+++ b/src/courier/types/tenants/template_replace_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["TemplateReplaceParams"]
+
+
+class TemplateReplaceParams(TypedDict, total=False):
+ tenant_id: Required[str]
+
+ template: Required["TenantTemplateInputParam"]
+ """Template configuration for creating or updating a tenant notification template"""
+
+ published: bool
+ """Whether to publish the template immediately after saving.
+
+ When true, the template becomes the active/published version. When false
+ (default), the template is saved as a draft.
+ """
+
+
+from ..tenant_template_input_param import TenantTemplateInputParam
diff --git a/src/courier/types/tenants/templates/__init__.py b/src/courier/types/tenants/templates/__init__.py
new file mode 100644
index 0000000..f8ee8b1
--- /dev/null
+++ b/src/courier/types/tenants/templates/__init__.py
@@ -0,0 +1,3 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
diff --git a/tests/api_resources/tenants/templates/__init__.py b/tests/api_resources/tenants/templates/__init__.py
new file mode 100644
index 0000000..fd8019a
--- /dev/null
+++ b/tests/api_resources/tenants/templates/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/tenants/templates/test_versions.py b/tests/api_resources/tenants/templates/test_versions.py
new file mode 100644
index 0000000..dcc8f82
--- /dev/null
+++ b/tests/api_resources/tenants/templates/test_versions.py
@@ -0,0 +1,152 @@
+# 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 BaseTemplateTenantAssociation
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestVersions:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Courier) -> None:
+ version = client.tenants.templates.versions.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ )
+ assert_matches_type(BaseTemplateTenantAssociation, version, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Courier) -> None:
+ response = client.tenants.templates.versions.with_raw_response.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ version = response.parse()
+ assert_matches_type(BaseTemplateTenantAssociation, version, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Courier) -> None:
+ with client.tenants.templates.versions.with_streaming_response.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ version = response.parse()
+ assert_matches_type(BaseTemplateTenantAssociation, version, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism 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 `tenant_id` but received ''"):
+ client.tenants.templates.versions.with_raw_response.retrieve(
+ version="version",
+ tenant_id="",
+ template_id="template_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.tenants.templates.versions.with_raw_response.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `version` but received ''"):
+ client.tenants.templates.versions.with_raw_response.retrieve(
+ version="",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ )
+
+
+class TestAsyncVersions:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncCourier) -> None:
+ version = await async_client.tenants.templates.versions.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ )
+ assert_matches_type(BaseTemplateTenantAssociation, version, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncCourier) -> None:
+ response = await async_client.tenants.templates.versions.with_raw_response.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ version = await response.parse()
+ assert_matches_type(BaseTemplateTenantAssociation, version, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncCourier) -> None:
+ async with async_client.tenants.templates.versions.with_streaming_response.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ version = await response.parse()
+ assert_matches_type(BaseTemplateTenantAssociation, version, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism 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 `tenant_id` but received ''"):
+ await async_client.tenants.templates.versions.with_raw_response.retrieve(
+ version="version",
+ tenant_id="",
+ template_id="template_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.tenants.templates.versions.with_raw_response.retrieve(
+ version="version",
+ tenant_id="tenant_id",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `version` but received ''"):
+ await async_client.tenants.templates.versions.with_raw_response.retrieve(
+ version="",
+ tenant_id="tenant_id",
+ template_id="template_id",
+ )
diff --git a/tests/api_resources/tenants/test_templates.py b/tests/api_resources/tenants/test_templates.py
index fe49113..6805fb0 100644
--- a/tests/api_resources/tenants/test_templates.py
+++ b/tests/api_resources/tenants/test_templates.py
@@ -9,8 +9,14 @@
from courier import Courier, AsyncCourier
from tests.utils import assert_matches_type
-from courier.types import BaseTemplateTenantAssociation
-from courier.types.tenants import TemplateListResponse
+from courier.types import (
+ PutTenantTemplateResponse,
+ BaseTemplateTenantAssociation,
+ PostTenantTemplatePublishResponse,
+)
+from courier.types.tenants import (
+ TemplateListResponse,
+)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -122,6 +128,217 @@ def test_path_params_list(self, client: Courier) -> None:
tenant_id="",
)
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_publish(self, client: Courier) -> None:
+ template = client.tenants.templates.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_publish_with_all_params(self, client: Courier) -> None:
+ template = client.tenants.templates.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ version="version",
+ )
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_publish(self, client: Courier) -> None:
+ response = client.tenants.templates.with_raw_response.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_publish(self, client: Courier) -> None:
+ with client.tenants.templates.with_streaming_response.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_path_params_publish(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.templates.with_raw_response.publish(
+ template_id="template_id",
+ tenant_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.tenants.templates.with_raw_response.publish(
+ template_id="",
+ tenant_id="tenant_id",
+ )
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_replace(self, client: Courier) -> None:
+ template = client.tenants.templates.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_replace_with_all_params(self, client: Courier) -> None:
+ template = client.tenants.templates.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [
+ {
+ "channels": ["string"],
+ "if": "if",
+ "loop": "loop",
+ "ref": "ref",
+ "type": "text",
+ }
+ ],
+ "version": "version",
+ "brand": "brand",
+ },
+ "channels": {
+ "foo": {
+ "brand_id": "brand_id",
+ "if": "if",
+ "metadata": {
+ "utm": {
+ "campaign": "campaign",
+ "content": "content",
+ "medium": "medium",
+ "source": "source",
+ "term": "term",
+ }
+ },
+ "override": {"foo": "bar"},
+ "providers": ["string"],
+ "routing_method": "all",
+ "timeouts": {
+ "channel": 0,
+ "provider": 0,
+ },
+ }
+ },
+ "providers": {
+ "foo": {
+ "if": "if",
+ "metadata": {
+ "utm": {
+ "campaign": "campaign",
+ "content": "content",
+ "medium": "medium",
+ "source": "source",
+ "term": "term",
+ }
+ },
+ "override": {"foo": "bar"},
+ "timeouts": 0,
+ }
+ },
+ "routing": {
+ "channels": ["string"],
+ "method": "all",
+ },
+ },
+ published=True,
+ )
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_replace(self, client: Courier) -> None:
+ response = client.tenants.templates.with_raw_response.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_replace(self, client: Courier) -> None:
+ with client.tenants.templates.with_streaming_response.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism 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 `tenant_id` but received ''"):
+ client.tenants.templates.with_raw_response.replace(
+ template_id="template_id",
+ tenant_id="",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.tenants.templates.with_raw_response.replace(
+ template_id="",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+
class TestAsyncTemplates:
parametrize = pytest.mark.parametrize(
@@ -231,3 +448,214 @@ async def test_path_params_list(self, async_client: AsyncCourier) -> None:
await async_client.tenants.templates.with_raw_response.list(
tenant_id="",
)
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_publish(self, async_client: AsyncCourier) -> None:
+ template = await async_client.tenants.templates.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_publish_with_all_params(self, async_client: AsyncCourier) -> None:
+ template = await async_client.tenants.templates.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ version="version",
+ )
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_publish(self, async_client: AsyncCourier) -> None:
+ response = await async_client.tenants.templates.with_raw_response.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_publish(self, async_client: AsyncCourier) -> None:
+ async with async_client.tenants.templates.with_streaming_response.publish(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(PostTenantTemplatePublishResponse, template, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_path_params_publish(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.templates.with_raw_response.publish(
+ template_id="template_id",
+ tenant_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.tenants.templates.with_raw_response.publish(
+ template_id="",
+ tenant_id="tenant_id",
+ )
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_replace(self, async_client: AsyncCourier) -> None:
+ template = await async_client.tenants.templates.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) -> None:
+ template = await async_client.tenants.templates.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [
+ {
+ "channels": ["string"],
+ "if": "if",
+ "loop": "loop",
+ "ref": "ref",
+ "type": "text",
+ }
+ ],
+ "version": "version",
+ "brand": "brand",
+ },
+ "channels": {
+ "foo": {
+ "brand_id": "brand_id",
+ "if": "if",
+ "metadata": {
+ "utm": {
+ "campaign": "campaign",
+ "content": "content",
+ "medium": "medium",
+ "source": "source",
+ "term": "term",
+ }
+ },
+ "override": {"foo": "bar"},
+ "providers": ["string"],
+ "routing_method": "all",
+ "timeouts": {
+ "channel": 0,
+ "provider": 0,
+ },
+ }
+ },
+ "providers": {
+ "foo": {
+ "if": "if",
+ "metadata": {
+ "utm": {
+ "campaign": "campaign",
+ "content": "content",
+ "medium": "medium",
+ "source": "source",
+ "term": "term",
+ }
+ },
+ "override": {"foo": "bar"},
+ "timeouts": 0,
+ }
+ },
+ "routing": {
+ "channels": ["string"],
+ "method": "all",
+ },
+ },
+ published=True,
+ )
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_replace(self, async_client: AsyncCourier) -> None:
+ response = await async_client.tenants.templates.with_raw_response.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_replace(self, async_client: AsyncCourier) -> None:
+ async with async_client.tenants.templates.with_streaming_response.replace(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(PutTenantTemplateResponse, template, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism 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 `tenant_id` but received ''"):
+ await async_client.tenants.templates.with_raw_response.replace(
+ template_id="template_id",
+ tenant_id="",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.tenants.templates.with_raw_response.replace(
+ template_id="",
+ tenant_id="tenant_id",
+ template={
+ "content": {
+ "elements": [{}],
+ "version": "version",
+ }
+ },
+ )
From 2d8b786fbee8f1f940208e3a00359ef549e2c4e1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 18:58:59 +0000
Subject: [PATCH 4/4] release: 7.8.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 10 ++++++++++
pyproject.toml | 2 +-
src/courier/_version.py | 2 +-
4 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 761cac3..6e39864 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "7.7.1"
+ ".": "7.8.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba6b28f..de6c40b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 7.8.0 (2026-02-06)
+
+Full Changelog: [v7.7.1...v7.8.0](https://github.com/trycourier/courier-python/compare/v7.7.1...v7.8.0)
+
+### Features
+
+* **api:** add publish/replace methods, versions resource to tenants.templates ([2b39781](https://github.com/trycourier/courier-python/commit/2b397816d696929cf5ad38b137b3c624f897ee26))
+* **api:** support list of recipients in send message ([76ccdfd](https://github.com/trycourier/courier-python/commit/76ccdfd9aee812c97cbad0d10b13b5746db4b24e))
+* **client:** add custom JSON encoder for extended type support ([b641cb6](https://github.com/trycourier/courier-python/commit/b641cb6819d5727a7ec7f1c3cf240a91a28ffd42))
+
## 7.7.1 (2026-01-27)
Full Changelog: [v7.7.0...v7.7.1](https://github.com/trycourier/courier-python/compare/v7.7.0...v7.7.1)
diff --git a/pyproject.toml b/pyproject.toml
index 93e608f..71b5355 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "trycourier"
-version = "7.7.1"
+version = "7.8.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 5d78273..b92a5b1 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.7.1" # x-release-please-version
+__version__ = "7.8.0" # x-release-please-version