diff --git a/.stats.yml b/.stats.yml
index 5fe5772a..ddcc51fc 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 158
+configured_endpoints: 167
diff --git a/api.md b/api.md
index 3d159d2d..146ec111 100644
--- a/api.md
+++ b/api.md
@@ -337,6 +337,29 @@ Methods:
- client.broadcasts.schedule(broadcast_id, \*\*params) -> Broadcast
- client.broadcasts.send(broadcast_id, \*\*params) -> Broadcast
+# Previews
+
+Types:
+
+```python
+from courier.types import (
+ CreateDeviceSetRequest,
+ DeviceSet,
+ DeviceSetListResponse,
+ PreviewDevice,
+ PreviewDeviceListResponse,
+)
+```
+
+Methods:
+
+- client.previews.archive_device_set(device_set_id) -> DeviceSet
+- client.previews.create_device_set(\*\*params) -> DeviceSet
+- client.previews.list_device_sets() -> DeviceSetListResponse
+- client.previews.list_devices() -> PreviewDeviceListResponse
+- client.previews.retrieve_device_set(device_set_id) -> DeviceSet
+- client.previews.update_device_set(device_set_id, \*\*params) -> DeviceSet
+
# Bulk
Types:
@@ -548,6 +571,32 @@ Methods:
- client.notifications.checks.list(submission_id, \*, id) -> CheckListResponse
- client.notifications.checks.delete(submission_id, \*, id) -> None
+## Previews
+
+### Runs
+
+Types:
+
+```python
+from courier.types.notifications.previews import (
+ CreatePreviewRunRequest,
+ PreviewResult,
+ PreviewResultFailureReason,
+ PreviewResultStatus,
+ PreviewRun,
+ PreviewRunDetail,
+ PreviewRunFailureReason,
+ PreviewRunListResponse,
+ PreviewRunStatus,
+)
+```
+
+Methods:
+
+- client.notifications.previews.runs.create(id, \*\*params) -> PreviewRun
+- client.notifications.previews.runs.retrieve(preview_run_id, \*, id) -> PreviewRunDetail
+- client.notifications.previews.runs.list(id, \*\*params) -> PreviewRunListResponse
+
# RoutingStrategies
Types:
diff --git a/pyproject.toml b/pyproject.toml
index e913bbac..3fb4fc0d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "trycourier"
-version = "9.10.0"
+version = "9.9.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 5585885f..b6937133 100644
--- a/src/courier/_client.py
+++ b/src/courier/_client.py
@@ -47,6 +47,7 @@
tenants,
journeys,
messages,
+ previews,
profiles,
requests,
audiences,
@@ -65,6 +66,7 @@
from .resources.brands import BrandsResource, AsyncBrandsResource
from .resources.inbound import InboundResource, AsyncInboundResource
from .resources.messages import MessagesResource, AsyncMessagesResource
+ from .resources.previews import PreviewsResource, AsyncPreviewsResource
from .resources.requests import RequestsResource, AsyncRequestsResource
from .resources.audiences import AudiencesResource, AsyncAudiencesResource
from .resources.broadcasts import BroadcastsResource, AsyncBroadcastsResource
@@ -222,6 +224,15 @@ def broadcasts(self) -> BroadcastsResource:
return BroadcastsResource(self)
+ @cached_property
+ def previews(self) -> PreviewsResource:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ from .resources.previews import PreviewsResource
+
+ return PreviewsResource(self)
+
@cached_property
def bulk(self) -> BulkResource:
from .resources.bulk import BulkResource
@@ -586,6 +597,15 @@ def broadcasts(self) -> AsyncBroadcastsResource:
return AsyncBroadcastsResource(self)
+ @cached_property
+ def previews(self) -> AsyncPreviewsResource:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ from .resources.previews import AsyncPreviewsResource
+
+ return AsyncPreviewsResource(self)
+
@cached_property
def bulk(self) -> AsyncBulkResource:
from .resources.bulk import AsyncBulkResource
@@ -892,6 +912,15 @@ def broadcasts(self) -> broadcasts.BroadcastsResourceWithRawResponse:
return BroadcastsResourceWithRawResponse(self._client.broadcasts)
+ @cached_property
+ def previews(self) -> previews.PreviewsResourceWithRawResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ from .resources.previews import PreviewsResourceWithRawResponse
+
+ return PreviewsResourceWithRawResponse(self._client.previews)
+
@cached_property
def bulk(self) -> bulk.BulkResourceWithRawResponse:
from .resources.bulk import BulkResourceWithRawResponse
@@ -1086,6 +1115,15 @@ def broadcasts(self) -> broadcasts.AsyncBroadcastsResourceWithRawResponse:
return AsyncBroadcastsResourceWithRawResponse(self._client.broadcasts)
+ @cached_property
+ def previews(self) -> previews.AsyncPreviewsResourceWithRawResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ from .resources.previews import AsyncPreviewsResourceWithRawResponse
+
+ return AsyncPreviewsResourceWithRawResponse(self._client.previews)
+
@cached_property
def bulk(self) -> bulk.AsyncBulkResourceWithRawResponse:
from .resources.bulk import AsyncBulkResourceWithRawResponse
@@ -1280,6 +1318,15 @@ def broadcasts(self) -> broadcasts.BroadcastsResourceWithStreamingResponse:
return BroadcastsResourceWithStreamingResponse(self._client.broadcasts)
+ @cached_property
+ def previews(self) -> previews.PreviewsResourceWithStreamingResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ from .resources.previews import PreviewsResourceWithStreamingResponse
+
+ return PreviewsResourceWithStreamingResponse(self._client.previews)
+
@cached_property
def bulk(self) -> bulk.BulkResourceWithStreamingResponse:
from .resources.bulk import BulkResourceWithStreamingResponse
@@ -1474,6 +1521,15 @@ def broadcasts(self) -> broadcasts.AsyncBroadcastsResourceWithStreamingResponse:
return AsyncBroadcastsResourceWithStreamingResponse(self._client.broadcasts)
+ @cached_property
+ def previews(self) -> previews.AsyncPreviewsResourceWithStreamingResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ from .resources.previews import AsyncPreviewsResourceWithStreamingResponse
+
+ return AsyncPreviewsResourceWithStreamingResponse(self._client.previews)
+
@cached_property
def bulk(self) -> bulk.AsyncBulkResourceWithStreamingResponse:
from .resources.bulk import AsyncBulkResourceWithStreamingResponse
diff --git a/src/courier/_version.py b/src/courier/_version.py
index 578d518e..d5fa624b 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__ = "9.10.0" # x-release-please-version
+__version__ = "9.9.0" # x-release-please-version
diff --git a/src/courier/resources/__init__.py b/src/courier/resources/__init__.py
index 4743fc7d..fbed97cd 100644
--- a/src/courier/resources/__init__.py
+++ b/src/courier/resources/__init__.py
@@ -88,6 +88,14 @@
MessagesResourceWithStreamingResponse,
AsyncMessagesResourceWithStreamingResponse,
)
+from .previews import (
+ PreviewsResource,
+ AsyncPreviewsResource,
+ PreviewsResourceWithRawResponse,
+ AsyncPreviewsResourceWithRawResponse,
+ PreviewsResourceWithStreamingResponse,
+ AsyncPreviewsResourceWithStreamingResponse,
+)
from .profiles import (
ProfilesResource,
AsyncProfilesResource,
@@ -226,6 +234,12 @@
"AsyncBroadcastsResourceWithRawResponse",
"BroadcastsResourceWithStreamingResponse",
"AsyncBroadcastsResourceWithStreamingResponse",
+ "PreviewsResource",
+ "AsyncPreviewsResource",
+ "PreviewsResourceWithRawResponse",
+ "AsyncPreviewsResourceWithRawResponse",
+ "PreviewsResourceWithStreamingResponse",
+ "AsyncPreviewsResourceWithStreamingResponse",
"BulkResource",
"AsyncBulkResource",
"BulkResourceWithRawResponse",
diff --git a/src/courier/resources/notifications/__init__.py b/src/courier/resources/notifications/__init__.py
index 75626ebf..7e57643f 100644
--- a/src/courier/resources/notifications/__init__.py
+++ b/src/courier/resources/notifications/__init__.py
@@ -8,6 +8,14 @@
ChecksResourceWithStreamingResponse,
AsyncChecksResourceWithStreamingResponse,
)
+from .previews import (
+ PreviewsResource,
+ AsyncPreviewsResource,
+ PreviewsResourceWithRawResponse,
+ AsyncPreviewsResourceWithRawResponse,
+ PreviewsResourceWithStreamingResponse,
+ AsyncPreviewsResourceWithStreamingResponse,
+)
from .notifications import (
NotificationsResource,
AsyncNotificationsResource,
@@ -24,6 +32,12 @@
"AsyncChecksResourceWithRawResponse",
"ChecksResourceWithStreamingResponse",
"AsyncChecksResourceWithStreamingResponse",
+ "PreviewsResource",
+ "AsyncPreviewsResource",
+ "PreviewsResourceWithRawResponse",
+ "AsyncPreviewsResourceWithRawResponse",
+ "PreviewsResourceWithStreamingResponse",
+ "AsyncPreviewsResourceWithStreamingResponse",
"NotificationsResource",
"AsyncNotificationsResource",
"NotificationsResourceWithRawResponse",
diff --git a/src/courier/resources/notifications/notifications.py b/src/courier/resources/notifications/notifications.py
index 645fecfa..4b1668b1 100644
--- a/src/courier/resources/notifications/notifications.py
+++ b/src/courier/resources/notifications/notifications.py
@@ -41,6 +41,14 @@
async_to_streamed_response_wrapper,
)
from ..._base_client import make_request_options
+from .previews.previews import (
+ PreviewsResource,
+ AsyncPreviewsResource,
+ PreviewsResourceWithRawResponse,
+ AsyncPreviewsResourceWithRawResponse,
+ PreviewsResourceWithStreamingResponse,
+ AsyncPreviewsResourceWithStreamingResponse,
+)
from ...types.notification_list_response import NotificationListResponse
from ...types.notification_template_state import NotificationTemplateState
from ...types.notification_metrics_response import NotificationMetricsResponse
@@ -65,6 +73,10 @@ def checks(self) -> ChecksResource:
"""
return ChecksResource(self._client)
+ @cached_property
+ def previews(self) -> PreviewsResource:
+ return PreviewsResource(self._client)
+
@cached_property
def with_raw_response(self) -> NotificationsResourceWithRawResponse:
"""
@@ -747,6 +759,10 @@ def checks(self) -> AsyncChecksResource:
"""
return AsyncChecksResource(self._client)
+ @cached_property
+ def previews(self) -> AsyncPreviewsResource:
+ return AsyncPreviewsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncNotificationsResourceWithRawResponse:
"""
@@ -1469,6 +1485,10 @@ def checks(self) -> ChecksResourceWithRawResponse:
"""
return ChecksResourceWithRawResponse(self._notifications.checks)
+ @cached_property
+ def previews(self) -> PreviewsResourceWithRawResponse:
+ return PreviewsResourceWithRawResponse(self._notifications.previews)
+
class AsyncNotificationsResourceWithRawResponse:
def __init__(self, notifications: AsyncNotificationsResource) -> None:
@@ -1518,6 +1538,10 @@ def checks(self) -> AsyncChecksResourceWithRawResponse:
"""
return AsyncChecksResourceWithRawResponse(self._notifications.checks)
+ @cached_property
+ def previews(self) -> AsyncPreviewsResourceWithRawResponse:
+ return AsyncPreviewsResourceWithRawResponse(self._notifications.previews)
+
class NotificationsResourceWithStreamingResponse:
def __init__(self, notifications: NotificationsResource) -> None:
@@ -1567,6 +1591,10 @@ def checks(self) -> ChecksResourceWithStreamingResponse:
"""
return ChecksResourceWithStreamingResponse(self._notifications.checks)
+ @cached_property
+ def previews(self) -> PreviewsResourceWithStreamingResponse:
+ return PreviewsResourceWithStreamingResponse(self._notifications.previews)
+
class AsyncNotificationsResourceWithStreamingResponse:
def __init__(self, notifications: AsyncNotificationsResource) -> None:
@@ -1615,3 +1643,7 @@ def checks(self) -> AsyncChecksResourceWithStreamingResponse:
Create, update, version, publish, and localize notification templates and their content.
"""
return AsyncChecksResourceWithStreamingResponse(self._notifications.checks)
+
+ @cached_property
+ def previews(self) -> AsyncPreviewsResourceWithStreamingResponse:
+ return AsyncPreviewsResourceWithStreamingResponse(self._notifications.previews)
diff --git a/src/courier/resources/notifications/previews/__init__.py b/src/courier/resources/notifications/previews/__init__.py
new file mode 100644
index 00000000..f3eed1aa
--- /dev/null
+++ b/src/courier/resources/notifications/previews/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .runs import (
+ RunsResource,
+ AsyncRunsResource,
+ RunsResourceWithRawResponse,
+ AsyncRunsResourceWithRawResponse,
+ RunsResourceWithStreamingResponse,
+ AsyncRunsResourceWithStreamingResponse,
+)
+from .previews import (
+ PreviewsResource,
+ AsyncPreviewsResource,
+ PreviewsResourceWithRawResponse,
+ AsyncPreviewsResourceWithRawResponse,
+ PreviewsResourceWithStreamingResponse,
+ AsyncPreviewsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "RunsResource",
+ "AsyncRunsResource",
+ "RunsResourceWithRawResponse",
+ "AsyncRunsResourceWithRawResponse",
+ "RunsResourceWithStreamingResponse",
+ "AsyncRunsResourceWithStreamingResponse",
+ "PreviewsResource",
+ "AsyncPreviewsResource",
+ "PreviewsResourceWithRawResponse",
+ "AsyncPreviewsResourceWithRawResponse",
+ "PreviewsResourceWithStreamingResponse",
+ "AsyncPreviewsResourceWithStreamingResponse",
+]
diff --git a/src/courier/resources/notifications/previews/previews.py b/src/courier/resources/notifications/previews/previews.py
new file mode 100644
index 00000000..dae4711e
--- /dev/null
+++ b/src/courier/resources/notifications/previews/previews.py
@@ -0,0 +1,120 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .runs import (
+ RunsResource,
+ AsyncRunsResource,
+ RunsResourceWithRawResponse,
+ AsyncRunsResourceWithRawResponse,
+ RunsResourceWithStreamingResponse,
+ AsyncRunsResourceWithStreamingResponse,
+)
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+
+__all__ = ["PreviewsResource", "AsyncPreviewsResource"]
+
+
+class PreviewsResource(SyncAPIResource):
+ @cached_property
+ def runs(self) -> RunsResource:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ return RunsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> PreviewsResourceWithRawResponse:
+ """
+ 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 PreviewsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> PreviewsResourceWithStreamingResponse:
+ """
+ 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 PreviewsResourceWithStreamingResponse(self)
+
+
+class AsyncPreviewsResource(AsyncAPIResource):
+ @cached_property
+ def runs(self) -> AsyncRunsResource:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ return AsyncRunsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncPreviewsResourceWithRawResponse:
+ """
+ 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 AsyncPreviewsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncPreviewsResourceWithStreamingResponse:
+ """
+ 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 AsyncPreviewsResourceWithStreamingResponse(self)
+
+
+class PreviewsResourceWithRawResponse:
+ def __init__(self, previews: PreviewsResource) -> None:
+ self._previews = previews
+
+ @cached_property
+ def runs(self) -> RunsResourceWithRawResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ return RunsResourceWithRawResponse(self._previews.runs)
+
+
+class AsyncPreviewsResourceWithRawResponse:
+ def __init__(self, previews: AsyncPreviewsResource) -> None:
+ self._previews = previews
+
+ @cached_property
+ def runs(self) -> AsyncRunsResourceWithRawResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ return AsyncRunsResourceWithRawResponse(self._previews.runs)
+
+
+class PreviewsResourceWithStreamingResponse:
+ def __init__(self, previews: PreviewsResource) -> None:
+ self._previews = previews
+
+ @cached_property
+ def runs(self) -> RunsResourceWithStreamingResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ return RunsResourceWithStreamingResponse(self._previews.runs)
+
+
+class AsyncPreviewsResourceWithStreamingResponse:
+ def __init__(self, previews: AsyncPreviewsResource) -> None:
+ self._previews = previews
+
+ @cached_property
+ def runs(self) -> AsyncRunsResourceWithStreamingResponse:
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+ return AsyncRunsResourceWithStreamingResponse(self._previews.runs)
diff --git a/src/courier/resources/notifications/previews/runs.py b/src/courier/resources/notifications/previews/runs.py
new file mode 100644
index 00000000..71ecf3a5
--- /dev/null
+++ b/src/courier/resources/notifications/previews/runs.py
@@ -0,0 +1,519 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Optional
+
+import httpx
+
+from ...._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ...._utils import path_template, maybe_transform, strip_not_given, 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.notifications.previews import run_list_params, run_create_params
+from ....types.notifications.previews.preview_run import PreviewRun
+from ....types.notifications.previews.preview_run_detail import PreviewRunDetail
+from ....types.notifications.previews.preview_run_list_response import PreviewRunListResponse
+
+__all__ = ["RunsResource", "AsyncRunsResource"]
+
+
+class RunsResource(SyncAPIResource):
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> RunsResourceWithRawResponse:
+ """
+ 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 RunsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> RunsResourceWithStreamingResponse:
+ """
+ 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 RunsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ id: str,
+ *,
+ data: Dict[str, object] | Omit = omit,
+ device_ids: SequenceNotStr[str] | Omit = omit,
+ device_set_id: str | Omit = omit,
+ locale: str | Omit = omit,
+ template_version: str | Omit = omit,
+ idempotency_key: str | Omit = omit,
+ x_idempotency_expiration: 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,
+ ) -> PreviewRun:
+ """
+ Render this template's email content on each of the requested devices.
+
+ Returns as soon as the run exists and its render is queued — the screenshots are
+ produced asynchronously. Poll
+ `GET /notifications/{id}/previews/runs/{previewRunId}` until every result
+ reaches a terminal status.
+
+ Name the devices either with `device_set_id`, for a saved set, or with
+ `device_ids`, for a one-off list. Exactly one of the two is required. Inline
+ `device_ids` must be ids listed by `GET /previews/devices`; any other id is a
+ 422, refused before the run exists or is billed.
+
+ A template that does not exist is a 404. One that exists but cannot be previewed
+ — not a Design Studio template, no email channel, or no such `template_version`
+ — is a 422, also refused before the run exists or is billed.
+
+ Preview runs are a metered add-on. A workspace without it, or with its billing
+ suspended, receives a 402.
+
+ Args:
+ data: Template variables to render with, the same shape as the `data` object on a
+ send.
+
+ device_ids: The devices to render on, by `PreviewDevice.id`, for a one-off run. Mutually
+ exclusive with `device_set_id`.
+
+ device_set_id: A saved device set naming the devices to render on. Mutually exclusive with
+ `device_ids`.
+
+ locale: Render the template's content for this locale, e.g. "fr-FR".
+
+ template_version: Which version of the template to render. Omit for the latest saved draft, which
+ always exists and is what the editor shows. `published` renders the live
+ version; a zero-padded `v002` renders that specific publish. Versions are
+ 1-based, so `v000` is not a version, and the unpadded `v2` is rejected — that
+ spelling belongs to journeys' AutomationVersionId, a different scheme in which
+ `v0` means published.
+
+ 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 id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {
+ **strip_not_given(
+ {
+ "Idempotency-Key": idempotency_key,
+ "x-idempotency-expiration": x_idempotency_expiration,
+ }
+ ),
+ **(extra_headers or {}),
+ }
+ return self._post(
+ path_template("/notifications/{id}/previews/runs", id=id),
+ body=maybe_transform(
+ {
+ "data": data,
+ "device_ids": device_ids,
+ "device_set_id": device_set_id,
+ "locale": locale,
+ "template_version": template_version,
+ },
+ run_create_params.RunCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreviewRun,
+ )
+
+ def retrieve(
+ self,
+ preview_run_id: str,
+ *,
+ 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,
+ ) -> PreviewRunDetail:
+ """
+ Retrieve one of this template's preview runs together with its per-device
+ results.
+
+ A run is only readable under the template it previewed: under any other template
+ it is a 404, the same as a run that does not exist.
+
+ `thumbnail_url` and `screenshot_url` are short-lived signed URLs, re-signed on
+ every read. Fetch them now rather than storing them. Both are null until
+ Courier's own copy of the image exists, which is what `status: COMPLETED` on a
+ result means.
+
+ 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 id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not preview_run_id:
+ raise ValueError(f"Expected a non-empty value for `preview_run_id` but received {preview_run_id!r}")
+ return self._get(
+ path_template("/notifications/{id}/previews/runs/{preview_run_id}", id=id, preview_run_id=preview_run_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreviewRunDetail,
+ )
+
+ def list(
+ self,
+ id: str,
+ *,
+ 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,
+ ) -> PreviewRunListResponse:
+ """List this template's preview runs, newest first.
+
+ Cursor-paginated.
+
+ A template that does not exist is a 404, the same as every other
+ `/notifications/{id}` route.
+
+ 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
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/notifications/{id}/previews/runs", id=id),
+ 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,
+ },
+ run_list_params.RunListParams,
+ ),
+ ),
+ cast_to=PreviewRunListResponse,
+ )
+
+
+class AsyncRunsResource(AsyncAPIResource):
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> AsyncRunsResourceWithRawResponse:
+ """
+ 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 AsyncRunsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncRunsResourceWithStreamingResponse:
+ """
+ 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 AsyncRunsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ id: str,
+ *,
+ data: Dict[str, object] | Omit = omit,
+ device_ids: SequenceNotStr[str] | Omit = omit,
+ device_set_id: str | Omit = omit,
+ locale: str | Omit = omit,
+ template_version: str | Omit = omit,
+ idempotency_key: str | Omit = omit,
+ x_idempotency_expiration: 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,
+ ) -> PreviewRun:
+ """
+ Render this template's email content on each of the requested devices.
+
+ Returns as soon as the run exists and its render is queued — the screenshots are
+ produced asynchronously. Poll
+ `GET /notifications/{id}/previews/runs/{previewRunId}` until every result
+ reaches a terminal status.
+
+ Name the devices either with `device_set_id`, for a saved set, or with
+ `device_ids`, for a one-off list. Exactly one of the two is required. Inline
+ `device_ids` must be ids listed by `GET /previews/devices`; any other id is a
+ 422, refused before the run exists or is billed.
+
+ A template that does not exist is a 404. One that exists but cannot be previewed
+ — not a Design Studio template, no email channel, or no such `template_version`
+ — is a 422, also refused before the run exists or is billed.
+
+ Preview runs are a metered add-on. A workspace without it, or with its billing
+ suspended, receives a 402.
+
+ Args:
+ data: Template variables to render with, the same shape as the `data` object on a
+ send.
+
+ device_ids: The devices to render on, by `PreviewDevice.id`, for a one-off run. Mutually
+ exclusive with `device_set_id`.
+
+ device_set_id: A saved device set naming the devices to render on. Mutually exclusive with
+ `device_ids`.
+
+ locale: Render the template's content for this locale, e.g. "fr-FR".
+
+ template_version: Which version of the template to render. Omit for the latest saved draft, which
+ always exists and is what the editor shows. `published` renders the live
+ version; a zero-padded `v002` renders that specific publish. Versions are
+ 1-based, so `v000` is not a version, and the unpadded `v2` is rejected — that
+ spelling belongs to journeys' AutomationVersionId, a different scheme in which
+ `v0` means published.
+
+ 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 id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {
+ **strip_not_given(
+ {
+ "Idempotency-Key": idempotency_key,
+ "x-idempotency-expiration": x_idempotency_expiration,
+ }
+ ),
+ **(extra_headers or {}),
+ }
+ return await self._post(
+ path_template("/notifications/{id}/previews/runs", id=id),
+ body=await async_maybe_transform(
+ {
+ "data": data,
+ "device_ids": device_ids,
+ "device_set_id": device_set_id,
+ "locale": locale,
+ "template_version": template_version,
+ },
+ run_create_params.RunCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreviewRun,
+ )
+
+ async def retrieve(
+ self,
+ preview_run_id: str,
+ *,
+ 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,
+ ) -> PreviewRunDetail:
+ """
+ Retrieve one of this template's preview runs together with its per-device
+ results.
+
+ A run is only readable under the template it previewed: under any other template
+ it is a 404, the same as a run that does not exist.
+
+ `thumbnail_url` and `screenshot_url` are short-lived signed URLs, re-signed on
+ every read. Fetch them now rather than storing them. Both are null until
+ Courier's own copy of the image exists, which is what `status: COMPLETED` on a
+ result means.
+
+ 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 id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not preview_run_id:
+ raise ValueError(f"Expected a non-empty value for `preview_run_id` but received {preview_run_id!r}")
+ return await self._get(
+ path_template("/notifications/{id}/previews/runs/{preview_run_id}", id=id, preview_run_id=preview_run_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreviewRunDetail,
+ )
+
+ async def list(
+ self,
+ id: str,
+ *,
+ 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,
+ ) -> PreviewRunListResponse:
+ """List this template's preview runs, newest first.
+
+ Cursor-paginated.
+
+ A template that does not exist is a 404, the same as every other
+ `/notifications/{id}` route.
+
+ 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
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/notifications/{id}/previews/runs", id=id),
+ 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,
+ },
+ run_list_params.RunListParams,
+ ),
+ ),
+ cast_to=PreviewRunListResponse,
+ )
+
+
+class RunsResourceWithRawResponse:
+ def __init__(self, runs: RunsResource) -> None:
+ self._runs = runs
+
+ self.create = to_raw_response_wrapper(
+ runs.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ runs.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ runs.list,
+ )
+
+
+class AsyncRunsResourceWithRawResponse:
+ def __init__(self, runs: AsyncRunsResource) -> None:
+ self._runs = runs
+
+ self.create = async_to_raw_response_wrapper(
+ runs.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ runs.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ runs.list,
+ )
+
+
+class RunsResourceWithStreamingResponse:
+ def __init__(self, runs: RunsResource) -> None:
+ self._runs = runs
+
+ self.create = to_streamed_response_wrapper(
+ runs.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ runs.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ runs.list,
+ )
+
+
+class AsyncRunsResourceWithStreamingResponse:
+ def __init__(self, runs: AsyncRunsResource) -> None:
+ self._runs = runs
+
+ self.create = async_to_streamed_response_wrapper(
+ runs.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ runs.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ runs.list,
+ )
diff --git a/src/courier/resources/previews.py b/src/courier/resources/previews.py
new file mode 100644
index 00000000..c57aa293
--- /dev/null
+++ b/src/courier/resources/previews.py
@@ -0,0 +1,585 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..types import preview_create_device_set_params, preview_update_device_set_params
+from .._types import Body, Query, Headers, NotGiven, SequenceNotStr, 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.device_set import DeviceSet
+from ..types.device_set_list_response import DeviceSetListResponse
+from ..types.preview_device_list_response import PreviewDeviceListResponse
+
+__all__ = ["PreviewsResource", "AsyncPreviewsResource"]
+
+
+class PreviewsResource(SyncAPIResource):
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> PreviewsResourceWithRawResponse:
+ """
+ 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 PreviewsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> PreviewsResourceWithStreamingResponse:
+ """
+ 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 PreviewsResourceWithStreamingResponse(self)
+
+ def archive_device_set(
+ self,
+ device_set_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,
+ ) -> DeviceSet:
+ """Archive a device set.
+
+ This is a soft delete — the archived set is returned and
+ no longer appears in list results. Runs already created against it keep their
+ own copy of the device list and are unaffected. The Courier-provided default set
+ cannot be archived and returns 409.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not device_set_id:
+ raise ValueError(f"Expected a non-empty value for `device_set_id` but received {device_set_id!r}")
+ return self._delete(
+ path_template("/previews/device-sets/{device_set_id}", device_set_id=device_set_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+ def create_device_set(
+ self,
+ *,
+ device_ids: SequenceNotStr[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,
+ ) -> DeviceSet:
+ """Create a named, reusable set of preview devices.
+
+ Every id must be one listed by
+ `GET /previews/devices`; any other is a 422.
+
+ Args:
+ device_ids: The devices the set contains, by `PreviewDevice.id`. At least one is required.
+
+ 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(
+ "/previews/device-sets",
+ body=maybe_transform(
+ {
+ "device_ids": device_ids,
+ "name": name,
+ },
+ preview_create_device_set_params.PreviewCreateDeviceSetParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+ def list_device_sets(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> DeviceSetListResponse:
+ """List the workspace's preview sets. Archived sets are not returned."""
+ return self._get(
+ "/previews/device-sets",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSetListResponse,
+ )
+
+ def list_devices(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> PreviewDeviceListResponse:
+ """List the devices a preview can be rendered on.
+
+ Reference data, identical for
+ every workspace — these ids are what a device set is built from and what a run
+ reports results for.
+ """
+ return self._get(
+ "/previews/devices",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreviewDeviceListResponse,
+ )
+
+ def retrieve_device_set(
+ self,
+ device_set_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,
+ ) -> DeviceSet:
+ """Retrieve a preview set by ID.
+
+ Archived sets 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 device_set_id:
+ raise ValueError(f"Expected a non-empty value for `device_set_id` but received {device_set_id!r}")
+ return self._get(
+ path_template("/previews/device-sets/{device_set_id}", device_set_id=device_set_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+ def update_device_set(
+ self,
+ device_set_id: str,
+ *,
+ device_ids: SequenceNotStr[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,
+ ) -> DeviceSet:
+ """Replace a device set.
+
+ This is a full replace, not a patch — both the name and
+ the device list are always written. The Courier-provided default set cannot be
+ changed and returns 409.
+
+ Args:
+ device_ids: The devices the set contains, by `PreviewDevice.id`. At least one is required.
+
+ 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
+ """
+ if not device_set_id:
+ raise ValueError(f"Expected a non-empty value for `device_set_id` but received {device_set_id!r}")
+ return self._put(
+ path_template("/previews/device-sets/{device_set_id}", device_set_id=device_set_id),
+ body=maybe_transform(
+ {
+ "device_ids": device_ids,
+ "name": name,
+ },
+ preview_update_device_set_params.PreviewUpdateDeviceSetParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+
+class AsyncPreviewsResource(AsyncAPIResource):
+ """
+ Render a template's email content on real email clients and read back the screenshots, so you can check how it looks before you send it.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> AsyncPreviewsResourceWithRawResponse:
+ """
+ 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 AsyncPreviewsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncPreviewsResourceWithStreamingResponse:
+ """
+ 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 AsyncPreviewsResourceWithStreamingResponse(self)
+
+ async def archive_device_set(
+ self,
+ device_set_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,
+ ) -> DeviceSet:
+ """Archive a device set.
+
+ This is a soft delete — the archived set is returned and
+ no longer appears in list results. Runs already created against it keep their
+ own copy of the device list and are unaffected. The Courier-provided default set
+ cannot be archived and returns 409.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not device_set_id:
+ raise ValueError(f"Expected a non-empty value for `device_set_id` but received {device_set_id!r}")
+ return await self._delete(
+ path_template("/previews/device-sets/{device_set_id}", device_set_id=device_set_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+ async def create_device_set(
+ self,
+ *,
+ device_ids: SequenceNotStr[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,
+ ) -> DeviceSet:
+ """Create a named, reusable set of preview devices.
+
+ Every id must be one listed by
+ `GET /previews/devices`; any other is a 422.
+
+ Args:
+ device_ids: The devices the set contains, by `PreviewDevice.id`. At least one is required.
+
+ 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(
+ "/previews/device-sets",
+ body=await async_maybe_transform(
+ {
+ "device_ids": device_ids,
+ "name": name,
+ },
+ preview_create_device_set_params.PreviewCreateDeviceSetParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+ async def list_device_sets(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> DeviceSetListResponse:
+ """List the workspace's preview sets. Archived sets are not returned."""
+ return await self._get(
+ "/previews/device-sets",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSetListResponse,
+ )
+
+ async def list_devices(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> PreviewDeviceListResponse:
+ """List the devices a preview can be rendered on.
+
+ Reference data, identical for
+ every workspace — these ids are what a device set is built from and what a run
+ reports results for.
+ """
+ return await self._get(
+ "/previews/devices",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=PreviewDeviceListResponse,
+ )
+
+ async def retrieve_device_set(
+ self,
+ device_set_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,
+ ) -> DeviceSet:
+ """Retrieve a preview set by ID.
+
+ Archived sets 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 device_set_id:
+ raise ValueError(f"Expected a non-empty value for `device_set_id` but received {device_set_id!r}")
+ return await self._get(
+ path_template("/previews/device-sets/{device_set_id}", device_set_id=device_set_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+ async def update_device_set(
+ self,
+ device_set_id: str,
+ *,
+ device_ids: SequenceNotStr[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,
+ ) -> DeviceSet:
+ """Replace a device set.
+
+ This is a full replace, not a patch — both the name and
+ the device list are always written. The Courier-provided default set cannot be
+ changed and returns 409.
+
+ Args:
+ device_ids: The devices the set contains, by `PreviewDevice.id`. At least one is required.
+
+ 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
+ """
+ if not device_set_id:
+ raise ValueError(f"Expected a non-empty value for `device_set_id` but received {device_set_id!r}")
+ return await self._put(
+ path_template("/previews/device-sets/{device_set_id}", device_set_id=device_set_id),
+ body=await async_maybe_transform(
+ {
+ "device_ids": device_ids,
+ "name": name,
+ },
+ preview_update_device_set_params.PreviewUpdateDeviceSetParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=DeviceSet,
+ )
+
+
+class PreviewsResourceWithRawResponse:
+ def __init__(self, previews: PreviewsResource) -> None:
+ self._previews = previews
+
+ self.archive_device_set = to_raw_response_wrapper(
+ previews.archive_device_set,
+ )
+ self.create_device_set = to_raw_response_wrapper(
+ previews.create_device_set,
+ )
+ self.list_device_sets = to_raw_response_wrapper(
+ previews.list_device_sets,
+ )
+ self.list_devices = to_raw_response_wrapper(
+ previews.list_devices,
+ )
+ self.retrieve_device_set = to_raw_response_wrapper(
+ previews.retrieve_device_set,
+ )
+ self.update_device_set = to_raw_response_wrapper(
+ previews.update_device_set,
+ )
+
+
+class AsyncPreviewsResourceWithRawResponse:
+ def __init__(self, previews: AsyncPreviewsResource) -> None:
+ self._previews = previews
+
+ self.archive_device_set = async_to_raw_response_wrapper(
+ previews.archive_device_set,
+ )
+ self.create_device_set = async_to_raw_response_wrapper(
+ previews.create_device_set,
+ )
+ self.list_device_sets = async_to_raw_response_wrapper(
+ previews.list_device_sets,
+ )
+ self.list_devices = async_to_raw_response_wrapper(
+ previews.list_devices,
+ )
+ self.retrieve_device_set = async_to_raw_response_wrapper(
+ previews.retrieve_device_set,
+ )
+ self.update_device_set = async_to_raw_response_wrapper(
+ previews.update_device_set,
+ )
+
+
+class PreviewsResourceWithStreamingResponse:
+ def __init__(self, previews: PreviewsResource) -> None:
+ self._previews = previews
+
+ self.archive_device_set = to_streamed_response_wrapper(
+ previews.archive_device_set,
+ )
+ self.create_device_set = to_streamed_response_wrapper(
+ previews.create_device_set,
+ )
+ self.list_device_sets = to_streamed_response_wrapper(
+ previews.list_device_sets,
+ )
+ self.list_devices = to_streamed_response_wrapper(
+ previews.list_devices,
+ )
+ self.retrieve_device_set = to_streamed_response_wrapper(
+ previews.retrieve_device_set,
+ )
+ self.update_device_set = to_streamed_response_wrapper(
+ previews.update_device_set,
+ )
+
+
+class AsyncPreviewsResourceWithStreamingResponse:
+ def __init__(self, previews: AsyncPreviewsResource) -> None:
+ self._previews = previews
+
+ self.archive_device_set = async_to_streamed_response_wrapper(
+ previews.archive_device_set,
+ )
+ self.create_device_set = async_to_streamed_response_wrapper(
+ previews.create_device_set,
+ )
+ self.list_device_sets = async_to_streamed_response_wrapper(
+ previews.list_device_sets,
+ )
+ self.list_devices = async_to_streamed_response_wrapper(
+ previews.list_devices,
+ )
+ self.retrieve_device_set = async_to_streamed_response_wrapper(
+ previews.retrieve_device_set,
+ )
+ self.update_device_set = async_to_streamed_response_wrapper(
+ previews.update_device_set,
+ )
diff --git a/src/courier/types/__init__.py b/src/courier/types/__init__.py
index eeb635da..f3a30f73 100644
--- a/src/courier/types/__init__.py
+++ b/src/courier/types/__init__.py
@@ -115,6 +115,7 @@
from .provider import Provider as Provider
from .broadcast import Broadcast as Broadcast
from .base_check import BaseCheck as BaseCheck
+from .device_set import DeviceSet as DeviceSet
from .email_head import EmailHead as EmailHead
from .logo_param import LogoParam as LogoParam
from .audit_event import AuditEvent as AuditEvent
@@ -130,6 +131,7 @@
from .brand_settings import BrandSettings as BrandSettings
from .brand_snippets import BrandSnippets as BrandSnippets
from .brand_template import BrandTemplate as BrandTemplate
+from .preview_device import PreviewDevice as PreviewDevice
from .digest_category import DigestCategory as DigestCategory
from .digest_instance import DigestInstance as DigestInstance
from .journey_ai_node import JourneyAINode as JourneyAINode
@@ -227,6 +229,7 @@
from .automation_run_list_item import AutomationRunListItem as AutomationRunListItem
from .bulk_create_job_response import BulkCreateJobResponse as BulkCreateJobResponse
from .bulk_list_users_response import BulkListUsersResponse as BulkListUsersResponse
+from .device_set_list_response import DeviceSetListResponse as DeviceSetListResponse
from .journey_conditions_field import JourneyConditionsField as JourneyConditionsField
from .journey_delay_until_node import JourneyDelayUntilNode as JourneyDelayUntilNode
from .journey_experiment_param import JourneyExperimentParam as JourneyExperimentParam
@@ -282,6 +285,7 @@
from .journey_throttle_static_node import JourneyThrottleStaticNode as JourneyThrottleStaticNode
from .journey_webhook_trigger_node import JourneyWebhookTriggerNode as JourneyWebhookTriggerNode
from .notification_retrieve_params import NotificationRetrieveParams as NotificationRetrieveParams
+from .preview_device_list_response import PreviewDeviceListResponse as PreviewDeviceListResponse
from .publish_preferences_response import PublishPreferencesResponse as PublishPreferencesResponse
from .put_tenant_template_response import PutTenantTemplateResponse as PutTenantTemplateResponse
from .routing_strategy_list_params import RoutingStrategyListParams as RoutingStrategyListParams
@@ -321,6 +325,8 @@
from .journey_experiment_variant_param import JourneyExperimentVariantParam as JourneyExperimentVariantParam
from .journey_send_node_to_slack_email import JourneySendNodeToSlackEmail as JourneySendNodeToSlackEmail
from .journey_send_node_to_slack_param import JourneySendNodeToSlackParam as JourneySendNodeToSlackParam
+from .preview_create_device_set_params import PreviewCreateDeviceSetParams as PreviewCreateDeviceSetParams
+from .preview_update_device_set_params import PreviewUpdateDeviceSetParams as PreviewUpdateDeviceSetParams
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
diff --git a/src/courier/types/device_set.py b/src/courier/types/device_set.py
new file mode 100644
index 00000000..0db6b25a
--- /dev/null
+++ b/src/courier/types/device_set.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+
+__all__ = ["DeviceSet"]
+
+
+class DeviceSet(BaseModel):
+ """A named, reusable list of preview devices."""
+
+ id: str
+ """Unique identifier for the device set."""
+
+ created_at: str
+ """ISO-8601 timestamp of when the set was created."""
+
+ device_ids: List[str]
+ """The devices in this set, by `PreviewDevice.id`."""
+
+ name: str
+ """Human-readable name."""
+
+ updated_at: str
+ """ISO-8601 timestamp of when the set was last written."""
+
+ archived_at: Optional[str] = None
+ """ISO-8601 timestamp of when the set was archived.
+
+ Present only on the archive response, which is the one place the state is
+ observable.
+ """
diff --git a/src/courier/types/device_set_list_response.py b/src/courier/types/device_set_list_response.py
new file mode 100644
index 00000000..6d08f26c
--- /dev/null
+++ b/src/courier/types/device_set_list_response.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .._models import BaseModel
+from .device_set import DeviceSet
+
+__all__ = ["DeviceSetListResponse"]
+
+
+class DeviceSetListResponse(BaseModel):
+ """The workspace's active device sets. Not paginated."""
+
+ results: List[DeviceSet]
diff --git a/src/courier/types/notifications/previews/__init__.py b/src/courier/types/notifications/previews/__init__.py
new file mode 100644
index 00000000..2c8cb69d
--- /dev/null
+++ b/src/courier/types/notifications/previews/__init__.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .preview_run import PreviewRun as PreviewRun
+from .preview_result import PreviewResult as PreviewResult
+from .run_list_params import RunListParams as RunListParams
+from .run_create_params import RunCreateParams as RunCreateParams
+from .preview_run_detail import PreviewRunDetail as PreviewRunDetail
+from .preview_run_status import PreviewRunStatus as PreviewRunStatus
+from .preview_result_status import PreviewResultStatus as PreviewResultStatus
+from .preview_run_list_response import PreviewRunListResponse as PreviewRunListResponse
+from .preview_run_failure_reason import PreviewRunFailureReason as PreviewRunFailureReason
+from .preview_result_failure_reason import PreviewResultFailureReason as PreviewResultFailureReason
diff --git a/src/courier/types/notifications/previews/preview_result.py b/src/courier/types/notifications/previews/preview_result.py
new file mode 100644
index 00000000..d92c3afa
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_result.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ...._models import BaseModel
+from .preview_result_status import PreviewResultStatus
+from .preview_result_failure_reason import PreviewResultFailureReason
+
+__all__ = ["PreviewResult"]
+
+
+class PreviewResult(BaseModel):
+ """One device's result within a preview run."""
+
+ device_id: str
+ """The device this result is for, by `PreviewDevice.id`."""
+
+ screenshot_url: Optional[str] = None
+ """Short-lived signed URL for the full-sized image.
+
+ Null until the screenshot exists. Re-signed on every read, so fetch it rather
+ than storing it.
+ """
+
+ status: PreviewResultStatus
+ """One device's outcome.
+
+ `COMPLETED` means the screenshot exists and its URLs are populated.
+ `UNSUPPORTED`, `TIMED_OUT` and `FAILED` are all terminal, and none stands in for
+ another — `UNSUPPORTED` means the device was retired at the vendor, `TIMED_OUT`
+ means it did not report in time.
+ """
+
+ thumbnail_url: Optional[str] = None
+ """Short-lived signed URL for the grid-sized image.
+
+ Null until the screenshot exists. Re-signed on every read, so fetch it rather
+ than storing it.
+ """
+
+ failure_reason: Optional[PreviewResultFailureReason] = None
+ """
+ Why one device's render failed, when its `status` is `FAILED` and the cause has
+ a public name. `DELIVERY_FAILED` means the rendering service could not deliver
+ the message to its own capture mailbox — infrastructure, not anything wrong with
+ the template.
+ """
diff --git a/src/courier/types/notifications/previews/preview_result_failure_reason.py b/src/courier/types/notifications/previews/preview_result_failure_reason.py
new file mode 100644
index 00000000..ce4699d4
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_result_failure_reason.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["PreviewResultFailureReason"]
+
+PreviewResultFailureReason: TypeAlias = Literal["DELIVERY_FAILED"]
diff --git a/src/courier/types/notifications/previews/preview_result_status.py b/src/courier/types/notifications/previews/preview_result_status.py
new file mode 100644
index 00000000..c8cb9955
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_result_status.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["PreviewResultStatus"]
+
+PreviewResultStatus: TypeAlias = Literal["PENDING", "PROCESSING", "COMPLETED", "UNSUPPORTED", "TIMED_OUT", "FAILED"]
diff --git a/src/courier/types/notifications/previews/preview_run.py b/src/courier/types/notifications/previews/preview_run.py
new file mode 100644
index 00000000..ccb19320
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_run.py
@@ -0,0 +1,48 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from ...._models import BaseModel
+from .preview_run_status import PreviewRunStatus
+from .preview_run_failure_reason import PreviewRunFailureReason
+
+__all__ = ["PreviewRun"]
+
+
+class PreviewRun(BaseModel):
+ """One render of a template across a set of devices. Billable."""
+
+ id: str
+ """Unique identifier for the preview run."""
+
+ created_at: str
+ """ISO-8601 timestamp of when the run was created."""
+
+ device_ids: List[str]
+ """The devices this run was submitted for, snapshotted when the run was created."""
+
+ status: PreviewRunStatus
+ """Where the run itself has got to.
+
+ `PENDING` and `RENDERED` mean Courier is still preparing the email, `SUBMITTED`
+ means it is with the rendering service, and `COMPLETED` means every device has
+ reported. `FAILED` is the run as a whole failing — an individual device failing
+ never fails the run.
+ """
+
+ template_id: str
+ """The template that was rendered."""
+
+ failure_reason: Optional[PreviewRunFailureReason] = None
+ """Why the run failed, when `status` is `FAILED`.
+
+ `NO_EMAIL_CHANNEL` and `TEMPLATE_NOT_SUPPORTED` mean there was nothing to
+ render; `ALL_DEVICES_UNSUPPORTED` means every requested device has been retired
+ and the request can be fixed by choosing others.
+ """
+
+ template_version: Optional[str] = None
+ """
+ The version of the template that was rendered — `draft`, or a zero-padded
+ published version such as `v002`. Absent until the render settles.
+ """
diff --git a/src/courier/types/notifications/previews/preview_run_detail.py b/src/courier/types/notifications/previews/preview_run_detail.py
new file mode 100644
index 00000000..647f5356
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_run_detail.py
@@ -0,0 +1,52 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from ...._models import BaseModel
+from .preview_result import PreviewResult
+from .preview_run_status import PreviewRunStatus
+from .preview_run_failure_reason import PreviewRunFailureReason
+
+__all__ = ["PreviewRunDetail"]
+
+
+class PreviewRunDetail(BaseModel):
+ """A preview run together with its per-device results."""
+
+ id: str
+ """Unique identifier for the preview run."""
+
+ created_at: str
+ """ISO-8601 timestamp of when the run was created."""
+
+ device_ids: List[str]
+ """The devices this run was submitted for, snapshotted when the run was created."""
+
+ results: List[PreviewResult]
+ """One entry per device in `device_ids`."""
+
+ status: PreviewRunStatus
+ """Where the run itself has got to.
+
+ `PENDING` and `RENDERED` mean Courier is still preparing the email, `SUBMITTED`
+ means it is with the rendering service, and `COMPLETED` means every device has
+ reported. `FAILED` is the run as a whole failing — an individual device failing
+ never fails the run.
+ """
+
+ template_id: str
+ """The template that was rendered."""
+
+ failure_reason: Optional[PreviewRunFailureReason] = None
+ """Why the run failed, when `status` is `FAILED`.
+
+ `NO_EMAIL_CHANNEL` and `TEMPLATE_NOT_SUPPORTED` mean there was nothing to
+ render; `ALL_DEVICES_UNSUPPORTED` means every requested device has been retired
+ and the request can be fixed by choosing others.
+ """
+
+ template_version: Optional[str] = None
+ """
+ The version of the template that was rendered — `draft`, or a zero-padded
+ published version such as `v002`. Absent until the render settles.
+ """
diff --git a/src/courier/types/notifications/previews/preview_run_failure_reason.py b/src/courier/types/notifications/previews/preview_run_failure_reason.py
new file mode 100644
index 00000000..7478f404
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_run_failure_reason.py
@@ -0,0 +1,9 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["PreviewRunFailureReason"]
+
+PreviewRunFailureReason: TypeAlias = Literal[
+ "TEMPLATE_NOT_SUPPORTED", "NO_EMAIL_CHANNEL", "RENDER_FAILED", "ALL_DEVICES_UNSUPPORTED", "VENDOR_ERROR"
+]
diff --git a/src/courier/types/notifications/previews/preview_run_list_response.py b/src/courier/types/notifications/previews/preview_run_list_response.py
new file mode 100644
index 00000000..622f8cce
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_run_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 .preview_run import PreviewRun
+from ...shared.paging import Paging
+
+__all__ = ["PreviewRunListResponse"]
+
+
+class PreviewRunListResponse(BaseModel):
+ """Paginated list of preview runs, newest first."""
+
+ paging: Paging
+
+ results: List[PreviewRun]
diff --git a/src/courier/types/notifications/previews/preview_run_status.py b/src/courier/types/notifications/previews/preview_run_status.py
new file mode 100644
index 00000000..38fd6b78
--- /dev/null
+++ b/src/courier/types/notifications/previews/preview_run_status.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["PreviewRunStatus"]
+
+PreviewRunStatus: TypeAlias = Literal["PENDING", "RENDERED", "SUBMITTED", "COMPLETED", "FAILED"]
diff --git a/src/courier/types/notifications/previews/run_create_params.py b/src/courier/types/notifications/previews/run_create_params.py
new file mode 100644
index 00000000..3fc783ce
--- /dev/null
+++ b/src/courier/types/notifications/previews/run_create_params.py
@@ -0,0 +1,48 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing_extensions import Annotated, TypedDict
+
+from ...._types import SequenceNotStr
+from ...._utils import PropertyInfo
+
+__all__ = ["RunCreateParams"]
+
+
+class RunCreateParams(TypedDict, total=False):
+ data: Dict[str, object]
+ """
+ Template variables to render with, the same shape as the `data` object on a
+ send.
+ """
+
+ device_ids: SequenceNotStr[str]
+ """The devices to render on, by `PreviewDevice.id`, for a one-off run.
+
+ Mutually exclusive with `device_set_id`.
+ """
+
+ device_set_id: str
+ """A saved device set naming the devices to render on.
+
+ Mutually exclusive with `device_ids`.
+ """
+
+ locale: str
+ """Render the template's content for this locale, e.g. "fr-FR"."""
+
+ template_version: str
+ """Which version of the template to render.
+
+ Omit for the latest saved draft, which always exists and is what the editor
+ shows. `published` renders the live version; a zero-padded `v002` renders that
+ specific publish. Versions are 1-based, so `v000` is not a version, and the
+ unpadded `v2` is rejected — that spelling belongs to journeys'
+ AutomationVersionId, a different scheme in which `v0` means published.
+ """
+
+ idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")]
+
+ x_idempotency_expiration: Annotated[str, PropertyInfo(alias="x-idempotency-expiration")]
diff --git a/src/courier/types/notifications/previews/run_list_params.py b/src/courier/types/notifications/previews/run_list_params.py
new file mode 100644
index 00000000..250cc026
--- /dev/null
+++ b/src/courier/types/notifications/previews/run_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__ = ["RunListParams"]
+
+
+class RunListParams(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/preview_create_device_set_params.py b/src/courier/types/preview_create_device_set_params.py
new file mode 100644
index 00000000..bca64559
--- /dev/null
+++ b/src/courier/types/preview_create_device_set_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["PreviewCreateDeviceSetParams"]
+
+
+class PreviewCreateDeviceSetParams(TypedDict, total=False):
+ device_ids: Required[SequenceNotStr[str]]
+ """The devices the set contains, by `PreviewDevice.id`. At least one is required."""
+
+ name: Required[str]
+ """Human-readable name."""
diff --git a/src/courier/types/preview_device.py b/src/courier/types/preview_device.py
new file mode 100644
index 00000000..df7846ae
--- /dev/null
+++ b/src/courier/types/preview_device.py
@@ -0,0 +1,62 @@
+# 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__ = ["PreviewDevice"]
+
+
+class PreviewDevice(BaseModel):
+ """
+ One mail app on one platform, operating system and theme that a preview can be rendered on. Reference data, identical for every workspace. Every field is always present; `platform` and `platform_version` are null where they do not apply.
+ """
+
+ id: str
+ """
+ The device's identifier, used in `device_ids` when creating a device set or a
+ run.
+ """
+
+ app: str
+ """The mail app.
+
+ For webmail it is the service (`outlook_com`, `gmail_com`); for mobile the app
+ (`apple_mail`, `gmail`); for desktop the app together with the version it is
+ sold under (`outlook_2019`, `outlook_microsoft_365`, `apple_mail_16`), because
+ that version is what separates one desktop Outlook from another.
+ """
+
+ category: Literal["webmail", "mobile", "desktop"]
+ """Where the app runs."""
+
+ name: str
+ """Display name.
+
+ Render it as-is rather than parsing it. It is also what separates the two
+ 120-dpi Outlook renders from their 100% siblings, which are otherwise identical
+ field for field.
+ """
+
+ os: str
+ """The operating system."""
+
+ os_version: str
+ """The operating system's version. Always set."""
+
+ platform: Optional[str] = None
+ """
+ What the app runs on — the browser for webmail (`chrome`, `edge`, `firefox`),
+ the phone for mobile (`iphone`, `pixel`). Null for desktop, where the app runs
+ on nothing but the OS.
+ """
+
+ platform_version: Optional[str] = None
+ """Which one of the platform — the phone model for mobile (`15_pro_max`, `10`).
+
+ Null for webmail, which always renders in the current browser, and for desktop.
+ """
+
+ theme: Literal["light", "dark"]
+ """Whether the email is rendered in light or dark mode."""
diff --git a/src/courier/types/preview_device_list_response.py b/src/courier/types/preview_device_list_response.py
new file mode 100644
index 00000000..ffb535c9
--- /dev/null
+++ b/src/courier/types/preview_device_list_response.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .._models import BaseModel
+from .preview_device import PreviewDevice
+
+__all__ = ["PreviewDeviceListResponse"]
+
+
+class PreviewDeviceListResponse(BaseModel):
+ """The full catalog of renderable devices. Not paginated."""
+
+ results: List[PreviewDevice]
diff --git a/src/courier/types/preview_update_device_set_params.py b/src/courier/types/preview_update_device_set_params.py
new file mode 100644
index 00000000..712d3e2f
--- /dev/null
+++ b/src/courier/types/preview_update_device_set_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["PreviewUpdateDeviceSetParams"]
+
+
+class PreviewUpdateDeviceSetParams(TypedDict, total=False):
+ device_ids: Required[SequenceNotStr[str]]
+ """The devices the set contains, by `PreviewDevice.id`. At least one is required."""
+
+ name: Required[str]
+ """Human-readable name."""
diff --git a/tests/api_resources/notifications/previews/__init__.py b/tests/api_resources/notifications/previews/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/notifications/previews/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/notifications/previews/test_runs.py b/tests/api_resources/notifications/previews/test_runs.py
new file mode 100644
index 00000000..482f9aec
--- /dev/null
+++ b/tests/api_resources/notifications/previews/test_runs.py
@@ -0,0 +1,350 @@
+# 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.notifications.previews import (
+ PreviewRun,
+ PreviewRunDetail,
+ PreviewRunListResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestRuns:
+ 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:
+ run = client.notifications.previews.runs.create(
+ id="id",
+ )
+ assert_matches_type(PreviewRun, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Courier) -> None:
+ run = client.notifications.previews.runs.create(
+ id="id",
+ data={"foo": "bar"},
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ device_set_id="device_set_id",
+ locale="locale",
+ template_version="draft",
+ idempotency_key="order-ORD-456-user-123",
+ x_idempotency_expiration="1785312000",
+ )
+ assert_matches_type(PreviewRun, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Courier) -> None:
+ response = client.notifications.previews.runs.with_raw_response.create(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = response.parse()
+ assert_matches_type(PreviewRun, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Courier) -> None:
+ with client.notifications.previews.runs.with_streaming_response.create(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = response.parse()
+ assert_matches_type(PreviewRun, run, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_create(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.notifications.previews.runs.with_raw_response.create(
+ id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Courier) -> None:
+ run = client.notifications.previews.runs.retrieve(
+ preview_run_id="previewRunId",
+ id="id",
+ )
+ assert_matches_type(PreviewRunDetail, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Courier) -> None:
+ response = client.notifications.previews.runs.with_raw_response.retrieve(
+ preview_run_id="previewRunId",
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = response.parse()
+ assert_matches_type(PreviewRunDetail, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Courier) -> None:
+ with client.notifications.previews.runs.with_streaming_response.retrieve(
+ preview_run_id="previewRunId",
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = response.parse()
+ assert_matches_type(PreviewRunDetail, run, 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 `id` but received ''"):
+ client.notifications.previews.runs.with_raw_response.retrieve(
+ preview_run_id="previewRunId",
+ id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `preview_run_id` but received ''"):
+ client.notifications.previews.runs.with_raw_response.retrieve(
+ preview_run_id="",
+ id="id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Courier) -> None:
+ run = client.notifications.previews.runs.list(
+ id="id",
+ )
+ assert_matches_type(PreviewRunListResponse, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Courier) -> None:
+ run = client.notifications.previews.runs.list(
+ id="id",
+ cursor="cursor",
+ limit=1,
+ )
+ assert_matches_type(PreviewRunListResponse, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Courier) -> None:
+ response = client.notifications.previews.runs.with_raw_response.list(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = response.parse()
+ assert_matches_type(PreviewRunListResponse, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Courier) -> None:
+ with client.notifications.previews.runs.with_streaming_response.list(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = response.parse()
+ assert_matches_type(PreviewRunListResponse, run, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.notifications.previews.runs.with_raw_response.list(
+ id="",
+ )
+
+
+class TestAsyncRuns:
+ 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:
+ run = await async_client.notifications.previews.runs.create(
+ id="id",
+ )
+ assert_matches_type(PreviewRun, run, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncCourier) -> None:
+ run = await async_client.notifications.previews.runs.create(
+ id="id",
+ data={"foo": "bar"},
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ device_set_id="device_set_id",
+ locale="locale",
+ template_version="draft",
+ idempotency_key="order-ORD-456-user-123",
+ x_idempotency_expiration="1785312000",
+ )
+ assert_matches_type(PreviewRun, run, 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.notifications.previews.runs.with_raw_response.create(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = await response.parse()
+ assert_matches_type(PreviewRun, run, 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.notifications.previews.runs.with_streaming_response.create(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = await response.parse()
+ assert_matches_type(PreviewRun, run, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.notifications.previews.runs.with_raw_response.create(
+ id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncCourier) -> None:
+ run = await async_client.notifications.previews.runs.retrieve(
+ preview_run_id="previewRunId",
+ id="id",
+ )
+ assert_matches_type(PreviewRunDetail, run, 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.notifications.previews.runs.with_raw_response.retrieve(
+ preview_run_id="previewRunId",
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = await response.parse()
+ assert_matches_type(PreviewRunDetail, run, 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.notifications.previews.runs.with_streaming_response.retrieve(
+ preview_run_id="previewRunId",
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = await response.parse()
+ assert_matches_type(PreviewRunDetail, run, 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 `id` but received ''"):
+ await async_client.notifications.previews.runs.with_raw_response.retrieve(
+ preview_run_id="previewRunId",
+ id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `preview_run_id` but received ''"):
+ await async_client.notifications.previews.runs.with_raw_response.retrieve(
+ preview_run_id="",
+ id="id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncCourier) -> None:
+ run = await async_client.notifications.previews.runs.list(
+ id="id",
+ )
+ assert_matches_type(PreviewRunListResponse, run, 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:
+ run = await async_client.notifications.previews.runs.list(
+ id="id",
+ cursor="cursor",
+ limit=1,
+ )
+ assert_matches_type(PreviewRunListResponse, run, 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.notifications.previews.runs.with_raw_response.list(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = await response.parse()
+ assert_matches_type(PreviewRunListResponse, run, 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.notifications.previews.runs.with_streaming_response.list(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = await response.parse()
+ assert_matches_type(PreviewRunListResponse, run, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.notifications.previews.runs.with_raw_response.list(
+ id="",
+ )
diff --git a/tests/api_resources/test_previews.py b/tests/api_resources/test_previews.py
new file mode 100644
index 00000000..ec870444
--- /dev/null
+++ b/tests/api_resources/test_previews.py
@@ -0,0 +1,482 @@
+# 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 (
+ DeviceSet,
+ DeviceSetListResponse,
+ PreviewDeviceListResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestPreviews:
+ 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_archive_device_set(self, client: Courier) -> None:
+ preview = client.previews.archive_device_set(
+ "deviceSetId",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_archive_device_set(self, client: Courier) -> None:
+ response = client.previews.with_raw_response.archive_device_set(
+ "deviceSetId",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_archive_device_set(self, client: Courier) -> None:
+ with client.previews.with_streaming_response.archive_device_set(
+ "deviceSetId",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, 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_device_set(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_set_id` but received ''"):
+ client.previews.with_raw_response.archive_device_set(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_device_set(self, client: Courier) -> None:
+ preview = client.previews.create_device_set(
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc"],
+ name="Mobile",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create_device_set(self, client: Courier) -> None:
+ response = client.previews.with_raw_response.create_device_set(
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc"],
+ name="Mobile",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create_device_set(self, client: Courier) -> None:
+ with client.previews.with_streaming_response.create_device_set(
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc"],
+ name="Mobile",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_device_sets(self, client: Courier) -> None:
+ preview = client.previews.list_device_sets()
+ assert_matches_type(DeviceSetListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_device_sets(self, client: Courier) -> None:
+ response = client.previews.with_raw_response.list_device_sets()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = response.parse()
+ assert_matches_type(DeviceSetListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_device_sets(self, client: Courier) -> None:
+ with client.previews.with_streaming_response.list_device_sets() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = response.parse()
+ assert_matches_type(DeviceSetListResponse, preview, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_devices(self, client: Courier) -> None:
+ preview = client.previews.list_devices()
+ assert_matches_type(PreviewDeviceListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_devices(self, client: Courier) -> None:
+ response = client.previews.with_raw_response.list_devices()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = response.parse()
+ assert_matches_type(PreviewDeviceListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_devices(self, client: Courier) -> None:
+ with client.previews.with_streaming_response.list_devices() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = response.parse()
+ assert_matches_type(PreviewDeviceListResponse, preview, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve_device_set(self, client: Courier) -> None:
+ preview = client.previews.retrieve_device_set(
+ "deviceSetId",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve_device_set(self, client: Courier) -> None:
+ response = client.previews.with_raw_response.retrieve_device_set(
+ "deviceSetId",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve_device_set(self, client: Courier) -> None:
+ with client.previews.with_streaming_response.retrieve_device_set(
+ "deviceSetId",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, 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_device_set(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_set_id` but received ''"):
+ client.previews.with_raw_response.retrieve_device_set(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_device_set(self, client: Courier) -> None:
+ preview = client.previews.update_device_set(
+ device_set_id="deviceSetId",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update_device_set(self, client: Courier) -> None:
+ response = client.previews.with_raw_response.update_device_set(
+ device_set_id="deviceSetId",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update_device_set(self, client: Courier) -> None:
+ with client.previews.with_streaming_response.update_device_set(
+ device_set_id="deviceSetId",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = response.parse()
+ assert_matches_type(DeviceSet, preview, 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_device_set(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_set_id` but received ''"):
+ client.previews.with_raw_response.update_device_set(
+ device_set_id="",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ )
+
+
+class TestAsyncPreviews:
+ 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_archive_device_set(self, async_client: AsyncCourier) -> None:
+ preview = await async_client.previews.archive_device_set(
+ "deviceSetId",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_archive_device_set(self, async_client: AsyncCourier) -> None:
+ response = await async_client.previews.with_raw_response.archive_device_set(
+ "deviceSetId",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_archive_device_set(self, async_client: AsyncCourier) -> None:
+ async with async_client.previews.with_streaming_response.archive_device_set(
+ "deviceSetId",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, 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_device_set(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_set_id` but received ''"):
+ await async_client.previews.with_raw_response.archive_device_set(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_device_set(self, async_client: AsyncCourier) -> None:
+ preview = await async_client.previews.create_device_set(
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc"],
+ name="Mobile",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create_device_set(self, async_client: AsyncCourier) -> None:
+ response = await async_client.previews.with_raw_response.create_device_set(
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc"],
+ name="Mobile",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create_device_set(self, async_client: AsyncCourier) -> None:
+ async with async_client.previews.with_streaming_response.create_device_set(
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc"],
+ name="Mobile",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_device_sets(self, async_client: AsyncCourier) -> None:
+ preview = await async_client.previews.list_device_sets()
+ assert_matches_type(DeviceSetListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_device_sets(self, async_client: AsyncCourier) -> None:
+ response = await async_client.previews.with_raw_response.list_device_sets()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = await response.parse()
+ assert_matches_type(DeviceSetListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_device_sets(self, async_client: AsyncCourier) -> None:
+ async with async_client.previews.with_streaming_response.list_device_sets() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = await response.parse()
+ assert_matches_type(DeviceSetListResponse, preview, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_devices(self, async_client: AsyncCourier) -> None:
+ preview = await async_client.previews.list_devices()
+ assert_matches_type(PreviewDeviceListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_devices(self, async_client: AsyncCourier) -> None:
+ response = await async_client.previews.with_raw_response.list_devices()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = await response.parse()
+ assert_matches_type(PreviewDeviceListResponse, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_devices(self, async_client: AsyncCourier) -> None:
+ async with async_client.previews.with_streaming_response.list_devices() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = await response.parse()
+ assert_matches_type(PreviewDeviceListResponse, preview, 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_device_set(self, async_client: AsyncCourier) -> None:
+ preview = await async_client.previews.retrieve_device_set(
+ "deviceSetId",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve_device_set(self, async_client: AsyncCourier) -> None:
+ response = await async_client.previews.with_raw_response.retrieve_device_set(
+ "deviceSetId",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve_device_set(self, async_client: AsyncCourier) -> None:
+ async with async_client.previews.with_streaming_response.retrieve_device_set(
+ "deviceSetId",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, 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_device_set(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_set_id` but received ''"):
+ await async_client.previews.with_raw_response.retrieve_device_set(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_device_set(self, async_client: AsyncCourier) -> None:
+ preview = await async_client.previews.update_device_set(
+ device_set_id="deviceSetId",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ )
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_update_device_set(self, async_client: AsyncCourier) -> None:
+ response = await async_client.previews.with_raw_response.update_device_set(
+ device_set_id="deviceSetId",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_update_device_set(self, async_client: AsyncCourier) -> None:
+ async with async_client.previews.with_streaming_response.update_device_set(
+ device_set_id="deviceSetId",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ preview = await response.parse()
+ assert_matches_type(DeviceSet, preview, 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_device_set(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_set_id` but received ''"):
+ await async_client.previews.with_raw_response.update_device_set(
+ device_set_id="",
+ device_ids=["pvd_1w6dgafr3aaycvv9a8bm996pkc", "pvd_34qvmj6p4dbqaa5mpys1ekt9jx"],
+ name="Mobile and desktop",
+ )