diff --git a/.release-please-manifest.json b/.release-please-manifest.json index afe4ebf..821a1fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.22.0" + ".": "7.23.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index cf69b29..29ea7c7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 136 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-47bd7f89519b4aa68b039b9a03888b689fac9d6ee763d26be0b267bbb0b2ddaa.yml -openapi_spec_hash: 67ff63b23cbfb19beac58d6e471836d3 -config_hash: b30c4b3086cd9f3da06a63b256d8c189 +configured_endpoints: 147 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-ca32540df5bca42c8113de4004e7751b74bb440e3d73267e7cbad84dd3c3e82a.yml +openapi_spec_hash: 5576c8cf8e5be1f250b5dd1eb6d30571 +config_hash: c6cefb51165221f01d442a42ac4a13b9 diff --git a/CHANGELOG.md b/CHANGELOG.md index 468bcb9..112e207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 7.23.0 (2026-07-29) + +Full Changelog: [v7.22.0...v7.23.0](https://github.com/trycourier/courier-python/compare/v7.22.0...v7.23.0) + +### Features + +* **broadcasts:** document Broadcasts CRUD REST API ([#170](https://github.com/trycourier/courier-python/issues/170)) ([8e63975](https://github.com/trycourier/courier-python/commit/8e639757661144e35f43d31923a7d6190874253e)) + ## 7.22.0 (2026-07-29) Full Changelog: [v7.21.0...v7.22.0](https://github.com/trycourier/courier-python/compare/v7.21.0...v7.22.0) diff --git a/api.md b/api.md index 1783645..4603c9d 100644 --- a/api.md +++ b/api.md @@ -264,6 +264,36 @@ Methods: - client.journeys.templates.replace(notification_id, \*, template_id, \*\*params) -> JourneyTemplateGetResponse - client.journeys.templates.retrieve_content(notification_id, \*, template_id, \*\*params) -> NotificationContentGetResponse +# Broadcasts + +Types: + +```python +from courier.types import ( + Broadcast, + BroadcastListResponse, + BroadcastSchedule, + CreateBroadcastRequest, + ScheduleBroadcastRequest, + SendBroadcastRequest, + UpdateBroadcastRequest, +) +``` + +Methods: + +- client.broadcasts.create(\*\*params) -> Broadcast +- client.broadcasts.retrieve(broadcast_id) -> Broadcast +- client.broadcasts.update(broadcast_id, \*\*params) -> Broadcast +- client.broadcasts.list(\*\*params) -> BroadcastListResponse +- client.broadcasts.archive(broadcast_id) -> Broadcast +- client.broadcasts.cancel(broadcast_id) -> Broadcast +- client.broadcasts.duplicate(broadcast_id) -> Broadcast +- client.broadcasts.put_content(broadcast_id, \*\*params) -> NotificationContentMutationResponse +- client.broadcasts.retrieve_content(broadcast_id, \*\*params) -> NotificationContentGetResponse +- client.broadcasts.schedule(broadcast_id, \*\*params) -> Broadcast +- client.broadcasts.send(broadcast_id, \*\*params) -> Broadcast + # Brands Types: diff --git a/pyproject.toml b/pyproject.toml index 5573410..1b672bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "trycourier" -version = "7.22.0" +version = "7.23.0" description = "The official Python library for the Courier API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/courier/_client.py b/src/courier/_client.py index 1e024eb..b6797bf 100644 --- a/src/courier/_client.py +++ b/src/courier/_client.py @@ -51,6 +51,7 @@ requests, audiences, providers, + broadcasts, automations, audit_events, translations, @@ -65,6 +66,7 @@ from .resources.messages import MessagesResource, AsyncMessagesResource from .resources.requests import RequestsResource, AsyncRequestsResource from .resources.audiences import AudiencesResource, AsyncAudiencesResource + from .resources.broadcasts import BroadcastsResource, AsyncBroadcastsResource from .resources.inbox.inbox import InboxResource, AsyncInboxResource from .resources.lists.lists import ListsResource, AsyncListsResource from .resources.users.users import UsersResource, AsyncUsersResource @@ -211,6 +213,12 @@ def journeys(self) -> JourneysResource: return JourneysResource(self) + @cached_property + def broadcasts(self) -> BroadcastsResource: + from .resources.broadcasts import BroadcastsResource + + return BroadcastsResource(self) + @cached_property def brands(self) -> BrandsResource: """ @@ -566,6 +574,12 @@ def journeys(self) -> AsyncJourneysResource: return AsyncJourneysResource(self) + @cached_property + def broadcasts(self) -> AsyncBroadcastsResource: + from .resources.broadcasts import AsyncBroadcastsResource + + return AsyncBroadcastsResource(self) + @cached_property def brands(self) -> AsyncBrandsResource: """ @@ -863,6 +877,12 @@ def journeys(self) -> journeys.JourneysResourceWithRawResponse: return JourneysResourceWithRawResponse(self._client.journeys) + @cached_property + def broadcasts(self) -> broadcasts.BroadcastsResourceWithRawResponse: + from .resources.broadcasts import BroadcastsResourceWithRawResponse + + return BroadcastsResourceWithRawResponse(self._client.broadcasts) + @cached_property def brands(self) -> brands.BrandsResourceWithRawResponse: """ @@ -1048,6 +1068,12 @@ def journeys(self) -> journeys.AsyncJourneysResourceWithRawResponse: return AsyncJourneysResourceWithRawResponse(self._client.journeys) + @cached_property + def broadcasts(self) -> broadcasts.AsyncBroadcastsResourceWithRawResponse: + from .resources.broadcasts import AsyncBroadcastsResourceWithRawResponse + + return AsyncBroadcastsResourceWithRawResponse(self._client.broadcasts) + @cached_property def brands(self) -> brands.AsyncBrandsResourceWithRawResponse: """ @@ -1233,6 +1259,12 @@ def journeys(self) -> journeys.JourneysResourceWithStreamingResponse: return JourneysResourceWithStreamingResponse(self._client.journeys) + @cached_property + def broadcasts(self) -> broadcasts.BroadcastsResourceWithStreamingResponse: + from .resources.broadcasts import BroadcastsResourceWithStreamingResponse + + return BroadcastsResourceWithStreamingResponse(self._client.broadcasts) + @cached_property def brands(self) -> brands.BrandsResourceWithStreamingResponse: """ @@ -1418,6 +1450,12 @@ def journeys(self) -> journeys.AsyncJourneysResourceWithStreamingResponse: return AsyncJourneysResourceWithStreamingResponse(self._client.journeys) + @cached_property + def broadcasts(self) -> broadcasts.AsyncBroadcastsResourceWithStreamingResponse: + from .resources.broadcasts import AsyncBroadcastsResourceWithStreamingResponse + + return AsyncBroadcastsResourceWithStreamingResponse(self._client.broadcasts) + @cached_property def brands(self) -> brands.AsyncBrandsResourceWithStreamingResponse: """ diff --git a/src/courier/_version.py b/src/courier/_version.py index 6b18c05..3bba92e 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.22.0" # x-release-please-version +__version__ = "7.23.0" # x-release-please-version diff --git a/src/courier/resources/__init__.py b/src/courier/resources/__init__.py index 3c433ca..1528692 100644 --- a/src/courier/resources/__init__.py +++ b/src/courier/resources/__init__.py @@ -120,6 +120,14 @@ ProvidersResourceWithStreamingResponse, AsyncProvidersResourceWithStreamingResponse, ) +from .broadcasts import ( + BroadcastsResource, + AsyncBroadcastsResource, + BroadcastsResourceWithRawResponse, + AsyncBroadcastsResourceWithRawResponse, + BroadcastsResourceWithStreamingResponse, + AsyncBroadcastsResourceWithStreamingResponse, +) from .automations import ( AutomationsResource, AsyncAutomationsResource, @@ -212,6 +220,12 @@ "AsyncJourneysResourceWithRawResponse", "JourneysResourceWithStreamingResponse", "AsyncJourneysResourceWithStreamingResponse", + "BroadcastsResource", + "AsyncBroadcastsResource", + "BroadcastsResourceWithRawResponse", + "AsyncBroadcastsResourceWithRawResponse", + "BroadcastsResourceWithStreamingResponse", + "AsyncBroadcastsResourceWithStreamingResponse", "BrandsResource", "AsyncBrandsResource", "BrandsResourceWithRawResponse", diff --git a/src/courier/resources/broadcasts.py b/src/courier/resources/broadcasts.py new file mode 100644 index 0000000..0465e10 --- /dev/null +++ b/src/courier/resources/broadcasts.py @@ -0,0 +1,1175 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ..types import ( + NotificationTemplateState, + broadcast_list_params, + broadcast_send_params, + broadcast_create_params, + broadcast_update_params, + broadcast_schedule_params, + broadcast_put_content_params, + broadcast_retrieve_content_params, +) +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.broadcast import Broadcast +from ..types.broadcast_list_response import BroadcastListResponse +from ..types.notification_template_state import NotificationTemplateState +from ..types.notification_content_get_response import NotificationContentGetResponse +from ..types.notification_content_mutation_response import NotificationContentMutationResponse + +__all__ = ["BroadcastsResource", "AsyncBroadcastsResource"] + + +class BroadcastsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BroadcastsResourceWithRawResponse: + """ + 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 BroadcastsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BroadcastsResourceWithStreamingResponse: + """ + 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 BroadcastsResourceWithStreamingResponse(self) + + def create( + self, + *, + channel: Literal["email", "sms", "push", "inbox", "slack", "msteams"], + name: 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, + ) -> Broadcast: + """Create a broadcast. + + Provisions a private notification template for the broadcast + and returns the new broadcast in the draft state. Exactly one channel is + required. + + Args: + channel: The single delivery channel for this broadcast. + + name: Human-readable name. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/broadcasts", + body=maybe_transform( + { + "channel": channel, + "name": name, + }, + broadcast_create_params.BroadcastCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def retrieve( + self, + broadcast_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, + ) -> Broadcast: + """Retrieve a broadcast by ID. + + Archived broadcasts return 404. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._get( + path_template("/broadcasts/{broadcast_id}", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def update( + self, + broadcast_id: str, + *, + name: 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, + ) -> Broadcast: + """Update a broadcast's name. + + Content is edited via the broadcast's notification + template, not this endpoint. + + Args: + name: New human-readable name. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._put( + path_template("/broadcasts/{broadcast_id}", broadcast_id=broadcast_id), + body=maybe_transform({"name": name}, broadcast_update_params.BroadcastUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def list( + self, + *, + cursor: Optional[str] | Omit = omit, + limit: 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, + ) -> BroadcastListResponse: + """List broadcasts in your workspace. + + Cursor-paginated; returns broadcasts + newest-first. + + Args: + cursor: Opaque pagination cursor from a previous response. Omit for the first page. + + limit: Maximum number of results per page. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/broadcasts", + 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, + }, + broadcast_list_params.BroadcastListParams, + ), + ), + cast_to=BroadcastListResponse, + ) + + def archive( + self, + broadcast_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, + ) -> Broadcast: + """Archive a broadcast. + + This is a soft delete — the archived broadcast is returned + and no longer appears in list results. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._delete( + path_template("/broadcasts/{broadcast_id}", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def cancel( + self, + broadcast_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, + ) -> Broadcast: + """Cancel a broadcast's pending schedule, returning it to the draft state. + + Only + valid for a scheduled broadcast. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._post( + path_template("/broadcasts/{broadcast_id}/cancel", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def duplicate( + self, + broadcast_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, + ) -> Broadcast: + """ + Duplicate a broadcast (and its template) into a new draft named "{source name} + (copy)". + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._post( + path_template("/broadcasts/{broadcast_id}/duplicate", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def put_content( + self, + broadcast_id: str, + *, + content: broadcast_put_content_params.Content, + state: NotificationTemplateState | 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, + ) -> NotificationContentMutationResponse: + """ + Author the broadcast's content by replacing the draft elemental content of its + private notification template. The draft is published automatically when the + broadcast is sent or scheduled. + + Args: + content: Elemental content payload. The server defaults `version` when omitted. + + state: Template state. Defaults to `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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._put( + path_template("/broadcasts/{broadcast_id}/content", broadcast_id=broadcast_id), + body=maybe_transform( + { + "content": content, + "state": state, + }, + broadcast_put_content_params.BroadcastPutContentParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NotificationContentMutationResponse, + ) + + def retrieve_content( + self, + broadcast_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, + ) -> NotificationContentGetResponse: + """ + Retrieve the broadcast's content — the elemental content of its private + notification template. Defaults to the working draft, since broadcast content is + authored as a draft until the broadcast is sent. + + Args: + version: Accepts `draft`, `published`, or a version string (e.g. `v001`). Defaults to + `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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._get( + path_template("/broadcasts/{broadcast_id}/content", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"version": version}, broadcast_retrieve_content_params.BroadcastRetrieveContentParams + ), + ), + cast_to=NotificationContentGetResponse, + ) + + def schedule( + self, + broadcast_id: str, + *, + recipient_id: str, + recipient_type: Literal["list", "audience"], + scheduled_to: str, + timezone: 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, + ) -> Broadcast: + """Schedule a broadcast for a future send to a list or audience. + + Publishes the + broadcast template first. Not allowed once the broadcast is sending or sent. For + an immediate send use POST /broadcasts/{broadcastId}/send. + + Args: + recipient_id: ID of the target list or audience. + + recipient_type: Whether the broadcast targets a list or an audience. + + scheduled_to: Wall-clock timestamp of the future send, no timezone offset (e.g. + "2026-07-21T20:00:00"). The zone is given by `timezone`. + + timezone: IANA timezone for the scheduled send (e.g. America/New_York). + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._post( + path_template("/broadcasts/{broadcast_id}/schedule", broadcast_id=broadcast_id), + body=maybe_transform( + { + "recipient_id": recipient_id, + "recipient_type": recipient_type, + "scheduled_to": scheduled_to, + "timezone": timezone, + }, + broadcast_schedule_params.BroadcastScheduleParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + def send( + self, + broadcast_id: str, + *, + recipient_id: str, + recipient_type: Literal["list", "audience"], + # 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, + ) -> Broadcast: + """Send a broadcast immediately to a list or audience. + + Publishes the broadcast + template first. Not allowed once the broadcast is sending or sent. + + Args: + recipient_id: ID of the target list or audience. + + recipient_type: Whether the broadcast targets a list or an audience. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return self._post( + path_template("/broadcasts/{broadcast_id}/send", broadcast_id=broadcast_id), + body=maybe_transform( + { + "recipient_id": recipient_id, + "recipient_type": recipient_type, + }, + broadcast_send_params.BroadcastSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + +class AsyncBroadcastsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBroadcastsResourceWithRawResponse: + """ + 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 AsyncBroadcastsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBroadcastsResourceWithStreamingResponse: + """ + 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 AsyncBroadcastsResourceWithStreamingResponse(self) + + async def create( + self, + *, + channel: Literal["email", "sms", "push", "inbox", "slack", "msteams"], + name: 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, + ) -> Broadcast: + """Create a broadcast. + + Provisions a private notification template for the broadcast + and returns the new broadcast in the draft state. Exactly one channel is + required. + + Args: + channel: The single delivery channel for this broadcast. + + name: Human-readable name. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/broadcasts", + body=await async_maybe_transform( + { + "channel": channel, + "name": name, + }, + broadcast_create_params.BroadcastCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def retrieve( + self, + broadcast_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, + ) -> Broadcast: + """Retrieve a broadcast by ID. + + Archived broadcasts return 404. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._get( + path_template("/broadcasts/{broadcast_id}", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def update( + self, + broadcast_id: str, + *, + name: 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, + ) -> Broadcast: + """Update a broadcast's name. + + Content is edited via the broadcast's notification + template, not this endpoint. + + Args: + name: New human-readable name. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._put( + path_template("/broadcasts/{broadcast_id}", broadcast_id=broadcast_id), + body=await async_maybe_transform({"name": name}, broadcast_update_params.BroadcastUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def list( + self, + *, + cursor: Optional[str] | Omit = omit, + limit: 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, + ) -> BroadcastListResponse: + """List broadcasts in your workspace. + + Cursor-paginated; returns broadcasts + newest-first. + + Args: + cursor: Opaque pagination cursor from a previous response. Omit for the first page. + + limit: Maximum number of results per page. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/broadcasts", + 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, + }, + broadcast_list_params.BroadcastListParams, + ), + ), + cast_to=BroadcastListResponse, + ) + + async def archive( + self, + broadcast_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, + ) -> Broadcast: + """Archive a broadcast. + + This is a soft delete — the archived broadcast is returned + and no longer appears in list results. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._delete( + path_template("/broadcasts/{broadcast_id}", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def cancel( + self, + broadcast_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, + ) -> Broadcast: + """Cancel a broadcast's pending schedule, returning it to the draft state. + + Only + valid for a scheduled broadcast. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._post( + path_template("/broadcasts/{broadcast_id}/cancel", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def duplicate( + self, + broadcast_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, + ) -> Broadcast: + """ + Duplicate a broadcast (and its template) into a new draft named "{source name} + (copy)". + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._post( + path_template("/broadcasts/{broadcast_id}/duplicate", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def put_content( + self, + broadcast_id: str, + *, + content: broadcast_put_content_params.Content, + state: NotificationTemplateState | 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, + ) -> NotificationContentMutationResponse: + """ + Author the broadcast's content by replacing the draft elemental content of its + private notification template. The draft is published automatically when the + broadcast is sent or scheduled. + + Args: + content: Elemental content payload. The server defaults `version` when omitted. + + state: Template state. Defaults to `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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._put( + path_template("/broadcasts/{broadcast_id}/content", broadcast_id=broadcast_id), + body=await async_maybe_transform( + { + "content": content, + "state": state, + }, + broadcast_put_content_params.BroadcastPutContentParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NotificationContentMutationResponse, + ) + + async def retrieve_content( + self, + broadcast_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, + ) -> NotificationContentGetResponse: + """ + Retrieve the broadcast's content — the elemental content of its private + notification template. Defaults to the working draft, since broadcast content is + authored as a draft until the broadcast is sent. + + Args: + version: Accepts `draft`, `published`, or a version string (e.g. `v001`). Defaults to + `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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._get( + path_template("/broadcasts/{broadcast_id}/content", broadcast_id=broadcast_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"version": version}, broadcast_retrieve_content_params.BroadcastRetrieveContentParams + ), + ), + cast_to=NotificationContentGetResponse, + ) + + async def schedule( + self, + broadcast_id: str, + *, + recipient_id: str, + recipient_type: Literal["list", "audience"], + scheduled_to: str, + timezone: 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, + ) -> Broadcast: + """Schedule a broadcast for a future send to a list or audience. + + Publishes the + broadcast template first. Not allowed once the broadcast is sending or sent. For + an immediate send use POST /broadcasts/{broadcastId}/send. + + Args: + recipient_id: ID of the target list or audience. + + recipient_type: Whether the broadcast targets a list or an audience. + + scheduled_to: Wall-clock timestamp of the future send, no timezone offset (e.g. + "2026-07-21T20:00:00"). The zone is given by `timezone`. + + timezone: IANA timezone for the scheduled send (e.g. America/New_York). + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._post( + path_template("/broadcasts/{broadcast_id}/schedule", broadcast_id=broadcast_id), + body=await async_maybe_transform( + { + "recipient_id": recipient_id, + "recipient_type": recipient_type, + "scheduled_to": scheduled_to, + "timezone": timezone, + }, + broadcast_schedule_params.BroadcastScheduleParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + async def send( + self, + broadcast_id: str, + *, + recipient_id: str, + recipient_type: Literal["list", "audience"], + # 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, + ) -> Broadcast: + """Send a broadcast immediately to a list or audience. + + Publishes the broadcast + template first. Not allowed once the broadcast is sending or sent. + + Args: + recipient_id: ID of the target list or audience. + + recipient_type: Whether the broadcast targets a list or an audience. + + 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 broadcast_id: + raise ValueError(f"Expected a non-empty value for `broadcast_id` but received {broadcast_id!r}") + return await self._post( + path_template("/broadcasts/{broadcast_id}/send", broadcast_id=broadcast_id), + body=await async_maybe_transform( + { + "recipient_id": recipient_id, + "recipient_type": recipient_type, + }, + broadcast_send_params.BroadcastSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Broadcast, + ) + + +class BroadcastsResourceWithRawResponse: + def __init__(self, broadcasts: BroadcastsResource) -> None: + self._broadcasts = broadcasts + + self.create = to_raw_response_wrapper( + broadcasts.create, + ) + self.retrieve = to_raw_response_wrapper( + broadcasts.retrieve, + ) + self.update = to_raw_response_wrapper( + broadcasts.update, + ) + self.list = to_raw_response_wrapper( + broadcasts.list, + ) + self.archive = to_raw_response_wrapper( + broadcasts.archive, + ) + self.cancel = to_raw_response_wrapper( + broadcasts.cancel, + ) + self.duplicate = to_raw_response_wrapper( + broadcasts.duplicate, + ) + self.put_content = to_raw_response_wrapper( + broadcasts.put_content, + ) + self.retrieve_content = to_raw_response_wrapper( + broadcasts.retrieve_content, + ) + self.schedule = to_raw_response_wrapper( + broadcasts.schedule, + ) + self.send = to_raw_response_wrapper( + broadcasts.send, + ) + + +class AsyncBroadcastsResourceWithRawResponse: + def __init__(self, broadcasts: AsyncBroadcastsResource) -> None: + self._broadcasts = broadcasts + + self.create = async_to_raw_response_wrapper( + broadcasts.create, + ) + self.retrieve = async_to_raw_response_wrapper( + broadcasts.retrieve, + ) + self.update = async_to_raw_response_wrapper( + broadcasts.update, + ) + self.list = async_to_raw_response_wrapper( + broadcasts.list, + ) + self.archive = async_to_raw_response_wrapper( + broadcasts.archive, + ) + self.cancel = async_to_raw_response_wrapper( + broadcasts.cancel, + ) + self.duplicate = async_to_raw_response_wrapper( + broadcasts.duplicate, + ) + self.put_content = async_to_raw_response_wrapper( + broadcasts.put_content, + ) + self.retrieve_content = async_to_raw_response_wrapper( + broadcasts.retrieve_content, + ) + self.schedule = async_to_raw_response_wrapper( + broadcasts.schedule, + ) + self.send = async_to_raw_response_wrapper( + broadcasts.send, + ) + + +class BroadcastsResourceWithStreamingResponse: + def __init__(self, broadcasts: BroadcastsResource) -> None: + self._broadcasts = broadcasts + + self.create = to_streamed_response_wrapper( + broadcasts.create, + ) + self.retrieve = to_streamed_response_wrapper( + broadcasts.retrieve, + ) + self.update = to_streamed_response_wrapper( + broadcasts.update, + ) + self.list = to_streamed_response_wrapper( + broadcasts.list, + ) + self.archive = to_streamed_response_wrapper( + broadcasts.archive, + ) + self.cancel = to_streamed_response_wrapper( + broadcasts.cancel, + ) + self.duplicate = to_streamed_response_wrapper( + broadcasts.duplicate, + ) + self.put_content = to_streamed_response_wrapper( + broadcasts.put_content, + ) + self.retrieve_content = to_streamed_response_wrapper( + broadcasts.retrieve_content, + ) + self.schedule = to_streamed_response_wrapper( + broadcasts.schedule, + ) + self.send = to_streamed_response_wrapper( + broadcasts.send, + ) + + +class AsyncBroadcastsResourceWithStreamingResponse: + def __init__(self, broadcasts: AsyncBroadcastsResource) -> None: + self._broadcasts = broadcasts + + self.create = async_to_streamed_response_wrapper( + broadcasts.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + broadcasts.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + broadcasts.update, + ) + self.list = async_to_streamed_response_wrapper( + broadcasts.list, + ) + self.archive = async_to_streamed_response_wrapper( + broadcasts.archive, + ) + self.cancel = async_to_streamed_response_wrapper( + broadcasts.cancel, + ) + self.duplicate = async_to_streamed_response_wrapper( + broadcasts.duplicate, + ) + self.put_content = async_to_streamed_response_wrapper( + broadcasts.put_content, + ) + self.retrieve_content = async_to_streamed_response_wrapper( + broadcasts.retrieve_content, + ) + self.schedule = async_to_streamed_response_wrapper( + broadcasts.schedule, + ) + self.send = async_to_streamed_response_wrapper( + broadcasts.send, + ) diff --git a/src/courier/types/__init__.py b/src/courier/types/__init__.py index b3c63f2..938f123 100644 --- a/src/courier/types/__init__.py +++ b/src/courier/types/__init__.py @@ -102,6 +102,7 @@ from .journey import Journey as Journey from .audience import Audience as Audience from .provider import Provider as Provider +from .broadcast import Broadcast as Broadcast from .base_check import BaseCheck as BaseCheck from .email_head import EmailHead as EmailHead from .logo_param import LogoParam as LogoParam @@ -131,6 +132,7 @@ from .subscription_list import SubscriptionList as SubscriptionList from .widget_background import WidgetBackground as WidgetBackground from .brand_colors_param import BrandColorsParam as BrandColorsParam +from .broadcast_schedule import BroadcastSchedule as BroadcastSchedule from .email_footer_param import EmailFooterParam as EmailFooterParam from .email_header_param import EmailHeaderParam as EmailHeaderParam from .journey_experiment import JourneyExperiment as JourneyExperiment @@ -158,6 +160,8 @@ from .tenant_list_response import TenantListResponse as TenantListResponse from .tenant_update_params import TenantUpdateParams as TenantUpdateParams from .brand_settings_in_app import BrandSettingsInApp as BrandSettingsInApp +from .broadcast_list_params import BroadcastListParams as BroadcastListParams +from .broadcast_send_params import BroadcastSendParams as BroadcastSendParams from .journey_ai_node_param import JourneyAINodeParam as JourneyAINodeParam from .journey_cancel_params import JourneyCancelParams as JourneyCancelParams from .journey_create_params import JourneyCreateParams as JourneyCreateParams @@ -183,6 +187,9 @@ from .subscription_topic_new import SubscriptionTopicNew as SubscriptionTopicNew from .audit_event_list_params import AuditEventListParams as AuditEventListParams from .auth_issue_token_params import AuthIssueTokenParams as AuthIssueTokenParams +from .broadcast_create_params import BroadcastCreateParams as BroadcastCreateParams +from .broadcast_list_response import BroadcastListResponse as BroadcastListResponse +from .broadcast_update_params import BroadcastUpdateParams as BroadcastUpdateParams from .cancel_journey_response import CancelJourneyResponse as CancelJourneyResponse from .journey_condition_group import JourneyConditionGroup as JourneyConditionGroup from .journey_exit_node_param import JourneyExitNodeParam as JourneyExitNodeParam @@ -208,6 +215,7 @@ from .tenant_list_users_params import TenantListUsersParams as TenantListUsersParams from .audit_event_list_response import AuditEventListResponse as AuditEventListResponse from .auth_issue_token_response import AuthIssueTokenResponse as AuthIssueTokenResponse +from .broadcast_schedule_params import BroadcastScheduleParams as BroadcastScheduleParams from .default_preferences_param import DefaultPreferencesParam as DefaultPreferencesParam from .message_retrieve_response import MessageRetrieveResponse as MessageRetrieveResponse from .profile_retrieve_response import ProfileRetrieveResponse as ProfileRetrieveResponse @@ -227,6 +235,7 @@ from .notification_template_state import NotificationTemplateState as NotificationTemplateState from .tenant_template_input_param import TenantTemplateInputParam as TenantTemplateInputParam from .audience_list_members_params import AudienceListMembersParams as AudienceListMembersParams +from .broadcast_put_content_params import BroadcastPutContentParams as BroadcastPutContentParams from .cancel_journey_request_param import CancelJourneyRequestParam as CancelJourneyRequestParam from .inbound_track_event_response import InboundTrackEventResponse as InboundTrackEventResponse from .journey_condition_atom_param import JourneyConditionAtomParam as JourneyConditionAtomParam @@ -263,6 +272,7 @@ from .base_template_tenant_association import BaseTemplateTenantAssociation as BaseTemplateTenantAssociation from .journey_experiment_variant_param import JourneyExperimentVariantParam as JourneyExperimentVariantParam from .automation_template_list_response import AutomationTemplateListResponse as AutomationTemplateListResponse +from .broadcast_retrieve_content_params import BroadcastRetrieveContentParams as BroadcastRetrieveContentParams from .journey_delay_duration_node_param import JourneyDelayDurationNodeParam as JourneyDelayDurationNodeParam from .journey_fetch_post_put_node_param import JourneyFetchPostPutNodeParam as JourneyFetchPostPutNodeParam from .notification_content_get_response import NotificationContentGetResponse as NotificationContentGetResponse diff --git a/src/courier/types/broadcast.py b/src/courier/types/broadcast.py new file mode 100644 index 0000000..b736a13 --- /dev/null +++ b/src/courier/types/broadcast.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .broadcast_schedule import BroadcastSchedule + +__all__ = ["Broadcast"] + + +class Broadcast(BaseModel): + """ + A broadcast — a single-channel message delivered to a known set of recipients (a list or audience). + """ + + id: str + """The broadcast ID (bst\\__ prefix).""" + + channel: Literal["email", "sms", "push", "inbox", "slack", "msteams"] + """The broadcast's delivery channel.""" + + created_at: str + """ISO 8601 timestamp when the broadcast was created.""" + + created_by: str + """Actor that created the broadcast.""" + + name: str + """Human-readable name.""" + + status: Literal["draft", "scheduled", "sending", "sent"] + """Lifecycle status of the broadcast.""" + + updated_at: str + """ISO 8601 timestamp of the last update.""" + + updated_by: str + """Actor that last updated the broadcast.""" + + archived_at: Optional[str] = None + """ISO 8601 timestamp when the broadcast was archived, if archived.""" + + archived_by: Optional[str] = None + """Actor that archived the broadcast, if archived.""" + + schedule: Optional[BroadcastSchedule] = None + """The delivery schedule and recipient targeting for a broadcast.""" diff --git a/src/courier/types/broadcast_create_params.py b/src/courier/types/broadcast_create_params.py new file mode 100644 index 0000000..330ab8b --- /dev/null +++ b/src/courier/types/broadcast_create_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["BroadcastCreateParams"] + + +class BroadcastCreateParams(TypedDict, total=False): + channel: Required[Literal["email", "sms", "push", "inbox", "slack", "msteams"]] + """The single delivery channel for this broadcast.""" + + name: Required[str] + """Human-readable name.""" diff --git a/src/courier/types/broadcast_list_params.py b/src/courier/types/broadcast_list_params.py new file mode 100644 index 0000000..9d89f6c --- /dev/null +++ b/src/courier/types/broadcast_list_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["BroadcastListParams"] + + +class BroadcastListParams(TypedDict, total=False): + cursor: Optional[str] + """Opaque pagination cursor from a previous response. Omit for the first page.""" + + limit: int + """Maximum number of results per page.""" diff --git a/src/courier/types/broadcast_list_response.py b/src/courier/types/broadcast_list_response.py new file mode 100644 index 0000000..acaa032 --- /dev/null +++ b/src/courier/types/broadcast_list_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .broadcast import Broadcast +from .shared.paging import Paging + +__all__ = ["BroadcastListResponse"] + + +class BroadcastListResponse(BaseModel): + """Paginated list of broadcasts.""" + + paging: Paging + + results: List[Broadcast] diff --git a/src/courier/types/broadcast_put_content_params.py b/src/courier/types/broadcast_put_content_params.py new file mode 100644 index 0000000..8825088 --- /dev/null +++ b/src/courier/types/broadcast_put_content_params.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, TypedDict + +from .notification_template_state import NotificationTemplateState +from .shared_params.elemental_node import ElementalNode + +__all__ = ["BroadcastPutContentParams", "Content"] + + +class BroadcastPutContentParams(TypedDict, total=False): + content: Required[Content] + """Elemental content payload. The server defaults `version` when omitted.""" + + state: NotificationTemplateState + """Template state. Defaults to `DRAFT`.""" + + +class Content(TypedDict, total=False): + """Elemental content payload. The server defaults `version` when omitted.""" + + elements: Required[Iterable[ElementalNode]] + + version: str + """Content version identifier (e.g., `2022-01-01`). + + Optional; server defaults when omitted. + """ diff --git a/src/courier/types/broadcast_retrieve_content_params.py b/src/courier/types/broadcast_retrieve_content_params.py new file mode 100644 index 0000000..c3192a4 --- /dev/null +++ b/src/courier/types/broadcast_retrieve_content_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BroadcastRetrieveContentParams"] + + +class BroadcastRetrieveContentParams(TypedDict, total=False): + version: str + """Accepts `draft`, `published`, or a version string (e.g. + + `v001`). Defaults to `draft`. + """ diff --git a/src/courier/types/broadcast_schedule.py b/src/courier/types/broadcast_schedule.py new file mode 100644 index 0000000..f907320 --- /dev/null +++ b/src/courier/types/broadcast_schedule.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["BroadcastSchedule"] + + +class BroadcastSchedule(BaseModel): + """The delivery schedule and recipient targeting for a broadcast.""" + + recipient_id: str + """ID of the target list or audience.""" + + recipient_type: Literal["list", "audience"] + """Whether the broadcast targets a list or an audience.""" + + scheduled_to: Optional[str] = None + """Wall-clock timestamp of the scheduled send, no timezone offset (e.g. + + "2026-07-21T20:00:00"). + """ + + timezone: Optional[str] = None + """IANA timezone for the scheduled send (e.g. America/New_York).""" diff --git a/src/courier/types/broadcast_schedule_params.py b/src/courier/types/broadcast_schedule_params.py new file mode 100644 index 0000000..4cc7f3e --- /dev/null +++ b/src/courier/types/broadcast_schedule_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 Literal, Required, TypedDict + +__all__ = ["BroadcastScheduleParams"] + + +class BroadcastScheduleParams(TypedDict, total=False): + recipient_id: Required[str] + """ID of the target list or audience.""" + + recipient_type: Required[Literal["list", "audience"]] + """Whether the broadcast targets a list or an audience.""" + + scheduled_to: Required[str] + """Wall-clock timestamp of the future send, no timezone offset (e.g. + + "2026-07-21T20:00:00"). The zone is given by `timezone`. + """ + + timezone: str + """IANA timezone for the scheduled send (e.g. America/New_York).""" diff --git a/src/courier/types/broadcast_send_params.py b/src/courier/types/broadcast_send_params.py new file mode 100644 index 0000000..07fa67b --- /dev/null +++ b/src/courier/types/broadcast_send_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["BroadcastSendParams"] + + +class BroadcastSendParams(TypedDict, total=False): + recipient_id: Required[str] + """ID of the target list or audience.""" + + recipient_type: Required[Literal["list", "audience"]] + """Whether the broadcast targets a list or an audience.""" diff --git a/src/courier/types/broadcast_update_params.py b/src/courier/types/broadcast_update_params.py new file mode 100644 index 0000000..7d4da50 --- /dev/null +++ b/src/courier/types/broadcast_update_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BroadcastUpdateParams"] + + +class BroadcastUpdateParams(TypedDict, total=False): + name: Required[str] + """New human-readable name.""" diff --git a/tests/api_resources/test_broadcasts.py b/tests/api_resources/test_broadcasts.py new file mode 100644 index 0000000..cc57f88 --- /dev/null +++ b/tests/api_resources/test_broadcasts.py @@ -0,0 +1,1087 @@ +# 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 ( + Broadcast, + BroadcastListResponse, + NotificationContentGetResponse, + NotificationContentMutationResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBroadcasts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Courier) -> None: + broadcast = client.broadcasts.create( + channel="email", + name="Spring Sale Announcement", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.create( + channel="email", + name="Spring Sale Announcement", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.create( + channel="email", + name="Spring Sale Announcement", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Courier) -> None: + broadcast = client.broadcasts.retrieve( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.retrieve( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.retrieve( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Courier) -> None: + broadcast = client.broadcasts.update( + broadcast_id="broadcastId", + name="Spring Sale Announcement (v2)", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.update( + broadcast_id="broadcastId", + name="Spring Sale Announcement (v2)", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.update( + broadcast_id="broadcastId", + name="Spring Sale Announcement (v2)", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.update( + broadcast_id="", + name="Spring Sale Announcement (v2)", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Courier) -> None: + broadcast = client.broadcasts.list() + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Courier) -> None: + broadcast = client.broadcasts.list( + cursor="cursor", + limit=1, + ) + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_archive(self, client: Courier) -> None: + broadcast = client.broadcasts.archive( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_archive(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.archive( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_archive(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.archive( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_archive(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.archive( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_cancel(self, client: Courier) -> None: + broadcast = client.broadcasts.cancel( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_cancel(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.cancel( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_cancel(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.cancel( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_cancel(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.cancel( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_duplicate(self, client: Courier) -> None: + broadcast = client.broadcasts.duplicate( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_duplicate(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.duplicate( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_duplicate(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.duplicate( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_duplicate(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.duplicate( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_put_content(self, client: Courier) -> None: + broadcast = client.broadcasts.put_content( + broadcast_id="broadcastId", + content={"elements": [{}, {}]}, + ) + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_put_content_with_all_params(self, client: Courier) -> None: + broadcast = client.broadcasts.put_content( + broadcast_id="broadcastId", + content={ + "elements": [ + { + "channels": ["string"], + "if": "if", + "loop": "loop", + "ref": "ref", + "type": "meta", + }, + { + "channels": ["string"], + "if": "if", + "loop": "loop", + "ref": "ref", + "type": "text", + }, + ], + "version": "2022-01-01", + }, + state="DRAFT", + ) + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_put_content(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.put_content( + broadcast_id="broadcastId", + content={"elements": [{}, {}]}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_put_content(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.put_content( + broadcast_id="broadcastId", + content={"elements": [{}, {}]}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_put_content(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.put_content( + broadcast_id="", + content={"elements": [{}, {}]}, + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_content(self, client: Courier) -> None: + broadcast = client.broadcasts.retrieve_content( + broadcast_id="broadcastId", + ) + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_content_with_all_params(self, client: Courier) -> None: + broadcast = client.broadcasts.retrieve_content( + broadcast_id="broadcastId", + version="version", + ) + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve_content(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.retrieve_content( + broadcast_id="broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve_content(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.retrieve_content( + broadcast_id="broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve_content(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.retrieve_content( + broadcast_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_schedule(self, client: Courier) -> None: + broadcast = client.broadcasts.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_schedule_with_all_params(self, client: Courier) -> None: + broadcast = client.broadcasts.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + timezone="America/New_York", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_schedule(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_schedule(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_schedule(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.schedule( + broadcast_id="", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_send(self, client: Courier) -> None: + broadcast = client.broadcasts.send( + broadcast_id="broadcastId", + recipient_id="cool-customers", + recipient_type="list", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_send(self, client: Courier) -> None: + response = client.broadcasts.with_raw_response.send( + broadcast_id="broadcastId", + recipient_id="cool-customers", + recipient_type="list", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_send(self, client: Courier) -> None: + with client.broadcasts.with_streaming_response.send( + broadcast_id="broadcastId", + recipient_id="cool-customers", + recipient_type="list", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_send(self, client: Courier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + client.broadcasts.with_raw_response.send( + broadcast_id="", + recipient_id="cool-customers", + recipient_type="list", + ) + + +class TestAsyncBroadcasts: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.create( + channel="email", + name="Spring Sale Announcement", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.create( + channel="email", + name="Spring Sale Announcement", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.create( + channel="email", + name="Spring Sale Announcement", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.retrieve( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.retrieve( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.retrieve( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.update( + broadcast_id="broadcastId", + name="Spring Sale Announcement (v2)", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.update( + broadcast_id="broadcastId", + name="Spring Sale Announcement (v2)", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.update( + broadcast_id="broadcastId", + name="Spring Sale Announcement (v2)", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.update( + broadcast_id="", + name="Spring Sale Announcement (v2)", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.list() + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.list( + cursor="cursor", + limit=1, + ) + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(BroadcastListResponse, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_archive(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.archive( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_archive(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.archive( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_archive(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.archive( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_archive(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.archive( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_cancel(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.cancel( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_cancel(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.cancel( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_cancel(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.cancel( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_cancel(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.cancel( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_duplicate(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.duplicate( + "broadcastId", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_duplicate(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.duplicate( + "broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_duplicate(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.duplicate( + "broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_duplicate(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.duplicate( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_put_content(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.put_content( + broadcast_id="broadcastId", + content={"elements": [{}, {}]}, + ) + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_put_content_with_all_params(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.put_content( + broadcast_id="broadcastId", + content={ + "elements": [ + { + "channels": ["string"], + "if": "if", + "loop": "loop", + "ref": "ref", + "type": "meta", + }, + { + "channels": ["string"], + "if": "if", + "loop": "loop", + "ref": "ref", + "type": "text", + }, + ], + "version": "2022-01-01", + }, + state="DRAFT", + ) + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_put_content(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.put_content( + broadcast_id="broadcastId", + content={"elements": [{}, {}]}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_put_content(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.put_content( + broadcast_id="broadcastId", + content={"elements": [{}, {}]}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(NotificationContentMutationResponse, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_put_content(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.put_content( + broadcast_id="", + content={"elements": [{}, {}]}, + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_content(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.retrieve_content( + broadcast_id="broadcastId", + ) + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_content_with_all_params(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.retrieve_content( + broadcast_id="broadcastId", + version="version", + ) + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve_content(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.retrieve_content( + broadcast_id="broadcastId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve_content(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.retrieve_content( + broadcast_id="broadcastId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(NotificationContentGetResponse, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve_content(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.retrieve_content( + broadcast_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_schedule(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_schedule_with_all_params(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + timezone="America/New_York", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_schedule(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_schedule(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.schedule( + broadcast_id="broadcastId", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_schedule(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.schedule( + broadcast_id="", + recipient_id="aud_01kx4h2jdafq8bk9amzvy6hbv0", + recipient_type="audience", + scheduled_to="2026-08-01T15:00:00", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_send(self, async_client: AsyncCourier) -> None: + broadcast = await async_client.broadcasts.send( + broadcast_id="broadcastId", + recipient_id="cool-customers", + recipient_type="list", + ) + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_send(self, async_client: AsyncCourier) -> None: + response = await async_client.broadcasts.with_raw_response.send( + broadcast_id="broadcastId", + recipient_id="cool-customers", + recipient_type="list", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_send(self, async_client: AsyncCourier) -> None: + async with async_client.broadcasts.with_streaming_response.send( + broadcast_id="broadcastId", + recipient_id="cool-customers", + recipient_type="list", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + broadcast = await response.parse() + assert_matches_type(Broadcast, broadcast, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_send(self, async_client: AsyncCourier) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `broadcast_id` but received ''"): + await async_client.broadcasts.with_raw_response.send( + broadcast_id="", + recipient_id="cool-customers", + recipient_type="list", + )