From bf0d01457d45affc8a6c5443d53927f78b9495b6 Mon Sep 17 00:00:00 2001 From: courier-codegen Date: Thu, 24 Sep 2026 19:55:51 +0000 Subject: [PATCH] feat(api): cross-client previews endpoints --- .stats.yml | 2 +- api.md | 49 ++ pyproject.toml | 2 +- src/courier/_client.py | 56 ++ src/courier/_version.py | 2 +- src/courier/resources/__init__.py | 14 + .../resources/notifications/__init__.py | 14 + .../resources/notifications/notifications.py | 32 + .../notifications/previews/__init__.py | 33 + .../notifications/previews/previews.py | 120 ++++ .../resources/notifications/previews/runs.py | 519 ++++++++++++++++ src/courier/resources/previews.py | 585 ++++++++++++++++++ src/courier/types/__init__.py | 6 + src/courier/types/device_set.py | 33 + src/courier/types/device_set_list_response.py | 14 + .../types/notifications/previews/__init__.py | 14 + .../notifications/previews/preview_result.py | 47 ++ .../previews/preview_result_failure_reason.py | 7 + .../previews/preview_result_status.py | 7 + .../notifications/previews/preview_run.py | 48 ++ .../previews/preview_run_detail.py | 52 ++ .../previews/preview_run_failure_reason.py | 9 + .../previews/preview_run_list_response.py | 17 + .../previews/preview_run_status.py | 7 + .../previews/run_create_params.py | 48 ++ .../notifications/previews/run_list_params.py | 16 + .../types/preview_create_device_set_params.py | 17 + src/courier/types/preview_device.py | 62 ++ .../types/preview_device_list_response.py | 14 + .../types/preview_update_device_set_params.py | 17 + .../notifications/previews/__init__.py | 1 + .../notifications/previews/test_runs.py | 350 +++++++++++ tests/api_resources/test_previews.py | 482 +++++++++++++++ 33 files changed, 2693 insertions(+), 3 deletions(-) create mode 100644 src/courier/resources/notifications/previews/__init__.py create mode 100644 src/courier/resources/notifications/previews/previews.py create mode 100644 src/courier/resources/notifications/previews/runs.py create mode 100644 src/courier/resources/previews.py create mode 100644 src/courier/types/device_set.py create mode 100644 src/courier/types/device_set_list_response.py create mode 100644 src/courier/types/notifications/previews/__init__.py create mode 100644 src/courier/types/notifications/previews/preview_result.py create mode 100644 src/courier/types/notifications/previews/preview_result_failure_reason.py create mode 100644 src/courier/types/notifications/previews/preview_result_status.py create mode 100644 src/courier/types/notifications/previews/preview_run.py create mode 100644 src/courier/types/notifications/previews/preview_run_detail.py create mode 100644 src/courier/types/notifications/previews/preview_run_failure_reason.py create mode 100644 src/courier/types/notifications/previews/preview_run_list_response.py create mode 100644 src/courier/types/notifications/previews/preview_run_status.py create mode 100644 src/courier/types/notifications/previews/run_create_params.py create mode 100644 src/courier/types/notifications/previews/run_list_params.py create mode 100644 src/courier/types/preview_create_device_set_params.py create mode 100644 src/courier/types/preview_device.py create mode 100644 src/courier/types/preview_device_list_response.py create mode 100644 src/courier/types/preview_update_device_set_params.py create mode 100644 tests/api_resources/notifications/previews/__init__.py create mode 100644 tests/api_resources/notifications/previews/test_runs.py create mode 100644 tests/api_resources/test_previews.py 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", + )