Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ from courier.types import (
ElementalMetaNode,
ElementalMetaNodeWithType,
ElementalNode,
ElementalNodeNonChannel,
ElementalQuoteNode,
ElementalQuoteNodeWithType,
ElementalTextNode,
Expand Down
14 changes: 14 additions & 0 deletions src/courier/resources/journeys/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ def create(
Defaults to `DRAFT`
state; pass `state: "PUBLISHED"` to publish on create.

The content tree must contain exactly one channel block whose `channel` matches
the `channel` on the request — a journey-scoped template carries a single
channel. Top-level elements, or a block for a different channel, return `400`.
The template designer renders only the channel block matching the tab it draws,
so content stored without one cannot be opened. An empty `elements` array is
accepted.

Args:
extra_headers: Send extra headers

Expand Down Expand Up @@ -614,6 +621,13 @@ async def create(
Defaults to `DRAFT`
state; pass `state: "PUBLISHED"` to publish on create.

The content tree must contain exactly one channel block whose `channel` matches
the `channel` on the request — a journey-scoped template carries a single
channel. Top-level elements, or a block for a different channel, return `400`.
The template designer renders only the channel block matching the tab it draws,
so content stored without one cannot be opened. An empty `elements` array is
accepted.

Args:
extra_headers: Send extra headers

Expand Down
20 changes: 20 additions & 0 deletions src/courier/resources/notifications/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ def create(
Requires all fields in the notification object.
Templates are created in draft state by default.

Content must place its elements inside a channel block —
`{ "type": "channel", "channel": "email", "elements": [...] }` — or the request
returns `400`. The template designer renders only the channel block matching the
tab it draws, so content stored without one cannot be opened. An empty
`elements` array is accepted, and the requirement applies to creation only:
`PUT /notifications/{id}` still accepts unwrapped content. Note this endpoint
takes versioned content only — the `{ title, body }` shorthand accepted by
`/send` is rejected here with an `invalid_request_error` on
`notification.content.version`.

Args:
notification: Template fields accepted in POST and PUT request bodies, nested under a
`notification` key.
Expand Down Expand Up @@ -769,6 +779,16 @@ async def create(
Requires all fields in the notification object.
Templates are created in draft state by default.

Content must place its elements inside a channel block —
`{ "type": "channel", "channel": "email", "elements": [...] }` — or the request
returns `400`. The template designer renders only the channel block matching the
tab it draws, so content stored without one cannot be opened. An empty
`elements` array is accepted, and the requirement applies to creation only:
`PUT /notifications/{id}` still accepts unwrapped content. Note this endpoint
takes versioned content only — the `{ title, body }` shorthand accepted by
`/send` is rejected here with an `invalid_request_error` on
`notification.content.version`.

Args:
notification: Template fields accepted in POST and PUT request bodies, nested under a
`notification` key.
Expand Down
20 changes: 20 additions & 0 deletions src/courier/resources/tenants/templates/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ def replace(
Creates or updates a notification template scoped to one tenant, letting a
tenant override the content the workspace template would send.

This is an upsert: it creates when the tenant has no template under
`template_id`, and updates when it does. On the create half, content must place
its elements inside a channel block —
`{ "type": "channel", "channel": "email", "elements": [...] }` — or the request
returns `400`. The template designer renders only the channel block matching the
tab it draws, so content stored without one cannot be opened. An empty
`elements` array is accepted, as is the `{ title, body }` shorthand, which has
no elements to wrap. Updates are not checked, so tenant templates already stored
without a wrapper stay editable.

Args:
template: Template configuration for creating or updating a tenant notification template

Expand Down Expand Up @@ -508,6 +518,16 @@ async def replace(
Creates or updates a notification template scoped to one tenant, letting a
tenant override the content the workspace template would send.

This is an upsert: it creates when the tenant has no template under
`template_id`, and updates when it does. On the create half, content must place
its elements inside a channel block —
`{ "type": "channel", "channel": "email", "elements": [...] }` — or the request
returns `400`. The template designer renders only the channel block matching the
tab it draws, so content stored without one cannot be opened. An empty
`elements` array is accepted, as is the `{ title, body }` shorthand, which has
no elements to wrap. Updates are not checked, so tenant templates already stored
without a wrapper stay editable.

Args:
template: Template configuration for creating or updating a tenant notification template

Expand Down
1 change: 1 addition & 0 deletions src/courier/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
WebhookAuthentication as WebhookAuthentication,
AirshipProfileAudience as AirshipProfileAudience,
SendToMsTeamsChannelID as SendToMsTeamsChannelID,
ElementalNodeNonChannel as ElementalNodeNonChannel,
SendToMsTeamsChannelName as SendToMsTeamsChannelName,
UserProfileFirebaseToken as UserProfileFirebaseToken,
ElementalHTMLNodeWithType as ElementalHTMLNodeWithType,
Expand Down
1 change: 1 addition & 0 deletions src/courier/types/shared/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from .airship_profile_audience import AirshipProfileAudience as AirshipProfileAudience
from .ms_teams_base_properties import MsTeamsBaseProperties as MsTeamsBaseProperties
from .send_to_ms_teams_user_id import SendToMsTeamsUserID as SendToMsTeamsUserID
from .elemental_node_non_channel import ElementalNodeNonChannel as ElementalNodeNonChannel
from .send_to_ms_teams_channel_id import SendToMsTeamsChannelID as SendToMsTeamsChannelID
from .user_profile_firebase_token import UserProfileFirebaseToken as UserProfileFirebaseToken
from .elemental_html_node_with_type import ElementalHTMLNodeWithType as ElementalHTMLNodeWithType
Expand Down
10 changes: 9 additions & 1 deletion src/courier/types/shared/elemental_channel_node.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

from typing import Dict, Optional
from typing import Dict, List, Optional

from .elemental_base_node import ElementalBaseNode
from .elemental_node_non_channel import ElementalNodeNonChannel

__all__ = ["ElementalChannelNode"]

Expand All @@ -18,6 +19,13 @@ class ElementalChannelNode(ElementalBaseNode):
Can be `email`, `push`, `direct_message`, `sms` or a provider such as slack
"""

elements: Optional[List[ElementalNodeNonChannel]] = None
"""An array of elements to apply to the channel.

If `raw` has not been specified, `elements` is `required`. Channel elements
cannot nest, so these are any node except another channel block.
"""

font_size: Optional[str] = None
"""Email only.

Expand Down
75 changes: 75 additions & 0 deletions src/courier/types/shared/elemental_node_non_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

from typing import Union, Optional
from typing_extensions import Literal, TypeAlias

from .elemental_html_node import ElementalHTMLNode
from .elemental_meta_node import ElementalMetaNode
from .elemental_text_node import ElementalTextNode
from .elemental_image_node import ElementalImageNode
from .elemental_quote_node import ElementalQuoteNode
from .elemental_action_node import ElementalActionNode
from .elemental_divider_node import ElementalDividerNode

__all__ = [
"ElementalNodeNonChannel",
"UnionMember0",
"UnionMember1",
"UnionMember2",
"UnionMember3",
"UnionMember4",
"UnionMember5",
"UnionMember6",
]


class UnionMember0(ElementalTextNode):
"""Represents a body of text to be rendered inside of the notification."""

type: Optional[Literal["text"]] = None


class UnionMember1(ElementalMetaNode):
"""
The meta element contains information describing the notification that may be used by a particular channel or provider. One important field is the title field which will be used as the title for channels that support it.
"""

type: Optional[Literal["meta"]] = None


class UnionMember2(ElementalImageNode):
"""Used to embed an image into the notification."""

type: Optional[Literal["image"]] = None


class UnionMember3(ElementalActionNode):
"""Allows the user to execute an action. Can be a button or a link."""

type: Optional[Literal["action"]] = None


class UnionMember4(ElementalDividerNode):
"""Renders a dividing line between elements."""

type: Optional[Literal["divider"]] = None


class UnionMember5(ElementalQuoteNode):
"""Renders a quote block."""

type: Optional[Literal["quote"]] = None


class UnionMember6(ElementalHTMLNode):
"""Raw HTML string inside an Elemental document.

When rendering a message, this node is turned into output only for the email channel; for other channels it produces no blocks.
"""

type: Optional[Literal["html"]] = None


ElementalNodeNonChannel: TypeAlias = Union[
UnionMember0, UnionMember1, UnionMember2, UnionMember3, UnionMember4, UnionMember5, UnionMember6
]
1 change: 1 addition & 0 deletions src/courier/types/shared_params/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from .elemental_content_sugar import ElementalContentSugar as ElementalContentSugar
from .message_routing_channel import MessageRoutingChannel as MessageRoutingChannel
from .send_to_ms_teams_user_id import SendToMsTeamsUserID as SendToMsTeamsUserID
from .elemental_node_non_channel import ElementalNodeNonChannel as ElementalNodeNonChannel
from .send_to_ms_teams_channel_id import SendToMsTeamsChannelID as SendToMsTeamsChannelID
from .elemental_html_node_with_type import ElementalHTMLNodeWithType as ElementalHTMLNodeWithType
from .elemental_meta_node_with_type import ElementalMetaNodeWithType as ElementalMetaNodeWithType
Expand Down
10 changes: 9 additions & 1 deletion src/courier/types/shared_params/elemental_channel_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

from typing import Dict, Optional
from typing import Dict, Iterable, Optional

from .elemental_base_node import ElementalBaseNode
from .elemental_node_non_channel import ElementalNodeNonChannel

__all__ = ["ElementalChannelNode"]

Expand All @@ -20,6 +21,13 @@ class ElementalChannelNode(ElementalBaseNode, total=False):
Can be `email`, `push`, `direct_message`, `sms` or a provider such as slack
"""

elements: Optional[Iterable[ElementalNodeNonChannel]]
"""An array of elements to apply to the channel.

If `raw` has not been specified, `elements` is `required`. Channel elements
cannot nest, so these are any node except another channel block.
"""

font_size: Optional[str]
"""Email only.

Expand Down
77 changes: 77 additions & 0 deletions src/courier/types/shared_params/elemental_node_non_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 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 Literal, TypeAlias

from .elemental_html_node import ElementalHTMLNode
from .elemental_meta_node import ElementalMetaNode
from .elemental_text_node import ElementalTextNode
from .elemental_image_node import ElementalImageNode
from .elemental_quote_node import ElementalQuoteNode
from .elemental_action_node import ElementalActionNode
from .elemental_divider_node import ElementalDividerNode

__all__ = [
"ElementalNodeNonChannel",
"UnionMember0",
"UnionMember1",
"UnionMember2",
"UnionMember3",
"UnionMember4",
"UnionMember5",
"UnionMember6",
]


class UnionMember0(ElementalTextNode, total=False):
"""Represents a body of text to be rendered inside of the notification."""

type: Literal["text"]


class UnionMember1(ElementalMetaNode, total=False):
"""
The meta element contains information describing the notification that may be used by a particular channel or provider. One important field is the title field which will be used as the title for channels that support it.
"""

type: Literal["meta"]


class UnionMember2(ElementalImageNode, total=False):
"""Used to embed an image into the notification."""

type: Literal["image"]


class UnionMember3(ElementalActionNode, total=False):
"""Allows the user to execute an action. Can be a button or a link."""

type: Literal["action"]


class UnionMember4(ElementalDividerNode, total=False):
"""Renders a dividing line between elements."""

type: Literal["divider"]


class UnionMember5(ElementalQuoteNode, total=False):
"""Renders a quote block."""

type: Literal["quote"]


class UnionMember6(ElementalHTMLNode, total=False):
"""Raw HTML string inside an Elemental document.

When rendering a message, this node is turned into output only for the email channel; for other channels it produces no blocks.
"""

type: Literal["html"]


ElementalNodeNonChannel: TypeAlias = Union[
UnionMember0, UnionMember1, UnionMember2, UnionMember3, UnionMember4, UnionMember5, UnionMember6
]
4 changes: 2 additions & 2 deletions tests/api_resources/journeys/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_method_create_with_all_params(self, client: Courier) -> None:
notification={
"brand": {"id": "id"},
"content": {
"elements": [{"type": "text"}],
"elements": [{"type": "channel"}],
"version": "2022-01-01",
"scope": "default",
},
Expand Down Expand Up @@ -777,7 +777,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncCourier) -
notification={
"brand": {"id": "id"},
"content": {
"elements": [{"type": "text"}],
"elements": [{"type": "channel"}],
"version": "2022-01-01",
"scope": "default",
},
Expand Down
4 changes: 2 additions & 2 deletions tests/api_resources/tenants/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def test_method_replace_with_all_params(self, client: Courier) -> None:
tenant_id="tenant_id",
template={
"content": {
"elements": [{"type": "text"}],
"elements": [{"type": "channel"}],
"version": "2022-01-01",
},
"channels": {
Expand Down Expand Up @@ -629,7 +629,7 @@ async def test_method_replace_with_all_params(self, async_client: AsyncCourier)
tenant_id="tenant_id",
template={
"content": {
"elements": [{"type": "text"}],
"elements": [{"type": "channel"}],
"version": "2022-01-01",
},
"channels": {
Expand Down