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