diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e150fdf4..8838e5b2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/courier-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
@@ -46,7 +46,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/courier-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
@@ -67,7 +67,7 @@ jobs:
github.repository == 'stainless-sdks/courier-python' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
- uses: actions/github-script@v8
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());
@@ -87,7 +87,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/courier-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index eebe3c0e..b4012590 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index e2505ec6..4205e012 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'trycourier/courier-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 46da4bba..a89bc43b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "7.11.2"
+ ".": "7.12.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index effa635e..17321d43 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 103
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-3f78581b4e078a1f620d9f587f18d77bcde6d20f56b0e4ae798648f4236494fb.yml
-openapi_spec_hash: 6bd33e0396d85e11bb46f0d549af93a3
-config_hash: afcc4f6f8c33ca3f338589e32e086f56
+configured_endpoints: 117
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-8e7ad3d889c555ff9c381518b627b24b85e3eb7376bdc3689adc7a96ec78e537.yml
+openapi_spec_hash: 53b3680aae719487c56efaa782bbe5b2
+config_hash: 10bd597dd6cc89023541bc551b6532b8
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb710bf7..695cc9fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
# Changelog
+## 7.12.0 (2026-05-19)
+
+Full Changelog: [v7.11.2...v7.12.0](https://github.com/trycourier/courier-python/compare/v7.11.2...v7.12.0)
+
+### Features
+
+* [C-18380] Journeys API reference: copy + naming cleanup ([974e8c8](https://github.com/trycourier/courier-python/commit/974e8c8bd203e9c497bdf4d7c1ecb5454e2acc7d))
+* [SUP-607] Add DELETE endpoint for Courier Create tenant templates ([3238616](https://github.com/trycourier/courier-python/commit/3238616527f879b97342e9ea9a4ac92ea99f4081))
+* **api:** add journey CRUD/versioning/templates endpoints, update types ([f352f50](https://github.com/trycourier/courier-python/commit/f352f50dde5c78a96ac7f12f1358d71dcc637ee3))
+* **internal/types:** support eagerly validating pydantic iterators ([87f6e77](https://github.com/trycourier/courier-python/commit/87f6e77c795a9eb8a0c51586401a9129ecc0a9f9))
+* support setting headers via env ([111bbaa](https://github.com/trycourier/courier-python/commit/111bbaa73c526cac005398950f3357631203fb4e))
+
+
+### Bug Fixes
+
+* **client:** add missing f-string prefix in file type error message ([ee15d9a](https://github.com/trycourier/courier-python/commit/ee15d9a08a7aa77c6daba0402545b3f9d7d95333))
+* use correct field name format for multipart file arrays ([4b98bee](https://github.com/trycourier/courier-python/commit/4b98beed2c666bebce1ddb8b1d8cccb68c40ffb6))
+
+
+### Performance Improvements
+
+* **client:** optimize file structure copying in multipart requests ([f33e194](https://github.com/trycourier/courier-python/commit/f33e194bd4cf761f56d37371b0a778b2bd72938b))
+
+
+### Chores
+
+* **internal:** more robust bootstrap script ([5be9e53](https://github.com/trycourier/courier-python/commit/5be9e537dd664f954d4a5348d8dc39f694972fb8))
+* **internal:** reformat pyproject.toml ([9d75a30](https://github.com/trycourier/courier-python/commit/9d75a3001d33be94d2942066c793ea828f84af0c))
+
## 7.11.2 (2026-04-14)
Full Changelog: [v7.11.1...v7.11.2](https://github.com/trycourier/courier-python/compare/v7.11.1...v7.11.2)
diff --git a/api.md b/api.md
index 7d5e528a..d5642933 100644
--- a/api.md
+++ b/api.md
@@ -197,7 +197,36 @@ Types:
```python
from courier.types import (
+ CreateJourneyRequest,
Journey,
+ JourneyAINode,
+ JourneyAPIInvokeTriggerNode,
+ JourneyConditionAtom,
+ JourneyConditionGroup,
+ JourneyConditionNestedGroup,
+ JourneyConditionsField,
+ JourneyDelayDurationNode,
+ JourneyDelayUntilNode,
+ JourneyExitNode,
+ JourneyFetchGetDeleteNode,
+ JourneyFetchPostPutNode,
+ JourneyMergeStrategy,
+ JourneyNode,
+ JourneyPublishRequest,
+ JourneyResponse,
+ JourneySegmentTriggerNode,
+ JourneySendNode,
+ JourneyState,
+ JourneyTemplateCreateRequest,
+ JourneyTemplateGetResponse,
+ JourneyTemplateListResponse,
+ JourneyTemplatePublishRequest,
+ JourneyTemplateReplaceRequest,
+ JourneyTemplateSummary,
+ JourneyThrottleDynamicNode,
+ JourneyThrottleStaticNode,
+ JourneyVersionItem,
+ JourneyVersionsListResponse,
JourneysInvokeRequest,
JourneysInvokeResponse,
JourneysListResponse,
@@ -206,8 +235,26 @@ from courier.types import (
Methods:
-- client.journeys.list(\*\*params) -> JourneysListResponse
-- client.journeys.invoke(template_id, \*\*params) -> JourneysInvokeResponse
+- client.journeys.create(\*\*params) -> JourneyResponse
+- client.journeys.retrieve(template_id, \*\*params) -> JourneyResponse
+- client.journeys.list(\*\*params) -> JourneysListResponse
+- client.journeys.archive(template_id) -> None
+- client.journeys.invoke(template_id, \*\*params) -> JourneysInvokeResponse
+- client.journeys.list_versions(template_id) -> JourneyVersionsListResponse
+- client.journeys.publish(template_id, \*\*params) -> JourneyResponse
+- client.journeys.replace(template_id, \*\*params) -> JourneyResponse
+
+## Templates
+
+Methods:
+
+- client.journeys.templates.create(template_id, \*\*params) -> JourneyTemplateGetResponse
+- client.journeys.templates.retrieve(notification_id, \*, template_id) -> JourneyTemplateGetResponse
+- client.journeys.templates.list(template_id, \*\*params) -> JourneyTemplateListResponse
+- client.journeys.templates.archive(notification_id, \*, template_id) -> None
+- client.journeys.templates.list_versions(notification_id, \*, template_id) -> NotificationTemplateVersionListResponse
+- client.journeys.templates.publish(notification_id, \*, template_id, \*\*params) -> None
+- client.journeys.templates.replace(notification_id, \*, template_id, \*\*params) -> JourneyTemplateGetResponse
# Brands
@@ -501,6 +548,7 @@ Methods:
- client.tenants.templates.retrieve(template_id, \*, tenant_id) -> BaseTemplateTenantAssociation
- client.tenants.templates.list(tenant_id, \*\*params) -> TemplateListResponse
+- client.tenants.templates.delete(template_id, \*, tenant_id) -> None
- client.tenants.templates.publish(template_id, \*, tenant_id, \*\*params) -> PostTenantTemplatePublishResponse
- client.tenants.templates.replace(template_id, \*, tenant_id, \*\*params) -> PutTenantTemplateResponse
diff --git a/pyproject.toml b/pyproject.toml
index 6402835a..9a1a8d93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "trycourier"
-version = "7.11.2"
+version = "7.12.0"
description = "The official Python library for the Courier API"
dynamic = ["readme"]
license = "Apache-2.0"
@@ -168,7 +168,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
-exclude = ['src/courier/_files.py', '_dev/.*.py', 'tests/.*']
+exclude = ["src/courier/_files.py", "_dev/.*.py", "tests/.*"]
strict_equality = true
implicit_reexport = true
diff --git a/scripts/bootstrap b/scripts/bootstrap
index b430fee3..fe8451e4 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
diff --git a/src/courier/_client.py b/src/courier/_client.py
index 752c4da2..30317ee0 100644
--- a/src/courier/_client.py
+++ b/src/courier/_client.py
@@ -19,7 +19,11 @@
RequestOptions,
not_given,
)
-from ._utils import is_given, get_async_library
+from ._utils import (
+ is_given,
+ is_mapping_t,
+ get_async_library,
+)
from ._compat import cached_property
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
@@ -57,7 +61,6 @@
from .resources.send import SendResource, AsyncSendResource
from .resources.brands import BrandsResource, AsyncBrandsResource
from .resources.inbound import InboundResource, AsyncInboundResource
- from .resources.journeys import JourneysResource, AsyncJourneysResource
from .resources.messages import MessagesResource, AsyncMessagesResource
from .resources.requests import RequestsResource, AsyncRequestsResource
from .resources.audiences import AudiencesResource, AsyncAudiencesResource
@@ -66,6 +69,7 @@
from .resources.audit_events import AuditEventsResource, AsyncAuditEventsResource
from .resources.translations import TranslationsResource, AsyncTranslationsResource
from .resources.tenants.tenants import TenantsResource, AsyncTenantsResource
+ from .resources.journeys.journeys import JourneysResource, AsyncJourneysResource
from .resources.profiles.profiles import ProfilesResource, AsyncProfilesResource
from .resources.routing_strategies import RoutingStrategiesResource, AsyncRoutingStrategiesResource
from .resources.providers.providers import ProvidersResource, AsyncProvidersResource
@@ -119,6 +123,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.courier.com"
+ custom_headers_env = os.environ.get("COURIER_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
@@ -401,6 +414,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.courier.com"
+ custom_headers_env = os.environ.get("COURIER_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
diff --git a/src/courier/_files.py b/src/courier/_files.py
index cc14c14f..76da9e08 100644
--- a/src/courier/_files.py
+++ b/src/courier/_files.py
@@ -3,8 +3,8 @@
import io
import os
import pathlib
-from typing import overload
-from typing_extensions import TypeGuard
+from typing import Sequence, cast, overload
+from typing_extensions import TypeVar, TypeGuard
import anyio
@@ -17,7 +17,9 @@
HttpxFileContent,
HttpxRequestFiles,
)
-from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
+from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
+
+_T = TypeVar("_T")
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -97,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
- raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
+ raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
return files
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
return await anyio.Path(file).read_bytes()
return file
+
+
+def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
+ """Copy only the containers along the given paths.
+
+ Used to guard against mutation by extract_files without copying the entire structure.
+ Only dicts and lists that lie on a path are copied; everything else
+ is returned by reference.
+
+ For example, given paths=[["foo", "files", "file"]] and the structure:
+ {
+ "foo": {
+ "bar": {"baz": {}},
+ "files": {"file": }
+ }
+ }
+ The root dict, "foo", and "files" are copied (they lie on the path).
+ "bar" and "baz" are returned by reference (off the path).
+ """
+ return _deepcopy_with_paths(item, paths, 0)
+
+
+def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
+ if not paths:
+ return item
+ if is_mapping(item):
+ key_to_paths: dict[str, list[Sequence[str]]] = {}
+ for path in paths:
+ if index < len(path):
+ key_to_paths.setdefault(path[index], []).append(path)
+
+ # if no path continues through this mapping, it won't be mutated and copying it is redundant
+ if not key_to_paths:
+ return item
+
+ result = dict(item)
+ for key, subpaths in key_to_paths.items():
+ if key in result:
+ result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
+ return cast(_T, result)
+ if is_list(item):
+ array_paths = [path for path in paths if index < len(path) and path[index] == ""]
+
+ # if no path expects a list here, nothing will be mutated inside it - return by reference
+ if not array_paths:
+ return cast(_T, item)
+ return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
+ return item
diff --git a/src/courier/_models.py b/src/courier/_models.py
index 29070e05..8c5ab260 100644
--- a/src/courier/_models.py
+++ b/src/courier/_models.py
@@ -25,7 +25,9 @@
ClassVar,
Protocol,
Required,
+ Annotated,
ParamSpec,
+ TypeAlias,
TypedDict,
TypeGuard,
final,
@@ -79,7 +81,15 @@
from ._constants import RAW_RESPONSE_HEADER
if TYPE_CHECKING:
+ from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
+ from pydantic_core import CoreSchema, core_schema
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
+else:
+ try:
+ from pydantic_core import CoreSchema, core_schema
+ except ImportError:
+ CoreSchema = None
+ core_schema = None
__all__ = ["BaseModel", "GenericModel"]
@@ -396,6 +406,76 @@ def model_dump_json(
)
+class _EagerIterable(list[_T], Generic[_T]):
+ """
+ Accepts any Iterable[T] input (including generators), consumes it
+ eagerly, and validates all items upfront.
+
+ Validation preserves the original container type where possible
+ (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON)
+ always emits a list — round-tripping through model_dump() will not
+ restore the original container type.
+ """
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls,
+ source_type: Any,
+ handler: GetCoreSchemaHandler,
+ ) -> CoreSchema:
+ (item_type,) = get_args(source_type) or (Any,)
+ item_schema: CoreSchema = handler.generate_schema(item_type)
+ list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema)
+
+ return core_schema.no_info_wrap_validator_function(
+ cls._validate,
+ list_of_items_schema,
+ serialization=core_schema.plain_serializer_function_ser_schema(
+ cls._serialize,
+ info_arg=False,
+ ),
+ )
+
+ @staticmethod
+ def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any:
+ original_type: type[Any] = type(v)
+
+ # Normalize to list so list_schema can validate each item
+ if isinstance(v, list):
+ items: list[_T] = v
+ else:
+ try:
+ items = list(v)
+ except TypeError as e:
+ raise TypeError("Value is not iterable") from e
+
+ # Validate items against the inner schema
+ validated: list[_T] = handler(items)
+
+ # Reconstruct original container type
+ if original_type is list:
+ return validated
+ # str(list) produces the list's repr, not a string built from items,
+ # so skip reconstruction for str and its subclasses.
+ if issubclass(original_type, str):
+ return validated
+ try:
+ return original_type(validated)
+ except (TypeError, ValueError):
+ # If the type cannot be reconstructed, just return the validated list
+ return validated
+
+ @staticmethod
+ def _serialize(v: Iterable[_T]) -> list[_T]:
+ """Always serialize as a list so Pydantic's JSON encoder is happy."""
+ if isinstance(v, list):
+ return v
+ return list(v)
+
+
+EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable]
+
+
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if value is None:
return field_get_default(field)
diff --git a/src/courier/_qs.py b/src/courier/_qs.py
index de8c99bc..4127c19c 100644
--- a/src/courier/_qs.py
+++ b/src/courier/_qs.py
@@ -2,17 +2,13 @@
from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
-from typing_extensions import Literal, get_args
+from typing_extensions import get_args
-from ._types import NotGiven, not_given
+from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
from ._utils import flatten
_T = TypeVar("_T")
-
-ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
-NestedFormat = Literal["dots", "brackets"]
-
PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
diff --git a/src/courier/_types.py b/src/courier/_types.py
index e92110d7..512a4f11 100644
--- a/src/courier/_types.py
+++ b/src/courier/_types.py
@@ -47,6 +47,9 @@
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
+ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
+NestedFormat = Literal["dots", "brackets"]
+
# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
diff --git a/src/courier/_utils/__init__.py b/src/courier/_utils/__init__.py
index 10cb66d2..1c090e51 100644
--- a/src/courier/_utils/__init__.py
+++ b/src/courier/_utils/__init__.py
@@ -24,7 +24,6 @@
coerce_integer as coerce_integer,
file_from_path as file_from_path,
strip_not_given as strip_not_given,
- deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
maybe_coerce_float as maybe_coerce_float,
get_required_header as get_required_header,
diff --git a/src/courier/_utils/_utils.py b/src/courier/_utils/_utils.py
index 63b8cd60..199cd231 100644
--- a/src/courier/_utils/_utils.py
+++ b/src/courier/_utils/_utils.py
@@ -17,11 +17,11 @@
)
from pathlib import Path
from datetime import date, datetime
-from typing_extensions import TypeGuard
+from typing_extensions import TypeGuard, get_args
import sniffio
-from .._types import Omit, NotGiven, FileTypes, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -40,25 +40,45 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
+ array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
A path may look like this ['foo', 'files', '', 'data'].
+ ``array_format`` controls how ```` segments contribute to the emitted
+ field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
+
Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
- files.extend(_extract_items(query, path, index=0, flattened_key=None))
+ files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
return files
+def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
+ if array_format == "brackets":
+ return "[]"
+ if array_format == "indices":
+ return f"[{array_index}]"
+ if array_format == "repeat" or array_format == "comma":
+ # Both repeat the bare field name for each file part; there is no
+ # meaningful way to comma-join binary parts.
+ return ""
+ raise NotImplementedError(
+ f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
+ )
+
+
def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
+ array_format: ArrayFormat,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
@@ -75,9 +95,11 @@ def _extract_items(
if is_list(obj):
files: list[tuple[str, FileTypes]] = []
- for entry in obj:
- assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
- files.append((flattened_key + "[]", cast(FileTypes, entry)))
+ for array_index, entry in enumerate(obj):
+ suffix = _array_suffix(array_format, array_index)
+ emitted_key = (flattened_key + suffix) if flattened_key else suffix
+ assert_is_file_content(entry, key=emitted_key)
+ files.append((emitted_key, cast(FileTypes, entry)))
return files
assert_is_file_content(obj, key=flattened_key)
@@ -106,6 +128,7 @@ def _extract_items(
path,
index=index,
flattened_key=flattened_key,
+ array_format=array_format,
)
elif is_list(obj):
if key != "":
@@ -117,9 +140,12 @@ def _extract_items(
item,
path,
index=index,
- flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
+ flattened_key=(
+ (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
+ ),
+ array_format=array_format,
)
- for item in obj
+ for array_index, item in enumerate(obj)
]
)
@@ -177,21 +203,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
return isinstance(obj, Iterable)
-def deepcopy_minimal(item: _T) -> _T:
- """Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
-
- - mappings, e.g. `dict`
- - list
-
- This is done for performance reasons.
- """
- if is_mapping(item):
- return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
- if is_list(item):
- return cast(_T, [deepcopy_minimal(entry) for entry in item])
- return item
-
-
# copied from https://github.com/Rapptz/RoboDanny
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
size = len(seq)
diff --git a/src/courier/_version.py b/src/courier/_version.py
index 73243ee4..9e5ed1e7 100644
--- a/src/courier/_version.py
+++ b/src/courier/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "courier"
-__version__ = "7.11.2" # x-release-please-version
+__version__ = "7.12.0" # x-release-please-version
diff --git a/src/courier/resources/journeys.py b/src/courier/resources/journeys.py
deleted file mode 100644
index 15b5139e..00000000
--- a/src/courier/resources/journeys.py
+++ /dev/null
@@ -1,331 +0,0 @@
-# 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 Literal
-
-import httpx
-
-from ..types import journey_list_params, journey_invoke_params
-from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from .._utils import path_template, maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from .._base_client import make_request_options
-from ..types.journeys_list_response import JourneysListResponse
-from ..types.journeys_invoke_response import JourneysInvokeResponse
-
-__all__ = ["JourneysResource", "AsyncJourneysResource"]
-
-
-class JourneysResource(SyncAPIResource):
- @cached_property
- def with_raw_response(self) -> JourneysResourceWithRawResponse:
- """
- 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 JourneysResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> JourneysResourceWithStreamingResponse:
- """
- 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 JourneysResourceWithStreamingResponse(self)
-
- def list(
- self,
- *,
- cursor: str | Omit = omit,
- version: Literal["published", "draft"] | 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,
- ) -> JourneysListResponse:
- """Get the list of journeys.
-
- Args:
- cursor: A cursor token for pagination.
-
- Use the cursor from the previous response to
- fetch the next page of results.
-
- version: The version of journeys to retrieve. Accepted values are published (for
- published journeys) or draft (for draft journeys). Defaults to 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
- """
- return self._get(
- "/journeys",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "cursor": cursor,
- "version": version,
- },
- journey_list_params.JourneyListParams,
- ),
- ),
- cast_to=JourneysListResponse,
- )
-
- def invoke(
- self,
- template_id: str,
- *,
- data: Dict[str, object] | Omit = omit,
- profile: Dict[str, object] | Omit = omit,
- user_id: 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,
- ) -> JourneysInvokeResponse:
- """
- Invoke a journey run from a journey template.
-
- Args:
- data: Data payload passed to the journey. The expected shape can be predefined using
- the schema builder in the journey editor. This data is available in journey
- steps for condition evaluation and template variable interpolation. Can also
- contain user identifiers (user_id, userId, anonymousId) if not provided
- elsewhere.
-
- profile: Profile data for the user. Can contain contact information (email,
- phone_number), user identifiers (user_id, userId, anonymousId), or any custom
- profile fields. Profile fields are merged with any existing stored profile for
- the user. Include context.tenant_id to load a tenant-scoped profile for
- multi-tenant scenarios.
-
- user_id: A unique identifier for the user. If not provided, the system will attempt to
- resolve the user identifier from profile or data objects.
-
- 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 template_id:
- raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
- return self._post(
- path_template("/journeys/{template_id}/invoke", template_id=template_id),
- body=maybe_transform(
- {
- "data": data,
- "profile": profile,
- "user_id": user_id,
- },
- journey_invoke_params.JourneyInvokeParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=JourneysInvokeResponse,
- )
-
-
-class AsyncJourneysResource(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncJourneysResourceWithRawResponse:
- """
- 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 AsyncJourneysResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncJourneysResourceWithStreamingResponse:
- """
- 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 AsyncJourneysResourceWithStreamingResponse(self)
-
- async def list(
- self,
- *,
- cursor: str | Omit = omit,
- version: Literal["published", "draft"] | 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,
- ) -> JourneysListResponse:
- """Get the list of journeys.
-
- Args:
- cursor: A cursor token for pagination.
-
- Use the cursor from the previous response to
- fetch the next page of results.
-
- version: The version of journeys to retrieve. Accepted values are published (for
- published journeys) or draft (for draft journeys). Defaults to 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
- """
- return await self._get(
- "/journeys",
- 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,
- "version": version,
- },
- journey_list_params.JourneyListParams,
- ),
- ),
- cast_to=JourneysListResponse,
- )
-
- async def invoke(
- self,
- template_id: str,
- *,
- data: Dict[str, object] | Omit = omit,
- profile: Dict[str, object] | Omit = omit,
- user_id: 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,
- ) -> JourneysInvokeResponse:
- """
- Invoke a journey run from a journey template.
-
- Args:
- data: Data payload passed to the journey. The expected shape can be predefined using
- the schema builder in the journey editor. This data is available in journey
- steps for condition evaluation and template variable interpolation. Can also
- contain user identifiers (user_id, userId, anonymousId) if not provided
- elsewhere.
-
- profile: Profile data for the user. Can contain contact information (email,
- phone_number), user identifiers (user_id, userId, anonymousId), or any custom
- profile fields. Profile fields are merged with any existing stored profile for
- the user. Include context.tenant_id to load a tenant-scoped profile for
- multi-tenant scenarios.
-
- user_id: A unique identifier for the user. If not provided, the system will attempt to
- resolve the user identifier from profile or data objects.
-
- 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 template_id:
- raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
- return await self._post(
- path_template("/journeys/{template_id}/invoke", template_id=template_id),
- body=await async_maybe_transform(
- {
- "data": data,
- "profile": profile,
- "user_id": user_id,
- },
- journey_invoke_params.JourneyInvokeParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=JourneysInvokeResponse,
- )
-
-
-class JourneysResourceWithRawResponse:
- def __init__(self, journeys: JourneysResource) -> None:
- self._journeys = journeys
-
- self.list = to_raw_response_wrapper(
- journeys.list,
- )
- self.invoke = to_raw_response_wrapper(
- journeys.invoke,
- )
-
-
-class AsyncJourneysResourceWithRawResponse:
- def __init__(self, journeys: AsyncJourneysResource) -> None:
- self._journeys = journeys
-
- self.list = async_to_raw_response_wrapper(
- journeys.list,
- )
- self.invoke = async_to_raw_response_wrapper(
- journeys.invoke,
- )
-
-
-class JourneysResourceWithStreamingResponse:
- def __init__(self, journeys: JourneysResource) -> None:
- self._journeys = journeys
-
- self.list = to_streamed_response_wrapper(
- journeys.list,
- )
- self.invoke = to_streamed_response_wrapper(
- journeys.invoke,
- )
-
-
-class AsyncJourneysResourceWithStreamingResponse:
- def __init__(self, journeys: AsyncJourneysResource) -> None:
- self._journeys = journeys
-
- self.list = async_to_streamed_response_wrapper(
- journeys.list,
- )
- self.invoke = async_to_streamed_response_wrapper(
- journeys.invoke,
- )
diff --git a/src/courier/resources/journeys/__init__.py b/src/courier/resources/journeys/__init__.py
new file mode 100644
index 00000000..f705015d
--- /dev/null
+++ b/src/courier/resources/journeys/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .journeys import (
+ JourneysResource,
+ AsyncJourneysResource,
+ JourneysResourceWithRawResponse,
+ AsyncJourneysResourceWithRawResponse,
+ JourneysResourceWithStreamingResponse,
+ AsyncJourneysResourceWithStreamingResponse,
+)
+from .templates import (
+ TemplatesResource,
+ AsyncTemplatesResource,
+ TemplatesResourceWithRawResponse,
+ AsyncTemplatesResourceWithRawResponse,
+ TemplatesResourceWithStreamingResponse,
+ AsyncTemplatesResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "TemplatesResource",
+ "AsyncTemplatesResource",
+ "TemplatesResourceWithRawResponse",
+ "AsyncTemplatesResourceWithRawResponse",
+ "TemplatesResourceWithStreamingResponse",
+ "AsyncTemplatesResourceWithStreamingResponse",
+ "JourneysResource",
+ "AsyncJourneysResource",
+ "JourneysResourceWithRawResponse",
+ "AsyncJourneysResourceWithRawResponse",
+ "JourneysResourceWithStreamingResponse",
+ "AsyncJourneysResourceWithStreamingResponse",
+]
diff --git a/src/courier/resources/journeys/journeys.py b/src/courier/resources/journeys/journeys.py
new file mode 100644
index 00000000..255d21d4
--- /dev/null
+++ b/src/courier/resources/journeys/journeys.py
@@ -0,0 +1,957 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Iterable
+from typing_extensions import Literal
+
+import httpx
+
+from ...types import (
+ JourneyState,
+ journey_list_params,
+ journey_create_params,
+ journey_invoke_params,
+ journey_publish_params,
+ journey_replace_params,
+ journey_retrieve_params,
+)
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from .templates import (
+ TemplatesResource,
+ AsyncTemplatesResource,
+ TemplatesResourceWithRawResponse,
+ AsyncTemplatesResourceWithRawResponse,
+ TemplatesResourceWithStreamingResponse,
+ AsyncTemplatesResourceWithStreamingResponse,
+)
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.journey_state import JourneyState
+from ...types.journey_response import JourneyResponse
+from ...types.journey_node_param import JourneyNodeParam
+from ...types.journeys_list_response import JourneysListResponse
+from ...types.journeys_invoke_response import JourneysInvokeResponse
+from ...types.journey_versions_list_response import JourneyVersionsListResponse
+
+__all__ = ["JourneysResource", "AsyncJourneysResource"]
+
+
+class JourneysResource(SyncAPIResource):
+ @cached_property
+ def templates(self) -> TemplatesResource:
+ return TemplatesResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> JourneysResourceWithRawResponse:
+ """
+ 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 JourneysResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> JourneysResourceWithStreamingResponse:
+ """
+ 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 JourneysResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ name: str,
+ nodes: Iterable[JourneyNodeParam],
+ enabled: bool | Omit = omit,
+ state: JourneyState | 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,
+ ) -> JourneyResponse:
+ """Create a journey.
+
+ Defaults to `DRAFT` state; pass `state: "PUBLISHED"` to
+ publish on create. Send nodes are not allowed on `POST`. The standard flow is:
+ create the journey shell here, add notification templates with
+ `POST /journeys/{templateId}/templates`, then wire them into the journey with
+ `PUT /journeys/{templateId}`. Call `POST /journeys/{templateId}/publish` to
+ publish a draft after the fact.
+
+ Args:
+ state: Lifecycle state of a journey.
+
+ 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(
+ "/journeys",
+ body=maybe_transform(
+ {
+ "name": name,
+ "nodes": nodes,
+ "enabled": enabled,
+ "state": state,
+ },
+ journey_create_params.JourneyCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyResponse,
+ )
+
+ def retrieve(
+ self,
+ template_id: str,
+ *,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyResponse:
+ """Fetch a journey by id.
+
+ Pass `?version=draft` (default `published`) to retrieve
+ the working draft, or `?version=vN` to retrieve a historical version.
+
+ Args:
+ version: Version selector: `draft`, `published` (default), or `vN`.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._get(
+ path_template("/journeys/{template_id}", template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"version": version}, journey_retrieve_params.JourneyRetrieveParams),
+ ),
+ cast_to=JourneyResponse,
+ )
+
+ def list(
+ self,
+ *,
+ cursor: str | Omit = omit,
+ version: Literal["published", "draft"] | 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,
+ ) -> JourneysListResponse:
+ """Get the list of journeys.
+
+ Args:
+ cursor: A cursor token for pagination.
+
+ Use the cursor from the previous response to
+ fetch the next page of results.
+
+ version: The version of journeys to retrieve. Accepted values are published (for
+ published journeys) or draft (for draft journeys). Defaults to 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
+ """
+ return self._get(
+ "/journeys",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "cursor": cursor,
+ "version": version,
+ },
+ journey_list_params.JourneyListParams,
+ ),
+ ),
+ cast_to=JourneysListResponse,
+ )
+
+ def archive(
+ self,
+ template_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Archive a journey.
+
+ Archived journeys cannot be invoked. Existing journey runs
+ continue to completion.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/journeys/{template_id}", template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def invoke(
+ self,
+ template_id: str,
+ *,
+ data: Dict[str, object] | Omit = omit,
+ profile: Dict[str, object] | Omit = omit,
+ user_id: 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,
+ ) -> JourneysInvokeResponse:
+ """Invoke a journey by id or alias to start a new run.
+
+ The response includes a
+ `runId` identifying the run.
+
+ Args:
+ data: Data payload passed to the journey. The expected shape can be predefined using
+ the schema builder in the journey editor. This data is available in journey
+ steps for condition evaluation and template variable interpolation. Can also
+ contain user identifiers (user_id, userId, anonymousId) if not provided
+ elsewhere.
+
+ profile: Profile data for the user. Can contain contact information (email,
+ phone_number), user identifiers (user_id, userId, anonymousId), or any custom
+ profile fields. Profile fields are merged with any existing stored profile for
+ the user. Include context.tenant_id to load a tenant-scoped profile for
+ multi-tenant scenarios.
+
+ user_id: A unique identifier for the user. If not provided, the system will attempt to
+ resolve the user identifier from profile or data objects.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._post(
+ path_template("/journeys/{template_id}/invoke", template_id=template_id),
+ body=maybe_transform(
+ {
+ "data": data,
+ "profile": profile,
+ "user_id": user_id,
+ },
+ journey_invoke_params.JourneyInvokeParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneysInvokeResponse,
+ )
+
+ def list_versions(
+ self,
+ template_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyVersionsListResponse:
+ """
+ List published versions of a journey, ordered most recent first.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._get(
+ path_template("/journeys/{template_id}/versions", template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyVersionsListResponse,
+ )
+
+ def publish(
+ self,
+ template_id: str,
+ *,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyResponse:
+ """Publish the current draft as a new version.
+
+ Body is optional; pass
+ `{ "version": "vN" }` to roll back to a prior version instead. Returns 404 if
+ the journey has no draft to publish.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._post(
+ path_template("/journeys/{template_id}/publish", template_id=template_id),
+ body=maybe_transform({"version": version}, journey_publish_params.JourneyPublishParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyResponse,
+ )
+
+ def replace(
+ self,
+ template_id: str,
+ *,
+ name: str,
+ nodes: Iterable[JourneyNodeParam],
+ enabled: bool | Omit = omit,
+ state: JourneyState | 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,
+ ) -> JourneyResponse:
+ """Replace the journey draft.
+
+ Updates the working draft only; call
+ `POST /journeys/{templateId}/publish` to make it live, or pass
+ `state: "PUBLISHED"` in this request to publish immediately. Send-node
+ `template` ids must already exist and be scoped to this journey, and node ids
+ must not be claimed by another journey.
+
+ Args:
+ state: Lifecycle state of a journey.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._put(
+ path_template("/journeys/{template_id}", template_id=template_id),
+ body=maybe_transform(
+ {
+ "name": name,
+ "nodes": nodes,
+ "enabled": enabled,
+ "state": state,
+ },
+ journey_replace_params.JourneyReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyResponse,
+ )
+
+
+class AsyncJourneysResource(AsyncAPIResource):
+ @cached_property
+ def templates(self) -> AsyncTemplatesResource:
+ return AsyncTemplatesResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncJourneysResourceWithRawResponse:
+ """
+ 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 AsyncJourneysResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncJourneysResourceWithStreamingResponse:
+ """
+ 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 AsyncJourneysResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ name: str,
+ nodes: Iterable[JourneyNodeParam],
+ enabled: bool | Omit = omit,
+ state: JourneyState | 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,
+ ) -> JourneyResponse:
+ """Create a journey.
+
+ Defaults to `DRAFT` state; pass `state: "PUBLISHED"` to
+ publish on create. Send nodes are not allowed on `POST`. The standard flow is:
+ create the journey shell here, add notification templates with
+ `POST /journeys/{templateId}/templates`, then wire them into the journey with
+ `PUT /journeys/{templateId}`. Call `POST /journeys/{templateId}/publish` to
+ publish a draft after the fact.
+
+ Args:
+ state: Lifecycle state of a journey.
+
+ 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(
+ "/journeys",
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "nodes": nodes,
+ "enabled": enabled,
+ "state": state,
+ },
+ journey_create_params.JourneyCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyResponse,
+ )
+
+ async def retrieve(
+ self,
+ template_id: str,
+ *,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyResponse:
+ """Fetch a journey by id.
+
+ Pass `?version=draft` (default `published`) to retrieve
+ the working draft, or `?version=vN` to retrieve a historical version.
+
+ Args:
+ version: Version selector: `draft`, `published` (default), or `vN`.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._get(
+ path_template("/journeys/{template_id}", template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"version": version}, journey_retrieve_params.JourneyRetrieveParams),
+ ),
+ cast_to=JourneyResponse,
+ )
+
+ async def list(
+ self,
+ *,
+ cursor: str | Omit = omit,
+ version: Literal["published", "draft"] | 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,
+ ) -> JourneysListResponse:
+ """Get the list of journeys.
+
+ Args:
+ cursor: A cursor token for pagination.
+
+ Use the cursor from the previous response to
+ fetch the next page of results.
+
+ version: The version of journeys to retrieve. Accepted values are published (for
+ published journeys) or draft (for draft journeys). Defaults to 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
+ """
+ return await self._get(
+ "/journeys",
+ 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,
+ "version": version,
+ },
+ journey_list_params.JourneyListParams,
+ ),
+ ),
+ cast_to=JourneysListResponse,
+ )
+
+ async def archive(
+ self,
+ template_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Archive a journey.
+
+ Archived journeys cannot be invoked. Existing journey runs
+ continue to completion.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/journeys/{template_id}", template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def invoke(
+ self,
+ template_id: str,
+ *,
+ data: Dict[str, object] | Omit = omit,
+ profile: Dict[str, object] | Omit = omit,
+ user_id: 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,
+ ) -> JourneysInvokeResponse:
+ """Invoke a journey by id or alias to start a new run.
+
+ The response includes a
+ `runId` identifying the run.
+
+ Args:
+ data: Data payload passed to the journey. The expected shape can be predefined using
+ the schema builder in the journey editor. This data is available in journey
+ steps for condition evaluation and template variable interpolation. Can also
+ contain user identifiers (user_id, userId, anonymousId) if not provided
+ elsewhere.
+
+ profile: Profile data for the user. Can contain contact information (email,
+ phone_number), user identifiers (user_id, userId, anonymousId), or any custom
+ profile fields. Profile fields are merged with any existing stored profile for
+ the user. Include context.tenant_id to load a tenant-scoped profile for
+ multi-tenant scenarios.
+
+ user_id: A unique identifier for the user. If not provided, the system will attempt to
+ resolve the user identifier from profile or data objects.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._post(
+ path_template("/journeys/{template_id}/invoke", template_id=template_id),
+ body=await async_maybe_transform(
+ {
+ "data": data,
+ "profile": profile,
+ "user_id": user_id,
+ },
+ journey_invoke_params.JourneyInvokeParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneysInvokeResponse,
+ )
+
+ async def list_versions(
+ self,
+ template_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyVersionsListResponse:
+ """
+ List published versions of a journey, ordered most recent first.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._get(
+ path_template("/journeys/{template_id}/versions", template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyVersionsListResponse,
+ )
+
+ async def publish(
+ self,
+ template_id: str,
+ *,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyResponse:
+ """Publish the current draft as a new version.
+
+ Body is optional; pass
+ `{ "version": "vN" }` to roll back to a prior version instead. Returns 404 if
+ the journey has no draft to publish.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._post(
+ path_template("/journeys/{template_id}/publish", template_id=template_id),
+ body=await async_maybe_transform({"version": version}, journey_publish_params.JourneyPublishParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyResponse,
+ )
+
+ async def replace(
+ self,
+ template_id: str,
+ *,
+ name: str,
+ nodes: Iterable[JourneyNodeParam],
+ enabled: bool | Omit = omit,
+ state: JourneyState | 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,
+ ) -> JourneyResponse:
+ """Replace the journey draft.
+
+ Updates the working draft only; call
+ `POST /journeys/{templateId}/publish` to make it live, or pass
+ `state: "PUBLISHED"` in this request to publish immediately. Send-node
+ `template` ids must already exist and be scoped to this journey, and node ids
+ must not be claimed by another journey.
+
+ Args:
+ state: Lifecycle state of a journey.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._put(
+ path_template("/journeys/{template_id}", template_id=template_id),
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "nodes": nodes,
+ "enabled": enabled,
+ "state": state,
+ },
+ journey_replace_params.JourneyReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyResponse,
+ )
+
+
+class JourneysResourceWithRawResponse:
+ def __init__(self, journeys: JourneysResource) -> None:
+ self._journeys = journeys
+
+ self.create = to_raw_response_wrapper(
+ journeys.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ journeys.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ journeys.list,
+ )
+ self.archive = to_raw_response_wrapper(
+ journeys.archive,
+ )
+ self.invoke = to_raw_response_wrapper(
+ journeys.invoke,
+ )
+ self.list_versions = to_raw_response_wrapper(
+ journeys.list_versions,
+ )
+ self.publish = to_raw_response_wrapper(
+ journeys.publish,
+ )
+ self.replace = to_raw_response_wrapper(
+ journeys.replace,
+ )
+
+ @cached_property
+ def templates(self) -> TemplatesResourceWithRawResponse:
+ return TemplatesResourceWithRawResponse(self._journeys.templates)
+
+
+class AsyncJourneysResourceWithRawResponse:
+ def __init__(self, journeys: AsyncJourneysResource) -> None:
+ self._journeys = journeys
+
+ self.create = async_to_raw_response_wrapper(
+ journeys.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ journeys.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ journeys.list,
+ )
+ self.archive = async_to_raw_response_wrapper(
+ journeys.archive,
+ )
+ self.invoke = async_to_raw_response_wrapper(
+ journeys.invoke,
+ )
+ self.list_versions = async_to_raw_response_wrapper(
+ journeys.list_versions,
+ )
+ self.publish = async_to_raw_response_wrapper(
+ journeys.publish,
+ )
+ self.replace = async_to_raw_response_wrapper(
+ journeys.replace,
+ )
+
+ @cached_property
+ def templates(self) -> AsyncTemplatesResourceWithRawResponse:
+ return AsyncTemplatesResourceWithRawResponse(self._journeys.templates)
+
+
+class JourneysResourceWithStreamingResponse:
+ def __init__(self, journeys: JourneysResource) -> None:
+ self._journeys = journeys
+
+ self.create = to_streamed_response_wrapper(
+ journeys.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ journeys.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ journeys.list,
+ )
+ self.archive = to_streamed_response_wrapper(
+ journeys.archive,
+ )
+ self.invoke = to_streamed_response_wrapper(
+ journeys.invoke,
+ )
+ self.list_versions = to_streamed_response_wrapper(
+ journeys.list_versions,
+ )
+ self.publish = to_streamed_response_wrapper(
+ journeys.publish,
+ )
+ self.replace = to_streamed_response_wrapper(
+ journeys.replace,
+ )
+
+ @cached_property
+ def templates(self) -> TemplatesResourceWithStreamingResponse:
+ return TemplatesResourceWithStreamingResponse(self._journeys.templates)
+
+
+class AsyncJourneysResourceWithStreamingResponse:
+ def __init__(self, journeys: AsyncJourneysResource) -> None:
+ self._journeys = journeys
+
+ self.create = async_to_streamed_response_wrapper(
+ journeys.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ journeys.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ journeys.list,
+ )
+ self.archive = async_to_streamed_response_wrapper(
+ journeys.archive,
+ )
+ self.invoke = async_to_streamed_response_wrapper(
+ journeys.invoke,
+ )
+ self.list_versions = async_to_streamed_response_wrapper(
+ journeys.list_versions,
+ )
+ self.publish = async_to_streamed_response_wrapper(
+ journeys.publish,
+ )
+ self.replace = async_to_streamed_response_wrapper(
+ journeys.replace,
+ )
+
+ @cached_property
+ def templates(self) -> AsyncTemplatesResourceWithStreamingResponse:
+ return AsyncTemplatesResourceWithStreamingResponse(self._journeys.templates)
diff --git a/src/courier/resources/journeys/templates.py b/src/courier/resources/journeys/templates.py
new file mode 100644
index 00000000..ce6f5ab9
--- /dev/null
+++ b/src/courier/resources/journeys/templates.py
@@ -0,0 +1,818 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.journeys import (
+ template_list_params,
+ template_create_params,
+ template_publish_params,
+ template_replace_params,
+)
+from ...types.journey_template_get_response import JourneyTemplateGetResponse
+from ...types.journey_template_list_response import JourneyTemplateListResponse
+from ...types.notification_template_version_list_response import NotificationTemplateVersionListResponse
+
+__all__ = ["TemplatesResource", "AsyncTemplatesResource"]
+
+
+class TemplatesResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> TemplatesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/trycourier/courier-python#accessing-raw-response-data-eg-headers
+ """
+ return TemplatesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> TemplatesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/trycourier/courier-python#with_streaming_response
+ """
+ return TemplatesResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ template_id: str,
+ *,
+ channel: str,
+ notification: template_create_params.Notification,
+ provider_key: str | Omit = omit,
+ state: 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,
+ ) -> JourneyTemplateGetResponse:
+ """Create a notification template scoped to this journey.
+
+ Defaults to `DRAFT`
+ state; pass `state: "PUBLISHED"` to publish on create.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._post(
+ path_template("/journeys/{template_id}/templates", template_id=template_id),
+ body=maybe_transform(
+ {
+ "channel": channel,
+ "notification": notification,
+ "provider_key": provider_key,
+ "state": state,
+ },
+ template_create_params.TemplateCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyTemplateGetResponse,
+ )
+
+ def retrieve(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyTemplateGetResponse:
+ """Fetch a journey-scoped notification template by id.
+
+ Pass `?version=draft`
+ (default `published`) to retrieve the working draft, or `?version=vN` for a
+ historical version.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ return self._get(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyTemplateGetResponse,
+ )
+
+ def list(
+ self,
+ template_id: str,
+ *,
+ cursor: 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,
+ ) -> JourneyTemplateListResponse:
+ """List notification templates scoped to this journey.
+
+ Journey-scoped notification
+ templates can only be referenced from `send` nodes within the same journey.
+
+ Args:
+ cursor: Pagination cursor from a prior response.
+
+ limit: Page size. Minimum 1, maximum 100.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return self._get(
+ path_template("/journeys/{template_id}/templates", template_id=template_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,
+ },
+ template_list_params.TemplateListParams,
+ ),
+ ),
+ cast_to=JourneyTemplateListResponse,
+ )
+
+ def archive(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Archive the journey-scoped notification template.
+
+ Archived templates cannot be
+ sent.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def list_versions(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> NotificationTemplateVersionListResponse:
+ """
+ List published versions of the journey-scoped notification template, ordered
+ most recent first.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ return self._get(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}/versions",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NotificationTemplateVersionListResponse,
+ )
+
+ def publish(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Publish the current draft of the journey-scoped notification template as a new
+ version. Optionally roll back to a prior version by passing
+ `{ "version": "vN" }`.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._post(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}/publish",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ body=maybe_transform({"version": version}, template_publish_params.TemplatePublishParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def replace(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ notification: template_replace_params.Notification,
+ state: 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,
+ ) -> JourneyTemplateGetResponse:
+ """
+ Replace the journey-scoped notification template draft.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ return self._put(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ body=maybe_transform(
+ {
+ "notification": notification,
+ "state": state,
+ },
+ template_replace_params.TemplateReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyTemplateGetResponse,
+ )
+
+
+class AsyncTemplatesResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncTemplatesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/trycourier/courier-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncTemplatesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncTemplatesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/trycourier/courier-python#with_streaming_response
+ """
+ return AsyncTemplatesResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ template_id: str,
+ *,
+ channel: str,
+ notification: template_create_params.Notification,
+ provider_key: str | Omit = omit,
+ state: 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,
+ ) -> JourneyTemplateGetResponse:
+ """Create a notification template scoped to this journey.
+
+ Defaults to `DRAFT`
+ state; pass `state: "PUBLISHED"` to publish on create.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._post(
+ path_template("/journeys/{template_id}/templates", template_id=template_id),
+ body=await async_maybe_transform(
+ {
+ "channel": channel,
+ "notification": notification,
+ "provider_key": provider_key,
+ "state": state,
+ },
+ template_create_params.TemplateCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyTemplateGetResponse,
+ )
+
+ async def retrieve(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> JourneyTemplateGetResponse:
+ """Fetch a journey-scoped notification template by id.
+
+ Pass `?version=draft`
+ (default `published`) to retrieve the working draft, or `?version=vN` for a
+ historical version.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ return await self._get(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyTemplateGetResponse,
+ )
+
+ async def list(
+ self,
+ template_id: str,
+ *,
+ cursor: 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,
+ ) -> JourneyTemplateListResponse:
+ """List notification templates scoped to this journey.
+
+ Journey-scoped notification
+ templates can only be referenced from `send` nodes within the same journey.
+
+ Args:
+ cursor: Pagination cursor from a prior response.
+
+ limit: Page size. Minimum 1, maximum 100.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ return await self._get(
+ path_template("/journeys/{template_id}/templates", template_id=template_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,
+ },
+ template_list_params.TemplateListParams,
+ ),
+ ),
+ cast_to=JourneyTemplateListResponse,
+ )
+
+ async def archive(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Archive the journey-scoped notification template.
+
+ Archived templates cannot be
+ sent.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def list_versions(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> NotificationTemplateVersionListResponse:
+ """
+ List published versions of the journey-scoped notification template, ordered
+ most recent first.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ return await self._get(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}/versions",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NotificationTemplateVersionListResponse,
+ )
+
+ async def publish(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ version: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Publish the current draft of the journey-scoped notification template as a new
+ version. Optionally roll back to a prior version by passing
+ `{ "version": "vN" }`.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._post(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}/publish",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ body=await async_maybe_transform({"version": version}, template_publish_params.TemplatePublishParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def replace(
+ self,
+ notification_id: str,
+ *,
+ template_id: str,
+ notification: template_replace_params.Notification,
+ state: 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,
+ ) -> JourneyTemplateGetResponse:
+ """
+ Replace the journey-scoped notification template draft.
+
+ 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 template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ if not notification_id:
+ raise ValueError(f"Expected a non-empty value for `notification_id` but received {notification_id!r}")
+ return await self._put(
+ path_template(
+ "/journeys/{template_id}/templates/{notification_id}",
+ template_id=template_id,
+ notification_id=notification_id,
+ ),
+ body=await async_maybe_transform(
+ {
+ "notification": notification,
+ "state": state,
+ },
+ template_replace_params.TemplateReplaceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=JourneyTemplateGetResponse,
+ )
+
+
+class TemplatesResourceWithRawResponse:
+ def __init__(self, templates: TemplatesResource) -> None:
+ self._templates = templates
+
+ self.create = to_raw_response_wrapper(
+ templates.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ templates.list,
+ )
+ self.archive = to_raw_response_wrapper(
+ templates.archive,
+ )
+ self.list_versions = to_raw_response_wrapper(
+ templates.list_versions,
+ )
+ self.publish = to_raw_response_wrapper(
+ templates.publish,
+ )
+ self.replace = to_raw_response_wrapper(
+ templates.replace,
+ )
+
+
+class AsyncTemplatesResourceWithRawResponse:
+ def __init__(self, templates: AsyncTemplatesResource) -> None:
+ self._templates = templates
+
+ self.create = async_to_raw_response_wrapper(
+ templates.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ templates.list,
+ )
+ self.archive = async_to_raw_response_wrapper(
+ templates.archive,
+ )
+ self.list_versions = async_to_raw_response_wrapper(
+ templates.list_versions,
+ )
+ self.publish = async_to_raw_response_wrapper(
+ templates.publish,
+ )
+ self.replace = async_to_raw_response_wrapper(
+ templates.replace,
+ )
+
+
+class TemplatesResourceWithStreamingResponse:
+ def __init__(self, templates: TemplatesResource) -> None:
+ self._templates = templates
+
+ self.create = to_streamed_response_wrapper(
+ templates.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ templates.list,
+ )
+ self.archive = to_streamed_response_wrapper(
+ templates.archive,
+ )
+ self.list_versions = to_streamed_response_wrapper(
+ templates.list_versions,
+ )
+ self.publish = to_streamed_response_wrapper(
+ templates.publish,
+ )
+ self.replace = to_streamed_response_wrapper(
+ templates.replace,
+ )
+
+
+class AsyncTemplatesResourceWithStreamingResponse:
+ def __init__(self, templates: AsyncTemplatesResource) -> None:
+ self._templates = templates
+
+ self.create = async_to_streamed_response_wrapper(
+ templates.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ templates.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ templates.list,
+ )
+ self.archive = async_to_streamed_response_wrapper(
+ templates.archive,
+ )
+ self.list_versions = async_to_streamed_response_wrapper(
+ templates.list_versions,
+ )
+ self.publish = async_to_streamed_response_wrapper(
+ templates.publish,
+ )
+ self.replace = async_to_streamed_response_wrapper(
+ templates.replace,
+ )
diff --git a/src/courier/resources/tenants/templates/templates.py b/src/courier/resources/tenants/templates/templates.py
index 12a3733b..accb1275 100644
--- a/src/courier/resources/tenants/templates/templates.py
+++ b/src/courier/resources/tenants/templates/templates.py
@@ -14,7 +14,7 @@
VersionsResourceWithStreamingResponse,
AsyncVersionsResourceWithStreamingResponse,
)
-from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from ...._utils import path_template, maybe_transform, async_maybe_transform
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
@@ -144,6 +144,48 @@ def list(
cast_to=TemplateListResponse,
)
+ def delete(
+ self,
+ template_id: str,
+ *,
+ tenant_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Deletes the tenant's notification template with the given `template_id`.
+
+ Returns **204 No Content** with an empty body on success.
+
+ Returns **404** if there is no template with this ID for the tenant, including a
+ second `DELETE` after a successful removal.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/tenants/{tenant_id}/templates/{template_id}", tenant_id=tenant_id, template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
def publish(
self,
template_id: str,
@@ -357,6 +399,48 @@ async def list(
cast_to=TemplateListResponse,
)
+ async def delete(
+ self,
+ template_id: str,
+ *,
+ tenant_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Deletes the tenant's notification template with the given `template_id`.
+
+ Returns **204 No Content** with an empty body on success.
+
+ Returns **404** if there is no template with this ID for the tenant, including a
+ second `DELETE` after a successful removal.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ if not template_id:
+ raise ValueError(f"Expected a non-empty value for `template_id` but received {template_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/tenants/{tenant_id}/templates/{template_id}", tenant_id=tenant_id, template_id=template_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
async def publish(
self,
template_id: str,
@@ -471,6 +555,9 @@ def __init__(self, templates: TemplatesResource) -> None:
self.list = to_raw_response_wrapper(
templates.list,
)
+ self.delete = to_raw_response_wrapper(
+ templates.delete,
+ )
self.publish = to_raw_response_wrapper(
templates.publish,
)
@@ -493,6 +580,9 @@ def __init__(self, templates: AsyncTemplatesResource) -> None:
self.list = async_to_raw_response_wrapper(
templates.list,
)
+ self.delete = async_to_raw_response_wrapper(
+ templates.delete,
+ )
self.publish = async_to_raw_response_wrapper(
templates.publish,
)
@@ -515,6 +605,9 @@ def __init__(self, templates: TemplatesResource) -> None:
self.list = to_streamed_response_wrapper(
templates.list,
)
+ self.delete = to_streamed_response_wrapper(
+ templates.delete,
+ )
self.publish = to_streamed_response_wrapper(
templates.publish,
)
@@ -537,6 +630,9 @@ def __init__(self, templates: AsyncTemplatesResource) -> None:
self.list = async_to_streamed_response_wrapper(
templates.list,
)
+ self.delete = async_to_streamed_response_wrapper(
+ templates.delete,
+ )
self.publish = async_to_streamed_response_wrapper(
templates.publish,
)
diff --git a/src/courier/types/__init__.py b/src/courier/types/__init__.py
index 31bd3e57..89b0b957 100644
--- a/src/courier/types/__init__.py
+++ b/src/courier/types/__init__.py
@@ -6,6 +6,7 @@
shared,
tenants,
audience,
+ journey_response,
audience_list_response,
element_with_checksums,
audience_update_response,
@@ -109,21 +110,28 @@
from .brand_colors import BrandColors as BrandColors
from .email_footer import EmailFooter as EmailFooter
from .email_header import EmailHeader as EmailHeader
+from .journey_node import JourneyNode as JourneyNode
from .version_node import VersionNode as VersionNode
from .brand_snippet import BrandSnippet as BrandSnippet
+from .journey_state import JourneyState as JourneyState
from .brand_settings import BrandSettings as BrandSettings
from .brand_snippets import BrandSnippets as BrandSnippets
from .brand_template import BrandTemplate as BrandTemplate
+from .journey_ai_node import JourneyAINode as JourneyAINode
from .message_details import MessageDetails as MessageDetails
from .base_check_param import BaseCheckParam as BaseCheckParam
from .email_head_param import EmailHeadParam as EmailHeadParam
+from .journey_response import JourneyResponse as JourneyResponse
from .list_list_params import ListListParams as ListListParams
from .brand_list_params import BrandListParams as BrandListParams
+from .journey_exit_node import JourneyExitNode as JourneyExitNode
+from .journey_send_node import JourneySendNode as JourneySendNode
from .subscription_list import SubscriptionList as SubscriptionList
from .widget_background import WidgetBackground as WidgetBackground
from .brand_colors_param import BrandColorsParam as BrandColorsParam
from .email_footer_param import EmailFooterParam as EmailFooterParam
from .email_header_param import EmailHeaderParam as EmailHeaderParam
+from .journey_node_param import JourneyNodeParam as JourneyNodeParam
from .list_list_response import ListListResponse as ListListResponse
from .list_update_params import ListUpdateParams as ListUpdateParams
from .tenant_association import TenantAssociation as TenantAssociation
@@ -143,11 +151,14 @@
from .brand_snippets_param import BrandSnippetsParam as BrandSnippetsParam
from .brand_template_param import BrandTemplateParam as BrandTemplateParam
from .inbound_bulk_message import InboundBulkMessage as InboundBulkMessage
+from .journey_version_item import JourneyVersionItem as JourneyVersionItem
from .provider_list_params import ProviderListParams as ProviderListParams
from .tenant_list_response import TenantListResponse as TenantListResponse
from .tenant_update_params import TenantUpdateParams as TenantUpdateParams
from .brand_settings_in_app import BrandSettingsInApp as BrandSettingsInApp
from .bulk_add_users_params import BulkAddUsersParams as BulkAddUsersParams
+from .journey_ai_node_param import JourneyAINodeParam as JourneyAINodeParam
+from .journey_create_params import JourneyCreateParams as JourneyCreateParams
from .journey_invoke_params import JourneyInvokeParams as JourneyInvokeParams
from .message_list_response import MessageListResponse as MessageListResponse
from .profile_create_params import ProfileCreateParams as ProfileCreateParams
@@ -159,6 +170,10 @@
from .bulk_create_job_params import BulkCreateJobParams as BulkCreateJobParams
from .bulk_list_users_params import BulkListUsersParams as BulkListUsersParams
from .element_with_checksums import ElementWithChecksums as ElementWithChecksums
+from .journey_condition_atom import JourneyConditionAtom as JourneyConditionAtom
+from .journey_merge_strategy import JourneyMergeStrategy as JourneyMergeStrategy
+from .journey_publish_params import JourneyPublishParams as JourneyPublishParams
+from .journey_replace_params import JourneyReplaceParams as JourneyReplaceParams
from .journeys_list_response import JourneysListResponse as JourneysListResponse
from .message_history_params import MessageHistoryParams as MessageHistoryParams
from .profile_replace_params import ProfileReplaceParams as ProfileReplaceParams
@@ -168,12 +183,19 @@
from .subscription_topic_new import SubscriptionTopicNew as SubscriptionTopicNew
from .audit_event_list_params import AuditEventListParams as AuditEventListParams
from .auth_issue_token_params import AuthIssueTokenParams as AuthIssueTokenParams
+from .journey_condition_group import JourneyConditionGroup as JourneyConditionGroup
+from .journey_exit_node_param import JourneyExitNodeParam as JourneyExitNodeParam
+from .journey_retrieve_params import JourneyRetrieveParams as JourneyRetrieveParams
+from .journey_send_node_param import JourneySendNodeParam as JourneySendNodeParam
from .profile_create_response import ProfileCreateResponse as ProfileCreateResponse
from .providers_catalog_entry import ProvidersCatalogEntry as ProvidersCatalogEntry
from .widget_background_param import WidgetBackgroundParam as WidgetBackgroundParam
from .audience_update_response import AudienceUpdateResponse as AudienceUpdateResponse
from .bulk_create_job_response import BulkCreateJobResponse as BulkCreateJobResponse
from .bulk_list_users_response import BulkListUsersResponse as BulkListUsersResponse
+from .journey_conditions_field import JourneyConditionsField as JourneyConditionsField
+from .journey_delay_until_node import JourneyDelayUntilNode as JourneyDelayUntilNode
+from .journey_template_summary import JourneyTemplateSummary as JourneyTemplateSummary
from .journeys_invoke_response import JourneysInvokeResponse as JourneysInvokeResponse
from .message_content_response import MessageContentResponse as MessageContentResponse
from .message_history_response import MessageHistoryResponse as MessageHistoryResponse
@@ -199,39 +221,62 @@
from .notification_list_response import NotificationListResponse as NotificationListResponse
from .tenant_list_users_response import TenantListUsersResponse as TenantListUsersResponse
from .brand_settings_in_app_param import BrandSettingsInAppParam as BrandSettingsInAppParam
+from .journey_delay_duration_node import JourneyDelayDurationNode as JourneyDelayDurationNode
+from .journey_fetch_post_put_node import JourneyFetchPostPutNode as JourneyFetchPostPutNode
from .notification_publish_params import NotificationPublishParams as NotificationPublishParams
from .notification_replace_params import NotificationReplaceParams as NotificationReplaceParams
from .notification_template_state import NotificationTemplateState as NotificationTemplateState
from .tenant_template_input_param import TenantTemplateInputParam as TenantTemplateInputParam
from .audience_list_members_params import AudienceListMembersParams as AudienceListMembersParams
from .inbound_track_event_response import InboundTrackEventResponse as InboundTrackEventResponse
+from .journey_condition_atom_param import JourneyConditionAtomParam as JourneyConditionAtomParam
+from .journey_segment_trigger_node import JourneySegmentTriggerNode as JourneySegmentTriggerNode
+from .journey_throttle_static_node import JourneyThrottleStaticNode as JourneyThrottleStaticNode
from .notification_retrieve_params import NotificationRetrieveParams as NotificationRetrieveParams
from .put_tenant_template_response import PutTenantTemplateResponse as PutTenantTemplateResponse
from .routing_strategy_list_params import RoutingStrategyListParams as RoutingStrategyListParams
from .subscription_topic_new_param import SubscriptionTopicNewParam as SubscriptionTopicNewParam
+from .journey_condition_group_param import JourneyConditionGroupParam as JourneyConditionGroupParam
+from .journey_fetch_get_delete_node import JourneyFetchGetDeleteNode as JourneyFetchGetDeleteNode
+from .journey_template_get_response import JourneyTemplateGetResponse as JourneyTemplateGetResponse
+from .journey_throttle_dynamic_node import JourneyThrottleDynamicNode as JourneyThrottleDynamicNode
from .notification_template_payload import NotificationTemplatePayload as NotificationTemplatePayload
from .notification_template_summary import NotificationTemplateSummary as NotificationTemplateSummary
from .routing_strategy_get_response import RoutingStrategyGetResponse as RoutingStrategyGetResponse
from .translation_retrieve_response import TranslationRetrieveResponse as TranslationRetrieveResponse
from .audience_list_members_response import AudienceListMembersResponse as AudienceListMembersResponse
+from .journey_condition_nested_group import JourneyConditionNestedGroup as JourneyConditionNestedGroup
+from .journey_conditions_field_param import JourneyConditionsFieldParam as JourneyConditionsFieldParam
+from .journey_delay_until_node_param import JourneyDelayUntilNodeParam as JourneyDelayUntilNodeParam
+from .journey_template_list_response import JourneyTemplateListResponse as JourneyTemplateListResponse
+from .journey_versions_list_response import JourneyVersionsListResponse as JourneyVersionsListResponse
from .notification_put_locale_params import NotificationPutLocaleParams as NotificationPutLocaleParams
from .notification_template_response import NotificationTemplateResponse as NotificationTemplateResponse
from .routing_strategy_create_params import RoutingStrategyCreateParams as RoutingStrategyCreateParams
from .routing_strategy_list_response import RoutingStrategyListResponse as RoutingStrategyListResponse
from .inbound_bulk_message_user_param import InboundBulkMessageUserParam as InboundBulkMessageUserParam
+from .journey_api_invoke_trigger_node import JourneyAPIInvokeTriggerNode as JourneyAPIInvokeTriggerNode
from .notification_put_content_params import NotificationPutContentParams as NotificationPutContentParams
from .notification_put_element_params import NotificationPutElementParams as NotificationPutElementParams
from .routing_strategy_replace_params import RoutingStrategyReplaceParams as RoutingStrategyReplaceParams
from .base_template_tenant_association import BaseTemplateTenantAssociation as BaseTemplateTenantAssociation
from .automation_template_list_response import AutomationTemplateListResponse as AutomationTemplateListResponse
+from .journey_delay_duration_node_param import JourneyDelayDurationNodeParam as JourneyDelayDurationNodeParam
+from .journey_fetch_post_put_node_param import JourneyFetchPostPutNodeParam as JourneyFetchPostPutNodeParam
from .notification_content_get_response import NotificationContentGetResponse as NotificationContentGetResponse
from .notification_list_versions_params import NotificationListVersionsParams as NotificationListVersionsParams
from .put_subscriptions_recipient_param import PutSubscriptionsRecipientParam as PutSubscriptionsRecipientParam
+from .journey_segment_trigger_node_param import JourneySegmentTriggerNodeParam as JourneySegmentTriggerNodeParam
+from .journey_throttle_static_node_param import JourneyThrottleStaticNodeParam as JourneyThrottleStaticNodeParam
+from .journey_fetch_get_delete_node_param import JourneyFetchGetDeleteNodeParam as JourneyFetchGetDeleteNodeParam
+from .journey_throttle_dynamic_node_param import JourneyThrottleDynamicNodeParam as JourneyThrottleDynamicNodeParam
from .notification_template_payload_param import NotificationTemplatePayloadParam as NotificationTemplatePayloadParam
+from .journey_condition_nested_group_param import JourneyConditionNestedGroupParam as JourneyConditionNestedGroupParam
from .notification_retrieve_content_params import NotificationRetrieveContentParams as NotificationRetrieveContentParams
from .associated_notification_list_response import (
AssociatedNotificationListResponse as AssociatedNotificationListResponse,
)
+from .journey_api_invoke_trigger_node_param import JourneyAPIInvokeTriggerNodeParam as JourneyAPIInvokeTriggerNodeParam
from .post_tenant_template_publish_response import (
PostTenantTemplatePublishResponse as PostTenantTemplatePublishResponse,
)
@@ -257,6 +302,7 @@
audience.Audience.update_forward_refs() # type: ignore
audience_update_response.AudienceUpdateResponse.update_forward_refs() # type: ignore
audience_list_response.AudienceListResponse.update_forward_refs() # type: ignore
+ journey_response.JourneyResponse.update_forward_refs() # type: ignore
element_with_checksums.ElementWithChecksums.update_forward_refs() # type: ignore
notification_content_get_response.NotificationContentGetResponse.update_forward_refs() # type: ignore
notification_list_response.NotificationListResponse.update_forward_refs() # type: ignore
@@ -269,6 +315,7 @@
audience.Audience.model_rebuild(_parent_namespace_depth=0)
audience_update_response.AudienceUpdateResponse.model_rebuild(_parent_namespace_depth=0)
audience_list_response.AudienceListResponse.model_rebuild(_parent_namespace_depth=0)
+ journey_response.JourneyResponse.model_rebuild(_parent_namespace_depth=0)
element_with_checksums.ElementWithChecksums.model_rebuild(_parent_namespace_depth=0)
notification_content_get_response.NotificationContentGetResponse.model_rebuild(_parent_namespace_depth=0)
notification_list_response.NotificationListResponse.model_rebuild(_parent_namespace_depth=0)
diff --git a/src/courier/types/journey_ai_node.py b/src/courier/types/journey_ai_node.py
new file mode 100644
index 00000000..d836a4b2
--- /dev/null
+++ b/src/courier/types/journey_ai_node.py
@@ -0,0 +1,36 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyAINode"]
+
+
+class JourneyAINode(BaseModel):
+ """Invoke an AI step with `user_prompt` and optional `web_search`.
+
+ Returns a structured response conforming to `output_schema`.
+ """
+
+ output_schema: Dict[str, object]
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
+
+ type: Literal["ai"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ model: Optional[str] = None
+
+ user_prompt: Optional[str] = None
+
+ web_search: Optional[bool] = None
diff --git a/src/courier/types/journey_ai_node_param.py b/src/courier/types/journey_ai_node_param.py
new file mode 100644
index 00000000..79093ca9
--- /dev/null
+++ b/src/courier/types/journey_ai_node_param.py
@@ -0,0 +1,37 @@
+# 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 Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyAINodeParam"]
+
+
+class JourneyAINodeParam(TypedDict, total=False):
+ """Invoke an AI step with `user_prompt` and optional `web_search`.
+
+ Returns a structured response conforming to `output_schema`.
+ """
+
+ output_schema: Required[Dict[str, object]]
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
+
+ type: Required[Literal["ai"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ model: str
+
+ user_prompt: str
+
+ web_search: bool
diff --git a/src/courier/types/journey_api_invoke_trigger_node.py b/src/courier/types/journey_api_invoke_trigger_node.py
new file mode 100644
index 00000000..fefbfda3
--- /dev/null
+++ b/src/courier/types/journey_api_invoke_trigger_node.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyAPIInvokeTriggerNode"]
+
+
+class JourneyAPIInvokeTriggerNode(BaseModel):
+ """Trigger fired when the journey is invoked via the API.
+
+ The optional `schema` field is a JSON Schema that validates the invocation payload.
+ """
+
+ trigger_type: Literal["api-invoke"]
+
+ type: Literal["trigger"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ schema_: Optional[Dict[str, object]] = FieldInfo(alias="schema", default=None)
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
diff --git a/src/courier/types/journey_api_invoke_trigger_node_param.py b/src/courier/types/journey_api_invoke_trigger_node_param.py
new file mode 100644
index 00000000..f5cc604b
--- /dev/null
+++ b/src/courier/types/journey_api_invoke_trigger_node_param.py
@@ -0,0 +1,33 @@
+# 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 Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyAPIInvokeTriggerNodeParam"]
+
+
+class JourneyAPIInvokeTriggerNodeParam(TypedDict, total=False):
+ """Trigger fired when the journey is invoked via the API.
+
+ The optional `schema` field is a JSON Schema that validates the invocation payload.
+ """
+
+ trigger_type: Required[Literal["api-invoke"]]
+
+ type: Required[Literal["trigger"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ schema: Dict[str, object]
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
diff --git a/src/courier/types/journey_condition_atom.py b/src/courier/types/journey_condition_atom.py
new file mode 100644
index 00000000..d29571e4
--- /dev/null
+++ b/src/courier/types/journey_condition_atom.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import TypeAlias
+
+__all__ = ["JourneyConditionAtom"]
+
+JourneyConditionAtom: TypeAlias = List[str]
diff --git a/src/courier/types/journey_condition_atom_param.py b/src/courier/types/journey_condition_atom_param.py
new file mode 100644
index 00000000..e23557f0
--- /dev/null
+++ b/src/courier/types/journey_condition_atom_param.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import TypeAlias
+
+__all__ = ["JourneyConditionAtomParam", "JourneyConditionAtomParamItem"]
+
+JourneyConditionAtomParamItem: TypeAlias = str
+
+JourneyConditionAtomParam: TypeAlias = List[str]
diff --git a/src/courier/types/journey_condition_group.py b/src/courier/types/journey_condition_group.py
new file mode 100644
index 00000000..e2b26796
--- /dev/null
+++ b/src/courier/types/journey_condition_group.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .journey_condition_atom import JourneyConditionAtom
+
+__all__ = ["JourneyConditionGroup"]
+
+
+class JourneyConditionGroup(BaseModel):
+ """A leaf condition group.
+
+ Exactly one of `AND` or `OR` must be present at runtime; each is a list of `JourneyConditionAtom` tuples.
+ """
+
+ and_: Optional[List[JourneyConditionAtom]] = FieldInfo(alias="AND", default=None)
+
+ or_: Optional[List[JourneyConditionAtom]] = FieldInfo(alias="OR", default=None)
diff --git a/src/courier/types/journey_condition_group_param.py b/src/courier/types/journey_condition_group_param.py
new file mode 100644
index 00000000..9067be7f
--- /dev/null
+++ b/src/courier/types/journey_condition_group_param.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Annotated, TypedDict
+
+from .._utils import PropertyInfo
+from .journey_condition_atom_param import JourneyConditionAtomParam
+
+__all__ = ["JourneyConditionGroupParam"]
+
+
+class JourneyConditionGroupParam(TypedDict, total=False):
+ """A leaf condition group.
+
+ Exactly one of `AND` or `OR` must be present at runtime; each is a list of `JourneyConditionAtom` tuples.
+ """
+
+ and_: Annotated[Iterable[JourneyConditionAtomParam], PropertyInfo(alias="AND")]
+
+ or_: Annotated[Iterable[JourneyConditionAtomParam], PropertyInfo(alias="OR")]
diff --git a/src/courier/types/journey_condition_nested_group.py b/src/courier/types/journey_condition_nested_group.py
new file mode 100644
index 00000000..c521bb16
--- /dev/null
+++ b/src/courier/types/journey_condition_nested_group.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .journey_condition_group import JourneyConditionGroup
+
+__all__ = ["JourneyConditionNestedGroup"]
+
+
+class JourneyConditionNestedGroup(BaseModel):
+ """A nested condition group.
+
+ Exactly one of `AND` or `OR` must be present at runtime; each is a list of `JourneyConditionGroup` items.
+ """
+
+ and_: Optional[List[JourneyConditionGroup]] = FieldInfo(alias="AND", default=None)
+
+ or_: Optional[List[JourneyConditionGroup]] = FieldInfo(alias="OR", default=None)
diff --git a/src/courier/types/journey_condition_nested_group_param.py b/src/courier/types/journey_condition_nested_group_param.py
new file mode 100644
index 00000000..0465bde7
--- /dev/null
+++ b/src/courier/types/journey_condition_nested_group_param.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Annotated, TypedDict
+
+from .._utils import PropertyInfo
+from .journey_condition_group_param import JourneyConditionGroupParam
+
+__all__ = ["JourneyConditionNestedGroupParam"]
+
+
+class JourneyConditionNestedGroupParam(TypedDict, total=False):
+ """A nested condition group.
+
+ Exactly one of `AND` or `OR` must be present at runtime; each is a list of `JourneyConditionGroup` items.
+ """
+
+ and_: Annotated[Iterable[JourneyConditionGroupParam], PropertyInfo(alias="AND")]
+
+ or_: Annotated[Iterable[JourneyConditionGroupParam], PropertyInfo(alias="OR")]
diff --git a/src/courier/types/journey_conditions_field.py b/src/courier/types/journey_conditions_field.py
new file mode 100644
index 00000000..e5e58e0c
--- /dev/null
+++ b/src/courier/types/journey_conditions_field.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import TypeAlias
+
+from .journey_condition_atom import JourneyConditionAtom
+from .journey_condition_group import JourneyConditionGroup
+from .journey_condition_nested_group import JourneyConditionNestedGroup
+
+__all__ = ["JourneyConditionsField"]
+
+JourneyConditionsField: TypeAlias = Union[JourneyConditionAtom, JourneyConditionGroup, JourneyConditionNestedGroup]
diff --git a/src/courier/types/journey_conditions_field_param.py b/src/courier/types/journey_conditions_field_param.py
new file mode 100644
index 00000000..258c8b73
--- /dev/null
+++ b/src/courier/types/journey_conditions_field_param.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 Union
+from typing_extensions import TypeAlias
+
+from .journey_condition_atom_param import JourneyConditionAtomParam
+from .journey_condition_group_param import JourneyConditionGroupParam
+from .journey_condition_nested_group_param import JourneyConditionNestedGroupParam
+
+__all__ = ["JourneyConditionsFieldParam"]
+
+JourneyConditionsFieldParam: TypeAlias = Union[
+ JourneyConditionAtomParam, JourneyConditionGroupParam, JourneyConditionNestedGroupParam
+]
diff --git a/src/courier/types/journey_create_params.py b/src/courier/types/journey_create_params.py
new file mode 100644
index 00000000..c5326eef
--- /dev/null
+++ b/src/courier/types/journey_create_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Required, TypedDict
+
+from .journey_state import JourneyState
+
+__all__ = ["JourneyCreateParams"]
+
+
+class JourneyCreateParams(TypedDict, total=False):
+ name: Required[str]
+
+ nodes: Required[Iterable["JourneyNodeParam"]]
+
+ enabled: bool
+
+ state: JourneyState
+ """Lifecycle state of a journey."""
+
+
+from .journey_node_param import JourneyNodeParam
diff --git a/src/courier/types/journey_delay_duration_node.py b/src/courier/types/journey_delay_duration_node.py
new file mode 100644
index 00000000..60e20bd6
--- /dev/null
+++ b/src/courier/types/journey_delay_duration_node.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyDelayDurationNode"]
+
+
+class JourneyDelayDurationNode(BaseModel):
+ """Pause the journey run for a fixed `duration`."""
+
+ duration: str
+
+ mode: Literal["duration"]
+
+ type: Literal["delay"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_delay_duration_node_param.py b/src/courier/types/journey_delay_duration_node_param.py
new file mode 100644
index 00000000..38b0d51e
--- /dev/null
+++ b/src/courier/types/journey_delay_duration_node_param.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyDelayDurationNodeParam"]
+
+
+class JourneyDelayDurationNodeParam(TypedDict, total=False):
+ """Pause the journey run for a fixed `duration`."""
+
+ duration: Required[str]
+
+ mode: Required[Literal["duration"]]
+
+ type: Required[Literal["delay"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_delay_until_node.py b/src/courier/types/journey_delay_until_node.py
new file mode 100644
index 00000000..09da90d7
--- /dev/null
+++ b/src/courier/types/journey_delay_until_node.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyDelayUntilNode"]
+
+
+class JourneyDelayUntilNode(BaseModel):
+ """Pause the journey run `until` a specific time."""
+
+ mode: Literal["until"]
+
+ type: Literal["delay"]
+
+ until: str
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_delay_until_node_param.py b/src/courier/types/journey_delay_until_node_param.py
new file mode 100644
index 00000000..35978be9
--- /dev/null
+++ b/src/courier/types/journey_delay_until_node_param.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyDelayUntilNodeParam"]
+
+
+class JourneyDelayUntilNodeParam(TypedDict, total=False):
+ """Pause the journey run `until` a specific time."""
+
+ mode: Required[Literal["until"]]
+
+ type: Required[Literal["delay"]]
+
+ until: Required[str]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_exit_node.py b/src/courier/types/journey_exit_node.py
new file mode 100644
index 00000000..9339c93d
--- /dev/null
+++ b/src/courier/types/journey_exit_node.py
@@ -0,0 +1,16 @@
+# 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__ = ["JourneyExitNode"]
+
+
+class JourneyExitNode(BaseModel):
+ """Terminate the journey run."""
+
+ type: Literal["exit"]
+
+ id: Optional[str] = None
diff --git a/src/courier/types/journey_exit_node_param.py b/src/courier/types/journey_exit_node_param.py
new file mode 100644
index 00000000..5a725dda
--- /dev/null
+++ b/src/courier/types/journey_exit_node_param.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["JourneyExitNodeParam"]
+
+
+class JourneyExitNodeParam(TypedDict, total=False):
+ """Terminate the journey run."""
+
+ type: Required[Literal["exit"]]
+
+ id: str
diff --git a/src/courier/types/journey_fetch_get_delete_node.py b/src/courier/types/journey_fetch_get_delete_node.py
new file mode 100644
index 00000000..5fdbdbb9
--- /dev/null
+++ b/src/courier/types/journey_fetch_get_delete_node.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_merge_strategy import JourneyMergeStrategy
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyFetchGetDeleteNode"]
+
+
+class JourneyFetchGetDeleteNode(BaseModel):
+ """
+ Issue an HTTP GET or DELETE request and merge the response into the journey state per `merge_strategy`.
+ """
+
+ merge_strategy: JourneyMergeStrategy
+ """Strategy for merging a fetch response into the journey run state."""
+
+ method: Literal["get", "delete"]
+
+ type: Literal["fetch"]
+
+ url: str
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ headers: Optional[Dict[str, str]] = None
+
+ query_params: Optional[Dict[str, str]] = None
+
+ response_schema: Optional[Dict[str, object]] = None
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
diff --git a/src/courier/types/journey_fetch_get_delete_node_param.py b/src/courier/types/journey_fetch_get_delete_node_param.py
new file mode 100644
index 00000000..642abba0
--- /dev/null
+++ b/src/courier/types/journey_fetch_get_delete_node_param.py
@@ -0,0 +1,42 @@
+# 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 Literal, Required, TypedDict
+
+from .journey_merge_strategy import JourneyMergeStrategy
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyFetchGetDeleteNodeParam"]
+
+
+class JourneyFetchGetDeleteNodeParam(TypedDict, total=False):
+ """
+ Issue an HTTP GET or DELETE request and merge the response into the journey state per `merge_strategy`.
+ """
+
+ merge_strategy: Required[JourneyMergeStrategy]
+ """Strategy for merging a fetch response into the journey run state."""
+
+ method: Required[Literal["get", "delete"]]
+
+ type: Required[Literal["fetch"]]
+
+ url: Required[str]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ headers: Dict[str, str]
+
+ query_params: Dict[str, str]
+
+ response_schema: Dict[str, object]
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
diff --git a/src/courier/types/journey_fetch_post_put_node.py b/src/courier/types/journey_fetch_post_put_node.py
new file mode 100644
index 00000000..f1c91aae
--- /dev/null
+++ b/src/courier/types/journey_fetch_post_put_node.py
@@ -0,0 +1,43 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_merge_strategy import JourneyMergeStrategy
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyFetchPostPutNode"]
+
+
+class JourneyFetchPostPutNode(BaseModel):
+ """
+ Issue an HTTP POST or PUT request with a `body` and merge the response into the journey state per `merge_strategy`.
+ """
+
+ merge_strategy: JourneyMergeStrategy
+ """Strategy for merging a fetch response into the journey run state."""
+
+ method: Literal["post", "put"]
+
+ type: Literal["fetch"]
+
+ url: str
+
+ id: Optional[str] = None
+
+ body: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ headers: Optional[Dict[str, str]] = None
+
+ query_params: Optional[Dict[str, str]] = None
+
+ response_schema: Optional[Dict[str, object]] = None
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
diff --git a/src/courier/types/journey_fetch_post_put_node_param.py b/src/courier/types/journey_fetch_post_put_node_param.py
new file mode 100644
index 00000000..f07ad037
--- /dev/null
+++ b/src/courier/types/journey_fetch_post_put_node_param.py
@@ -0,0 +1,44 @@
+# 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 Literal, Required, TypedDict
+
+from .journey_merge_strategy import JourneyMergeStrategy
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyFetchPostPutNodeParam"]
+
+
+class JourneyFetchPostPutNodeParam(TypedDict, total=False):
+ """
+ Issue an HTTP POST or PUT request with a `body` and merge the response into the journey state per `merge_strategy`.
+ """
+
+ merge_strategy: Required[JourneyMergeStrategy]
+ """Strategy for merging a fetch response into the journey run state."""
+
+ method: Required[Literal["post", "put"]]
+
+ type: Required[Literal["fetch"]]
+
+ url: Required[str]
+
+ id: str
+
+ body: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ headers: Dict[str, str]
+
+ query_params: Dict[str, str]
+
+ response_schema: Dict[str, object]
+ """A JSONSchema object (Draft-07-compatible). Validated at runtime by Ajv."""
diff --git a/src/courier/types/journey_merge_strategy.py b/src/courier/types/journey_merge_strategy.py
new file mode 100644
index 00000000..8c9fd787
--- /dev/null
+++ b/src/courier/types/journey_merge_strategy.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__ = ["JourneyMergeStrategy"]
+
+JourneyMergeStrategy: TypeAlias = Literal["overwrite", "soft-merge", "replace", "none"]
diff --git a/src/courier/types/journey_node.py b/src/courier/types/journey_node.py
new file mode 100644
index 00000000..05b37e4b
--- /dev/null
+++ b/src/courier/types/journey_node.py
@@ -0,0 +1,72 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Union, Optional
+from typing_extensions import Literal, TypeAlias
+
+from .._models import BaseModel
+from .journey_ai_node import JourneyAINode
+from .journey_exit_node import JourneyExitNode
+from .journey_send_node import JourneySendNode
+from .journey_conditions_field import JourneyConditionsField
+from .journey_delay_until_node import JourneyDelayUntilNode
+from .journey_delay_duration_node import JourneyDelayDurationNode
+from .journey_fetch_post_put_node import JourneyFetchPostPutNode
+from .journey_segment_trigger_node import JourneySegmentTriggerNode
+from .journey_throttle_static_node import JourneyThrottleStaticNode
+from .journey_fetch_get_delete_node import JourneyFetchGetDeleteNode
+from .journey_throttle_dynamic_node import JourneyThrottleDynamicNode
+from .journey_api_invoke_trigger_node import JourneyAPIInvokeTriggerNode
+
+__all__ = ["JourneyNode", "JourneyBranchNode", "JourneyBranchNodeDefault", "JourneyBranchNodePath"]
+
+
+class JourneyBranchNodeDefault(BaseModel):
+ nodes: List["JourneyNode"]
+
+ label: Optional[str] = None
+
+
+class JourneyBranchNodePath(BaseModel):
+ conditions: JourneyConditionsField
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ nodes: List["JourneyNode"]
+
+ label: Optional[str] = None
+
+
+class JourneyBranchNode(BaseModel):
+ """Branch node.
+
+ Routes to the first entry in `paths[]` whose `conditions` match, else falls through to `default.nodes`.
+ """
+
+ default: JourneyBranchNodeDefault
+
+ paths: List[JourneyBranchNodePath]
+
+ type: Literal["branch"]
+
+ id: Optional[str] = None
+
+
+JourneyNode: TypeAlias = Union[
+ JourneyAPIInvokeTriggerNode,
+ JourneySegmentTriggerNode,
+ JourneySendNode,
+ JourneyDelayDurationNode,
+ JourneyDelayUntilNode,
+ JourneyFetchGetDeleteNode,
+ JourneyFetchPostPutNode,
+ JourneyAINode,
+ JourneyThrottleStaticNode,
+ JourneyThrottleDynamicNode,
+ JourneyExitNode,
+ JourneyBranchNode,
+]
diff --git a/src/courier/types/journey_node_param.py b/src/courier/types/journey_node_param.py
new file mode 100644
index 00000000..65b0ef11
--- /dev/null
+++ b/src/courier/types/journey_node_param.py
@@ -0,0 +1,71 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union, Iterable
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+from .journey_ai_node_param import JourneyAINodeParam
+from .journey_exit_node_param import JourneyExitNodeParam
+from .journey_send_node_param import JourneySendNodeParam
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+from .journey_delay_until_node_param import JourneyDelayUntilNodeParam
+from .journey_delay_duration_node_param import JourneyDelayDurationNodeParam
+from .journey_fetch_post_put_node_param import JourneyFetchPostPutNodeParam
+from .journey_segment_trigger_node_param import JourneySegmentTriggerNodeParam
+from .journey_throttle_static_node_param import JourneyThrottleStaticNodeParam
+from .journey_fetch_get_delete_node_param import JourneyFetchGetDeleteNodeParam
+from .journey_throttle_dynamic_node_param import JourneyThrottleDynamicNodeParam
+from .journey_api_invoke_trigger_node_param import JourneyAPIInvokeTriggerNodeParam
+
+__all__ = ["JourneyNodeParam", "JourneyBranchNode", "JourneyBranchNodeDefault", "JourneyBranchNodePath"]
+
+
+class JourneyBranchNodeDefault(TypedDict, total=False):
+ nodes: Required[Iterable["JourneyNodeParam"]]
+
+ label: str
+
+
+class JourneyBranchNodePath(TypedDict, total=False):
+ conditions: Required[JourneyConditionsFieldParam]
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ nodes: Required[Iterable["JourneyNodeParam"]]
+
+ label: str
+
+
+class JourneyBranchNode(TypedDict, total=False):
+ """Branch node.
+
+ Routes to the first entry in `paths[]` whose `conditions` match, else falls through to `default.nodes`.
+ """
+
+ default: Required[JourneyBranchNodeDefault]
+
+ paths: Required[Iterable[JourneyBranchNodePath]]
+
+ type: Required[Literal["branch"]]
+
+ id: str
+
+
+JourneyNodeParam: TypeAlias = Union[
+ JourneyAPIInvokeTriggerNodeParam,
+ JourneySegmentTriggerNodeParam,
+ JourneySendNodeParam,
+ JourneyDelayDurationNodeParam,
+ JourneyDelayUntilNodeParam,
+ JourneyFetchGetDeleteNodeParam,
+ JourneyFetchPostPutNodeParam,
+ JourneyAINodeParam,
+ JourneyThrottleStaticNodeParam,
+ JourneyThrottleDynamicNodeParam,
+ JourneyExitNodeParam,
+ JourneyBranchNode,
+]
diff --git a/src/courier/types/journey_publish_params.py b/src/courier/types/journey_publish_params.py
new file mode 100644
index 00000000..2c6fcb7f
--- /dev/null
+++ b/src/courier/types/journey_publish_params.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["JourneyPublishParams"]
+
+
+class JourneyPublishParams(TypedDict, total=False):
+ version: str
diff --git a/src/courier/types/journey_replace_params.py b/src/courier/types/journey_replace_params.py
new file mode 100644
index 00000000..f19a65f1
--- /dev/null
+++ b/src/courier/types/journey_replace_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Required, TypedDict
+
+from .journey_state import JourneyState
+
+__all__ = ["JourneyReplaceParams"]
+
+
+class JourneyReplaceParams(TypedDict, total=False):
+ name: Required[str]
+
+ nodes: Required[Iterable["JourneyNodeParam"]]
+
+ enabled: bool
+
+ state: JourneyState
+ """Lifecycle state of a journey."""
+
+
+from .journey_node_param import JourneyNodeParam
diff --git a/src/courier/types/journey_response.py b/src/courier/types/journey_response.py
new file mode 100644
index 00000000..afa83bf2
--- /dev/null
+++ b/src/courier/types/journey_response.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+
+from .._models import BaseModel
+from .journey_state import JourneyState
+
+__all__ = ["JourneyResponse"]
+
+
+class JourneyResponse(BaseModel):
+ """A journey, with its current draft or published nodes and metadata."""
+
+ id: str
+
+ created: Optional[int] = None
+
+ creator: Optional[str] = None
+
+ enabled: bool
+
+ name: str
+
+ nodes: List["JourneyNode"]
+
+ published: Optional[int] = None
+
+ state: JourneyState
+ """Lifecycle state of a journey."""
+
+ updated: Optional[int] = None
+
+ updater: Optional[str] = None
+
+
+from .journey_node import JourneyNode
diff --git a/src/courier/types/journey_retrieve_params.py b/src/courier/types/journey_retrieve_params.py
new file mode 100644
index 00000000..b2f5528b
--- /dev/null
+++ b/src/courier/types/journey_retrieve_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["JourneyRetrieveParams"]
+
+
+class JourneyRetrieveParams(TypedDict, total=False):
+ version: str
+ """Version selector: `draft`, `published` (default), or `vN`."""
diff --git a/src/courier/types/journey_segment_trigger_node.py b/src/courier/types/journey_segment_trigger_node.py
new file mode 100644
index 00000000..74fb1866
--- /dev/null
+++ b/src/courier/types/journey_segment_trigger_node.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneySegmentTriggerNode"]
+
+
+class JourneySegmentTriggerNode(BaseModel):
+ """Trigger fired by a segment event (`identify`, `group`, or `track`)."""
+
+ request_type: Literal["identify", "group", "track"]
+
+ trigger_type: Literal["segment"]
+
+ type: Literal["trigger"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ event_id: Optional[str] = None
diff --git a/src/courier/types/journey_segment_trigger_node_param.py b/src/courier/types/journey_segment_trigger_node_param.py
new file mode 100644
index 00000000..2dcbba28
--- /dev/null
+++ b/src/courier/types/journey_segment_trigger_node_param.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneySegmentTriggerNodeParam"]
+
+
+class JourneySegmentTriggerNodeParam(TypedDict, total=False):
+ """Trigger fired by a segment event (`identify`, `group`, or `track`)."""
+
+ request_type: Required[Literal["identify", "group", "track"]]
+
+ trigger_type: Required[Literal["segment"]]
+
+ type: Required[Literal["trigger"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
+
+ event_id: str
diff --git a/src/courier/types/journey_send_node.py b/src/courier/types/journey_send_node.py
new file mode 100644
index 00000000..34d87ef9
--- /dev/null
+++ b/src/courier/types/journey_send_node.py
@@ -0,0 +1,53 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneySendNode", "Message", "MessageDelay", "MessageTo"]
+
+
+class MessageDelay(BaseModel):
+ until: str
+
+ timezone: Optional[str] = None
+
+
+class MessageTo(BaseModel):
+ email_override: Optional[str] = None
+
+ phone_number_override: Optional[str] = None
+
+ user_id_override: Optional[str] = None
+
+
+class Message(BaseModel):
+ template: str
+
+ data: Optional[Dict[str, object]] = None
+
+ delay: Optional[MessageDelay] = None
+
+ to: Optional[MessageTo] = None
+
+
+class JourneySendNode(BaseModel):
+ """Send a notification template to the recipient.
+
+ Optionally override the recipient address, delay the send, or attach `data`.
+ """
+
+ message: Message
+
+ type: Literal["send"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_send_node_param.py b/src/courier/types/journey_send_node_param.py
new file mode 100644
index 00000000..2d6e33a8
--- /dev/null
+++ b/src/courier/types/journey_send_node_param.py
@@ -0,0 +1,54 @@
+# 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 Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneySendNodeParam", "Message", "MessageDelay", "MessageTo"]
+
+
+class MessageDelay(TypedDict, total=False):
+ until: Required[str]
+
+ timezone: str
+
+
+class MessageTo(TypedDict, total=False):
+ email_override: str
+
+ phone_number_override: str
+
+ user_id_override: str
+
+
+class Message(TypedDict, total=False):
+ template: Required[str]
+
+ data: Dict[str, object]
+
+ delay: MessageDelay
+
+ to: MessageTo
+
+
+class JourneySendNodeParam(TypedDict, total=False):
+ """Send a notification template to the recipient.
+
+ Optionally override the recipient address, delay the send, or attach `data`.
+ """
+
+ message: Required[Message]
+
+ type: Required[Literal["send"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_state.py b/src/courier/types/journey_state.py
new file mode 100644
index 00000000..3aa34acb
--- /dev/null
+++ b/src/courier/types/journey_state.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__ = ["JourneyState"]
+
+JourneyState: TypeAlias = Literal["DRAFT", "PUBLISHED"]
diff --git a/src/courier/types/journey_template_get_response.py b/src/courier/types/journey_template_get_response.py
new file mode 100644
index 00000000..11137390
--- /dev/null
+++ b/src/courier/types/journey_template_get_response.py
@@ -0,0 +1,51 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.elemental_node import ElementalNode
+
+__all__ = ["JourneyTemplateGetResponse", "Brand", "Content", "Subscription"]
+
+
+class Brand(BaseModel):
+ id: str
+
+
+class Content(BaseModel):
+ elements: List[ElementalNode]
+
+ version: Literal["2022-01-01"]
+
+ scope: Optional[Literal["default", "strict"]] = None
+
+
+class Subscription(BaseModel):
+ topic_id: str
+
+
+class JourneyTemplateGetResponse(BaseModel):
+ """A journey-scoped notification template."""
+
+ id: str
+
+ brand: Optional[Brand] = None
+
+ content: Content
+
+ created: int
+
+ creator: str
+
+ name: str
+
+ state: Literal["DRAFT", "PUBLISHED"]
+
+ subscription: Optional[Subscription] = None
+
+ tags: List[str]
+
+ updated: Optional[int] = None
+
+ updater: Optional[str] = None
diff --git a/src/courier/types/journey_template_list_response.py b/src/courier/types/journey_template_list_response.py
new file mode 100644
index 00000000..3f8a2feb
--- /dev/null
+++ b/src/courier/types/journey_template_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 .shared.paging import Paging
+from .journey_template_summary import JourneyTemplateSummary
+
+__all__ = ["JourneyTemplateListResponse"]
+
+
+class JourneyTemplateListResponse(BaseModel):
+ """Paged list of journey-scoped notification templates."""
+
+ paging: Paging
+
+ results: List[JourneyTemplateSummary]
diff --git a/src/courier/types/journey_template_summary.py b/src/courier/types/journey_template_summary.py
new file mode 100644
index 00000000..d8b19186
--- /dev/null
+++ b/src/courier/types/journey_template_summary.py
@@ -0,0 +1,29 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+
+__all__ = ["JourneyTemplateSummary"]
+
+
+class JourneyTemplateSummary(BaseModel):
+ """
+ Summary fields of a journey-scoped notification template returned in list responses.
+ """
+
+ id: str
+
+ created: int
+
+ creator: str
+
+ name: str
+
+ state: str
+
+ tags: List[str]
+
+ updated: Optional[int] = None
+
+ updater: Optional[str] = None
diff --git a/src/courier/types/journey_throttle_dynamic_node.py b/src/courier/types/journey_throttle_dynamic_node.py
new file mode 100644
index 00000000..cea48b8d
--- /dev/null
+++ b/src/courier/types/journey_throttle_dynamic_node.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyThrottleDynamicNode"]
+
+
+class JourneyThrottleDynamicNode(BaseModel):
+ """
+ Throttle the journey by a dynamic `throttle_key`, allowing at most `max_allowed` invocations per `period`.
+ """
+
+ max_allowed: int
+
+ period: str
+
+ scope: Literal["dynamic"]
+
+ throttle_key: str
+
+ type: Literal["throttle"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_throttle_dynamic_node_param.py b/src/courier/types/journey_throttle_dynamic_node_param.py
new file mode 100644
index 00000000..19124e71
--- /dev/null
+++ b/src/courier/types/journey_throttle_dynamic_node_param.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyThrottleDynamicNodeParam"]
+
+
+class JourneyThrottleDynamicNodeParam(TypedDict, total=False):
+ """
+ Throttle the journey by a dynamic `throttle_key`, allowing at most `max_allowed` invocations per `period`.
+ """
+
+ max_allowed: Required[int]
+
+ period: Required[str]
+
+ scope: Required[Literal["dynamic"]]
+
+ throttle_key: Required[str]
+
+ type: Required[Literal["throttle"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_throttle_static_node.py b/src/courier/types/journey_throttle_static_node.py
new file mode 100644
index 00000000..b31ca062
--- /dev/null
+++ b/src/courier/types/journey_throttle_static_node.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .journey_conditions_field import JourneyConditionsField
+
+__all__ = ["JourneyThrottleStaticNode"]
+
+
+class JourneyThrottleStaticNode(BaseModel):
+ """
+ Throttle the journey by a static `scope` (`user` or `global`), allowing at most `max_allowed` invocations per `period`.
+ """
+
+ max_allowed: int
+
+ period: str
+
+ scope: Literal["user", "global"]
+
+ type: Literal["throttle"]
+
+ id: Optional[str] = None
+
+ conditions: Optional[JourneyConditionsField] = None
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_throttle_static_node_param.py b/src/courier/types/journey_throttle_static_node_param.py
new file mode 100644
index 00000000..780c68a4
--- /dev/null
+++ b/src/courier/types/journey_throttle_static_node_param.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .journey_conditions_field_param import JourneyConditionsFieldParam
+
+__all__ = ["JourneyThrottleStaticNodeParam"]
+
+
+class JourneyThrottleStaticNodeParam(TypedDict, total=False):
+ """
+ Throttle the journey by a static `scope` (`user` or `global`), allowing at most `max_allowed` invocations per `period`.
+ """
+
+ max_allowed: Required[int]
+
+ period: Required[str]
+
+ scope: Required[Literal["user", "global"]]
+
+ type: Required[Literal["throttle"]]
+
+ id: str
+
+ conditions: JourneyConditionsFieldParam
+ """Condition spec for a journey node.
+
+ Accepts a single condition atom, an AND/OR group, or an AND/OR nested group.
+ Omit the `conditions` property entirely to express "no conditions".
+ """
diff --git a/src/courier/types/journey_version_item.py b/src/courier/types/journey_version_item.py
new file mode 100644
index 00000000..722470da
--- /dev/null
+++ b/src/courier/types/journey_version_item.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["JourneyVersionItem"]
+
+
+class JourneyVersionItem(BaseModel):
+ """A published version of a journey."""
+
+ created: Optional[int] = None
+
+ creator: Optional[str] = None
+
+ name: str
+
+ published: Optional[int] = None
+
+ version: str
diff --git a/src/courier/types/journey_versions_list_response.py b/src/courier/types/journey_versions_list_response.py
new file mode 100644
index 00000000..ad51098f
--- /dev/null
+++ b/src/courier/types/journey_versions_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 .shared.paging import Paging
+from .journey_version_item import JourneyVersionItem
+
+__all__ = ["JourneyVersionsListResponse"]
+
+
+class JourneyVersionsListResponse(BaseModel):
+ """Paged list of published journey versions, most recent first."""
+
+ paging: Paging
+
+ results: List[JourneyVersionItem]
diff --git a/src/courier/types/journeys/__init__.py b/src/courier/types/journeys/__init__.py
new file mode 100644
index 00000000..42cd2530
--- /dev/null
+++ b/src/courier/types/journeys/__init__.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .template_list_params import TemplateListParams as TemplateListParams
+from .template_create_params import TemplateCreateParams as TemplateCreateParams
+from .template_publish_params import TemplatePublishParams as TemplatePublishParams
+from .template_replace_params import TemplateReplaceParams as TemplateReplaceParams
diff --git a/src/courier/types/journeys/template_create_params.py b/src/courier/types/journeys/template_create_params.py
new file mode 100644
index 00000000..e3a4bb80
--- /dev/null
+++ b/src/courier/types/journeys/template_create_params.py
@@ -0,0 +1,56 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable, Optional
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from ..._types import SequenceNotStr
+from ..._utils import PropertyInfo
+from ..shared_params.elemental_node import ElementalNode
+
+__all__ = [
+ "TemplateCreateParams",
+ "Notification",
+ "NotificationBrand",
+ "NotificationContent",
+ "NotificationSubscription",
+]
+
+
+class TemplateCreateParams(TypedDict, total=False):
+ channel: Required[str]
+
+ notification: Required[Notification]
+
+ provider_key: Annotated[str, PropertyInfo(alias="providerKey")]
+
+ state: str
+
+
+class NotificationBrand(TypedDict, total=False):
+ id: Required[str]
+
+
+class NotificationContent(TypedDict, total=False):
+ elements: Required[Iterable[ElementalNode]]
+
+ version: Required[Literal["2022-01-01"]]
+
+ scope: Literal["default", "strict"]
+
+
+class NotificationSubscription(TypedDict, total=False):
+ topic_id: Required[str]
+
+
+class Notification(TypedDict, total=False):
+ brand: Required[Optional[NotificationBrand]]
+
+ content: Required[NotificationContent]
+
+ name: Required[str]
+
+ subscription: Required[Optional[NotificationSubscription]]
+
+ tags: Required[SequenceNotStr[str]]
diff --git a/src/courier/types/journeys/template_list_params.py b/src/courier/types/journeys/template_list_params.py
new file mode 100644
index 00000000..3c30e48c
--- /dev/null
+++ b/src/courier/types/journeys/template_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["TemplateListParams"]
+
+
+class TemplateListParams(TypedDict, total=False):
+ cursor: str
+ """Pagination cursor from a prior response."""
+
+ limit: int
+ """Page size. Minimum 1, maximum 100."""
diff --git a/src/courier/types/journeys/template_publish_params.py b/src/courier/types/journeys/template_publish_params.py
new file mode 100644
index 00000000..efbfdd7a
--- /dev/null
+++ b/src/courier/types/journeys/template_publish_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["TemplatePublishParams"]
+
+
+class TemplatePublishParams(TypedDict, total=False):
+ template_id: Required[Annotated[str, PropertyInfo(alias="templateId")]]
+
+ version: str
diff --git a/src/courier/types/journeys/template_replace_params.py b/src/courier/types/journeys/template_replace_params.py
new file mode 100644
index 00000000..a607b37d
--- /dev/null
+++ b/src/courier/types/journeys/template_replace_params.py
@@ -0,0 +1,54 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable, Optional
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from ..._types import SequenceNotStr
+from ..._utils import PropertyInfo
+from ..shared_params.elemental_node import ElementalNode
+
+__all__ = [
+ "TemplateReplaceParams",
+ "Notification",
+ "NotificationBrand",
+ "NotificationContent",
+ "NotificationSubscription",
+]
+
+
+class TemplateReplaceParams(TypedDict, total=False):
+ template_id: Required[Annotated[str, PropertyInfo(alias="templateId")]]
+
+ notification: Required[Notification]
+
+ state: str
+
+
+class NotificationBrand(TypedDict, total=False):
+ id: Required[str]
+
+
+class NotificationContent(TypedDict, total=False):
+ elements: Required[Iterable[ElementalNode]]
+
+ version: Required[Literal["2022-01-01"]]
+
+ scope: Literal["default", "strict"]
+
+
+class NotificationSubscription(TypedDict, total=False):
+ topic_id: Required[str]
+
+
+class Notification(TypedDict, total=False):
+ brand: Required[Optional[NotificationBrand]]
+
+ content: Required[NotificationContent]
+
+ name: Required[str]
+
+ subscription: Required[Optional[NotificationSubscription]]
+
+ tags: Required[SequenceNotStr[str]]
diff --git a/tests/api_resources/journeys/__init__.py b/tests/api_resources/journeys/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/journeys/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/journeys/test_templates.py b/tests/api_resources/journeys/test_templates.py
new file mode 100644
index 00000000..57ec626c
--- /dev/null
+++ b/tests/api_resources/journeys/test_templates.py
@@ -0,0 +1,1062 @@
+# 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 (
+ JourneyTemplateGetResponse,
+ JourneyTemplateListResponse,
+ NotificationTemplateVersionListResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestTemplates:
+ 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:
+ template = client.journeys.templates.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Courier) -> None:
+ template = client.journeys.templates.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [
+ {
+ "channels": ["string"],
+ "if": "if",
+ "loop": "loop",
+ "ref": "ref",
+ "type": "text",
+ }
+ ],
+ "version": "2022-01-01",
+ "scope": "default",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ provider_key="x",
+ state="state",
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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 `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.create(
+ template_id="",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Courier) -> None:
+ template = client.journeys.templates.retrieve(
+ notification_id="x",
+ template_id="x",
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.retrieve(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.retrieve(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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 `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.retrieve(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ client.journeys.templates.with_raw_response.retrieve(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Courier) -> None:
+ template = client.journeys.templates.list(
+ template_id="x",
+ )
+ assert_matches_type(JourneyTemplateListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Courier) -> None:
+ template = client.journeys.templates.list(
+ template_id="x",
+ cursor="cursor",
+ limit=1,
+ )
+ assert_matches_type(JourneyTemplateListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.list(
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(JourneyTemplateListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.list(
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(JourneyTemplateListResponse, template, 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 `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.list(
+ template_id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_archive(self, client: Courier) -> None:
+ template = client.journeys.templates.archive(
+ notification_id="x",
+ template_id="x",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_archive(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.archive(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_archive(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.archive(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert template is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_archive(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.archive(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ client.journeys.templates.with_raw_response.archive(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_versions(self, client: Courier) -> None:
+ template = client.journeys.templates.list_versions(
+ notification_id="x",
+ template_id="x",
+ )
+ assert_matches_type(NotificationTemplateVersionListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_versions(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.list_versions(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(NotificationTemplateVersionListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_versions(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.list_versions(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(NotificationTemplateVersionListResponse, template, 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_versions(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.list_versions(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ client.journeys.templates.with_raw_response.list_versions(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_publish(self, client: Courier) -> None:
+ template = client.journeys.templates.publish(
+ notification_id="x",
+ template_id="x",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_publish_with_all_params(self, client: Courier) -> None:
+ template = client.journeys.templates.publish(
+ notification_id="x",
+ template_id="x",
+ version="v321669910225",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_publish(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.publish(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_publish(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.publish(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert template is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_publish(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.publish(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ client.journeys.templates.with_raw_response.publish(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace(self, client: Courier) -> None:
+ template = client.journeys.templates.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace_with_all_params(self, client: Courier) -> None:
+ template = client.journeys.templates.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [
+ {
+ "channels": ["string"],
+ "if": "if",
+ "loop": "loop",
+ "ref": "ref",
+ "type": "text",
+ }
+ ],
+ "version": "2022-01-01",
+ "scope": "default",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ state="state",
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_replace(self, client: Courier) -> None:
+ response = client.journeys.templates.with_raw_response.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_replace(self, client: Courier) -> None:
+ with client.journeys.templates.with_streaming_response.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_replace(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.templates.with_raw_response.replace(
+ notification_id="x",
+ template_id="",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ client.journeys.templates.with_raw_response.replace(
+ notification_id="",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+
+class TestAsyncTemplates:
+ 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:
+ template = await async_client.journeys.templates.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, 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:
+ template = await async_client.journeys.templates.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [
+ {
+ "channels": ["string"],
+ "if": "if",
+ "loop": "loop",
+ "ref": "ref",
+ "type": "text",
+ }
+ ],
+ "version": "2022-01-01",
+ "scope": "default",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ provider_key="x",
+ state="state",
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, 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.journeys.templates.with_raw_response.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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.journeys.templates.with_streaming_response.create(
+ template_id="x",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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 `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.create(
+ template_id="",
+ channel="email",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "Welcome email",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.retrieve(
+ notification_id="x",
+ template_id="x",
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, 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.journeys.templates.with_raw_response.retrieve(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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.journeys.templates.with_streaming_response.retrieve(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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 `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.retrieve(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.retrieve(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.list(
+ template_id="x",
+ )
+ assert_matches_type(JourneyTemplateListResponse, template, 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:
+ template = await async_client.journeys.templates.list(
+ template_id="x",
+ cursor="cursor",
+ limit=1,
+ )
+ assert_matches_type(JourneyTemplateListResponse, template, 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.journeys.templates.with_raw_response.list(
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateListResponse, template, 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.journeys.templates.with_streaming_response.list(
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateListResponse, template, 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 `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.list(
+ template_id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_archive(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.archive(
+ notification_id="x",
+ template_id="x",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_archive(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.templates.with_raw_response.archive(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_archive(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.templates.with_streaming_response.archive(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert template is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_archive(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.archive(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.archive(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_versions(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.list_versions(
+ notification_id="x",
+ template_id="x",
+ )
+ assert_matches_type(NotificationTemplateVersionListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_versions(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.templates.with_raw_response.list_versions(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(NotificationTemplateVersionListResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_versions(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.templates.with_streaming_response.list_versions(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(NotificationTemplateVersionListResponse, template, 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_versions(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.list_versions(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.list_versions(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_publish(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.publish(
+ notification_id="x",
+ template_id="x",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_publish_with_all_params(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.publish(
+ notification_id="x",
+ template_id="x",
+ version="v321669910225",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_publish(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.templates.with_raw_response.publish(
+ notification_id="x",
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_publish(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.templates.with_streaming_response.publish(
+ notification_id="x",
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert template is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_publish(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.publish(
+ notification_id="x",
+ template_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.publish(
+ notification_id="",
+ template_id="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) -> None:
+ template = await async_client.journeys.templates.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [
+ {
+ "channels": ["string"],
+ "if": "if",
+ "loop": "loop",
+ "ref": "ref",
+ "type": "text",
+ }
+ ],
+ "version": "2022-01-01",
+ "scope": "default",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ state="state",
+ )
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_replace(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.templates.with_raw_response.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_replace(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.templates.with_streaming_response.replace(
+ notification_id="x",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert_matches_type(JourneyTemplateGetResponse, template, 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_replace(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.replace(
+ notification_id="x",
+ template_id="",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `notification_id` but received ''"):
+ await async_client.journeys.templates.with_raw_response.replace(
+ notification_id="",
+ template_id="x",
+ notification={
+ "brand": {"id": "id"},
+ "content": {
+ "elements": [{}],
+ "version": "2022-01-01",
+ },
+ "name": "name",
+ "subscription": {"topic_id": "topic_id"},
+ "tags": ["string"],
+ },
+ )
diff --git a/tests/api_resources/tenants/test_templates.py b/tests/api_resources/tenants/test_templates.py
index 978445d9..6a9c7e48 100644
--- a/tests/api_resources/tenants/test_templates.py
+++ b/tests/api_resources/tenants/test_templates.py
@@ -128,6 +128,58 @@ def test_path_params_list(self, client: Courier) -> None:
tenant_id="",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Courier) -> None:
+ template = client.tenants.templates.delete(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Courier) -> None:
+ response = client.tenants.templates.with_raw_response.delete(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = response.parse()
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Courier) -> None:
+ with client.tenants.templates.with_streaming_response.delete(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = response.parse()
+ assert template is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.templates.with_raw_response.delete(
+ template_id="template_id",
+ tenant_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.tenants.templates.with_raw_response.delete(
+ template_id="",
+ tenant_id="tenant_id",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_publish(self, client: Courier) -> None:
@@ -448,6 +500,58 @@ async def test_path_params_list(self, async_client: AsyncCourier) -> None:
tenant_id="",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncCourier) -> None:
+ template = await async_client.tenants.templates.delete(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncCourier) -> None:
+ response = await async_client.tenants.templates.with_raw_response.delete(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ template = await response.parse()
+ assert template is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncCourier) -> None:
+ async with async_client.tenants.templates.with_streaming_response.delete(
+ template_id="template_id",
+ tenant_id="tenant_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ template = await response.parse()
+ assert template is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.templates.with_raw_response.delete(
+ template_id="template_id",
+ tenant_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.tenants.templates.with_raw_response.delete(
+ template_id="",
+ tenant_id="tenant_id",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_publish(self, async_client: AsyncCourier) -> None:
diff --git a/tests/api_resources/test_journeys.py b/tests/api_resources/test_journeys.py
index 2c44578c..36dca1fe 100644
--- a/tests/api_resources/test_journeys.py
+++ b/tests/api_resources/test_journeys.py
@@ -9,7 +9,12 @@
from courier import Courier, AsyncCourier
from tests.utils import assert_matches_type
-from courier.types import JourneysListResponse, JourneysInvokeResponse
+from courier.types import (
+ JourneyResponse,
+ JourneysListResponse,
+ JourneysInvokeResponse,
+ JourneyVersionsListResponse,
+)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -17,6 +22,147 @@
class TestJourneys:
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:
+ journey = client.journeys.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ ],
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Courier) -> None:
+ journey = client.journeys.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ "id": "trigger-1",
+ "conditions": ["string", "string"],
+ "schema": {"foo": "bar"},
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ "id": "send-1",
+ "conditions": ["string", "string"],
+ "schema": {"foo": "bar"},
+ },
+ ],
+ enabled=True,
+ state="DRAFT",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Courier) -> None:
+ response = client.journeys.with_raw_response.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ ],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Courier) -> None:
+ with client.journeys.with_streaming_response.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ ],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Courier) -> None:
+ journey = client.journeys.retrieve(
+ template_id="x",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Courier) -> None:
+ journey = client.journeys.retrieve(
+ template_id="x",
+ version="published",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Courier) -> None:
+ response = client.journeys.with_raw_response.retrieve(
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Courier) -> None:
+ with client.journeys.with_streaming_response.retrieve(
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, 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 `template_id` but received ''"):
+ client.journeys.with_raw_response.retrieve(
+ template_id="",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_list(self, client: Courier) -> None:
@@ -54,6 +200,48 @@ def test_streaming_response_list(self, client: Courier) -> None:
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_archive(self, client: Courier) -> None:
+ journey = client.journeys.archive(
+ "x",
+ )
+ assert journey is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_archive(self, client: Courier) -> None:
+ response = client.journeys.with_raw_response.archive(
+ "x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = response.parse()
+ assert journey is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_archive(self, client: Courier) -> None:
+ with client.journeys.with_streaming_response.archive(
+ "x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = response.parse()
+ assert journey is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_archive(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.with_raw_response.archive(
+ "",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_invoke(self, client: Courier) -> None:
@@ -110,12 +298,336 @@ def test_path_params_invoke(self, client: Courier) -> None:
template_id="",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_versions(self, client: Courier) -> None:
+ journey = client.journeys.list_versions(
+ "x",
+ )
+ assert_matches_type(JourneyVersionsListResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_versions(self, client: Courier) -> None:
+ response = client.journeys.with_raw_response.list_versions(
+ "x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = response.parse()
+ assert_matches_type(JourneyVersionsListResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_versions(self, client: Courier) -> None:
+ with client.journeys.with_streaming_response.list_versions(
+ "x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = response.parse()
+ assert_matches_type(JourneyVersionsListResponse, journey, 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_versions(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.with_raw_response.list_versions(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_publish(self, client: Courier) -> None:
+ journey = client.journeys.publish(
+ template_id="x",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_publish_with_all_params(self, client: Courier) -> None:
+ journey = client.journeys.publish(
+ template_id="x",
+ version="v321669910225",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_publish(self, client: Courier) -> None:
+ response = client.journeys.with_raw_response.publish(
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_publish(self, client: Courier) -> None:
+ with client.journeys.with_streaming_response.publish(
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_publish(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.with_raw_response.publish(
+ template_id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace(self, client: Courier) -> None:
+ journey = client.journeys.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_replace_with_all_params(self, client: Courier) -> None:
+ journey = client.journeys.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ "id": "x",
+ "conditions": ["string", "string"],
+ "schema": {"foo": "bar"},
+ }
+ ],
+ enabled=True,
+ state="DRAFT",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_replace(self, client: Courier) -> None:
+ response = client.journeys.with_raw_response.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_replace(self, client: Courier) -> None:
+ with client.journeys.with_streaming_response.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_replace(self, client: Courier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ client.journeys.with_raw_response.replace(
+ template_id="",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ )
+
class TestAsyncJourneys:
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:
+ journey = await async_client.journeys.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ ],
+ )
+ assert_matches_type(JourneyResponse, journey, 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:
+ journey = await async_client.journeys.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ "id": "trigger-1",
+ "conditions": ["string", "string"],
+ "schema": {"foo": "bar"},
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ "id": "send-1",
+ "conditions": ["string", "string"],
+ "schema": {"foo": "bar"},
+ },
+ ],
+ enabled=True,
+ state="DRAFT",
+ )
+ assert_matches_type(JourneyResponse, journey, 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.journeys.with_raw_response.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ ],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, 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.journeys.with_streaming_response.create(
+ name="Welcome Journey",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ },
+ ],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.retrieve(
+ template_id="x",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.retrieve(
+ template_id="x",
+ version="published",
+ )
+ assert_matches_type(JourneyResponse, journey, 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.journeys.with_raw_response.retrieve(
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, 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.journeys.with_streaming_response.retrieve(
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, 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 `template_id` but received ''"):
+ await async_client.journeys.with_raw_response.retrieve(
+ template_id="",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncCourier) -> None:
@@ -153,6 +665,48 @@ async def test_streaming_response_list(self, async_client: AsyncCourier) -> None
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_archive(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.archive(
+ "x",
+ )
+ assert journey is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_archive(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.with_raw_response.archive(
+ "x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = await response.parse()
+ assert journey is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_archive(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.with_streaming_response.archive(
+ "x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = await response.parse()
+ assert journey is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_archive(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.with_raw_response.archive(
+ "",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_invoke(self, async_client: AsyncCourier) -> None:
@@ -208,3 +762,186 @@ async def test_path_params_invoke(self, async_client: AsyncCourier) -> None:
await async_client.journeys.with_raw_response.invoke(
template_id="",
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_versions(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.list_versions(
+ "x",
+ )
+ assert_matches_type(JourneyVersionsListResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_versions(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.with_raw_response.list_versions(
+ "x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = await response.parse()
+ assert_matches_type(JourneyVersionsListResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_versions(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.with_streaming_response.list_versions(
+ "x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = await response.parse()
+ assert_matches_type(JourneyVersionsListResponse, journey, 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_versions(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.with_raw_response.list_versions(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_publish(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.publish(
+ template_id="x",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_publish_with_all_params(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.publish(
+ template_id="x",
+ version="v321669910225",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_publish(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.with_raw_response.publish(
+ template_id="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_publish(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.with_streaming_response.publish(
+ template_id="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, 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_publish(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.with_raw_response.publish(
+ template_id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_replace_with_all_params(self, async_client: AsyncCourier) -> None:
+ journey = await async_client.journeys.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ "id": "x",
+ "conditions": ["string", "string"],
+ "schema": {"foo": "bar"},
+ }
+ ],
+ enabled=True,
+ state="DRAFT",
+ )
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_replace(self, async_client: AsyncCourier) -> None:
+ response = await async_client.journeys.with_raw_response.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_replace(self, async_client: AsyncCourier) -> None:
+ async with async_client.journeys.with_streaming_response.replace(
+ template_id="x",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ journey = await response.parse()
+ assert_matches_type(JourneyResponse, journey, 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_replace(self, async_client: AsyncCourier) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `template_id` but received ''"):
+ await async_client.journeys.with_raw_response.replace(
+ template_id="",
+ name="Welcome Journey v2",
+ nodes=[
+ {
+ "trigger_type": "api-invoke",
+ "type": "trigger",
+ }
+ ],
+ )
diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py
deleted file mode 100644
index 19b4fa50..00000000
--- a/tests/test_deepcopy.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from courier._utils import deepcopy_minimal
-
-
-def assert_different_identities(obj1: object, obj2: object) -> None:
- assert obj1 == obj2
- assert id(obj1) != id(obj2)
-
-
-def test_simple_dict() -> None:
- obj1 = {"foo": "bar"}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_dict() -> None:
- obj1 = {"foo": {"bar": True}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
-
-
-def test_complex_nested_dict() -> None:
- obj1 = {"foo": {"bar": [{"hello": "world"}]}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
- assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"])
- assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0])
-
-
-def test_simple_list() -> None:
- obj1 = ["a", "b", "c"]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_list() -> None:
- obj1 = ["a", [1, 2, 3]]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1[1], obj2[1])
-
-
-class MyObject: ...
-
-
-def test_ignores_other_types() -> None:
- # custom classes
- my_obj = MyObject()
- obj1 = {"foo": my_obj}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert obj1["foo"] is my_obj
-
- # tuples
- obj3 = ("a", "b")
- obj4 = deepcopy_minimal(obj3)
- assert obj3 is obj4
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index b1010d71..21892d26 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -4,7 +4,7 @@
import pytest
-from courier._types import FileTypes
+from courier._types import FileTypes, ArrayFormat
from courier._utils import extract_files
@@ -37,10 +37,7 @@ def test_multiple_files() -> None:
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [
- ("files[]", b"file one"),
- ("files[]", b"file two"),
- ]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
assert query == {"title": "hello"}
@@ -71,3 +68,24 @@ def test_ignores_incorrect_paths(
expected: list[tuple[str, FileTypes]],
) -> None:
assert extract_files(query, paths=paths) == expected
+
+
+@pytest.mark.parametrize(
+ "array_format,expected_top_level,expected_nested",
+ [
+ ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]),
+ ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]),
+ ],
+)
+def test_array_format_controls_file_field_names(
+ array_format: ArrayFormat,
+ expected_top_level: list[tuple[str, FileTypes]],
+ expected_nested: list[tuple[str, FileTypes]],
+) -> None:
+ top_level = {"files": [b"a", b"b"]}
+ assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level
+
+ nested = {"items": [{"file": b"a"}, {"file": b"b"}]}
+ assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested
diff --git a/tests/test_files.py b/tests/test_files.py
index 02c7ea1d..0a711de9 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -4,7 +4,8 @@
import pytest
from dirty_equals import IsDict, IsList, IsBytes, IsTuple
-from courier._files import to_httpx_files, async_to_httpx_files
+from courier._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files
+from courier._utils import extract_files
readme_path = Path(__file__).parent.parent.joinpath("README.md")
@@ -49,3 +50,99 @@ def test_string_not_allowed() -> None:
"file": "foo", # type: ignore
}
)
+
+
+def assert_different_identities(obj1: object, obj2: object) -> None:
+ assert obj1 == obj2
+ assert obj1 is not obj2
+
+
+class TestDeepcopyWithPaths:
+ def test_copies_top_level_dict(self) -> None:
+ original = {"file": b"data", "other": "value"}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+
+ def test_file_value_is_same_reference(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+ assert result["file"] is file_bytes
+
+ def test_list_popped_wholesale(self) -> None:
+ files = [b"f1", b"f2"]
+ original = {"files": files, "title": "t"}
+ result = deepcopy_with_paths(original, [["files", ""]])
+ assert_different_identities(result, original)
+ result_files = result["files"]
+ assert isinstance(result_files, list)
+ assert_different_identities(result_files, files)
+
+ def test_nested_array_path_copies_list_and_elements(self) -> None:
+ elem1 = {"file": b"f1", "extra": 1}
+ elem2 = {"file": b"f2", "extra": 2}
+ original = {"items": [elem1, elem2]}
+ result = deepcopy_with_paths(original, [["items", "", "file"]])
+ assert_different_identities(result, original)
+ result_items = result["items"]
+ assert isinstance(result_items, list)
+ assert_different_identities(result_items, original["items"])
+ assert_different_identities(result_items[0], elem1)
+ assert_different_identities(result_items[1], elem2)
+
+ def test_empty_paths_returns_same_object(self) -> None:
+ original = {"foo": "bar"}
+ result = deepcopy_with_paths(original, [])
+ assert result is original
+
+ def test_multiple_paths(self) -> None:
+ f1 = b"file1"
+ f2 = b"file2"
+ original = {"a": f1, "b": f2, "c": "unchanged"}
+ result = deepcopy_with_paths(original, [["a"], ["b"]])
+ assert_different_identities(result, original)
+ assert result["a"] is f1
+ assert result["b"] is f2
+ assert result["c"] is original["c"]
+
+ def test_extract_files_does_not_mutate_original_top_level(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes, "other": "value"}
+
+ copied = deepcopy_with_paths(original, [["file"]])
+ extracted = extract_files(copied, paths=[["file"]])
+
+ assert extracted == [("file", file_bytes)]
+ assert original == {"file": file_bytes, "other": "value"}
+ assert copied == {"other": "value"}
+
+ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
+ file1 = b"f1"
+ file2 = b"f2"
+ original = {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+
+ copied = deepcopy_with_paths(original, [["items", "", "file"]])
+ extracted = extract_files(copied, paths=[["items", "", "file"]])
+
+ assert [entry for _, entry in extracted] == [file1, file2]
+ assert original == {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+ assert copied == {
+ "items": [
+ {"extra": 1},
+ {"extra": 2},
+ ],
+ "title": "example",
+ }
diff --git a/tests/test_models.py b/tests/test_models.py
index dea68008..61638ab8 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,8 @@
import json
-from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast
from datetime import datetime, timezone
-from typing_extensions import Literal, Annotated, TypeAliasType
+from collections import deque
+from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType
import pytest
import pydantic
@@ -9,7 +10,7 @@
from courier._utils import PropertyInfo
from courier._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from courier._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
+from courier._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type
class BasicModel(BaseModel):
@@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ...
assert model.a.prop == 1
assert isinstance(model.a, Item)
assert model.other == "foo"
+
+
+# NOTE: Workaround for Pydantic Iterable behavior.
+# Iterable fields are replaced with a ValidatorIterator and may be consumed
+# during serialization, which can cause subsequent dumps to return empty data.
+# See: https://github.com/pydantic/pydantic/issues/9541
+@pytest.mark.parametrize(
+ "data, expected_validated",
+ [
+ ([1, 2, 3], [1, 2, 3]),
+ ((1, 2, 3), (1, 2, 3)),
+ (set([1, 2, 3]), set([1, 2, 3])),
+ (iter([1, 2, 3]), [1, 2, 3]),
+ ([], []),
+ ((x for x in [1, 2, 3]), [1, 2, 3]),
+ (map(lambda x: x, [1, 2, 3]), [1, 2, 3]),
+ (frozenset([1, 2, 3]), frozenset([1, 2, 3])),
+ (deque([1, 2, 3]), deque([1, 2, 3])),
+ ],
+ ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"],
+)
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None:
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[int]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": data}})
+ assert m.data["items"] == expected_validated
+
+ # Verify repeated dumps don't lose data (the original bug)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+
+
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction_str_falls_back_to_list() -> None:
+ # str is iterable (over chars), but str(list_of_chars) produces the list's repr
+ # rather than reconstructing a string from items. We special-case str to fall
+ # back to list instead of attempting reconstruction.
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[str]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": "hello"}})
+
+ # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"])
+ assert m.data["items"] == ["h", "e", "l", "l", "o"]
+ assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]