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"]