From 336a1e3575959396dcd6ecf64d15fa1c40ff7cc8 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 23 Oct 2026 18:31:15 +0300 Subject: [PATCH 1/2] docs(templates): clone answers the new copy's id clone_email_template, clone_sms_template and clone_push_template already return whatever the Hub answers. The gateway now answers an IdResponse ({"id": "..."}) for the three clone requests, so the docstrings and the notifications page say so, with an example. Co-Authored-By: Claude Opus 5.5 --- docs/hub/notifications.md | 11 ++++++++++ src/norbix_python/hub/notifications.py | 30 ++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/hub/notifications.md b/docs/hub/notifications.md index a80d695..667317f 100644 --- a/docs/hub/notifications.md +++ b/docs/hub/notifications.md @@ -126,3 +126,14 @@ Every method above has an async twin on `client.hub.notifications` when the client is an `AsyncNorbix`. See [push.md](./push.md) for how to pick a push campaign audience and a push provider, and [sms.md](./sms.md) for the same two choices on an SMS campaign. + +## Cloning a template + +`clone_email_template`, `clone_sms_template` and `clone_push_template` copy a +template and answer the new copy's id, so you can open or edit the copy +without listing the templates again: + +```python +copy = client.hub.notifications.clone_email_template(id="tpl_welcome") +client.hub.notifications.get_email_template(id=copy["id"]) +``` diff --git a/src/norbix_python/hub/notifications.py b/src/norbix_python/hub/notifications.py index f643ee0..e8ea7de 100644 --- a/src/norbix_python/hub/notifications.py +++ b/src/norbix_python/hub/notifications.py @@ -420,7 +420,10 @@ def archive_email_template(self, id: str, *, timeout: float | None = None, beare ) def clone_email_template(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/notifications/email/templates/{Id}/clone""" + """POST /{version}/notifications/email/templates/{Id}/clone + + Answers the new copy's id: ``{"id": "..."}``. + """ return self._transport.send( target="hub", path="/{version}/notifications/email/templates/{Id}/clone", @@ -700,7 +703,10 @@ def archive_push_template(self, id: str, *, timeout: float | None = None, bearer ) def clone_push_template(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/notifications/push/templates/{Id}/clone""" + """POST /{version}/notifications/push/templates/{Id}/clone + + Answers the new copy's id: ``{"id": "..."}``. + """ return self._transport.send( target="hub", path="/{version}/notifications/push/templates/{Id}/clone", @@ -1700,7 +1706,10 @@ def archive_sms_template(self, id: str, *, timeout: float | None = None, bearer_ ) def clone_sms_template(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/notifications/sms/templates/{Id}/clone""" + """POST /{version}/notifications/sms/templates/{Id}/clone + + Answers the new copy's id: ``{"id": "..."}``. + """ return self._transport.send( target="hub", path="/{version}/notifications/sms/templates/{Id}/clone", @@ -2167,7 +2176,10 @@ async def archive_email_template(self, id: str, *, timeout: float | None = None, ) async def clone_email_template(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/notifications/email/templates/{Id}/clone""" + """POST /{version}/notifications/email/templates/{Id}/clone + + Answers the new copy's id: ``{"id": "..."}``. + """ return await self._transport.send( target="hub", path="/{version}/notifications/email/templates/{Id}/clone", @@ -2447,7 +2459,10 @@ async def archive_push_template(self, id: str, *, timeout: float | None = None, ) async def clone_push_template(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/notifications/push/templates/{Id}/clone""" + """POST /{version}/notifications/push/templates/{Id}/clone + + Answers the new copy's id: ``{"id": "..."}``. + """ return await self._transport.send( target="hub", path="/{version}/notifications/push/templates/{Id}/clone", @@ -3447,7 +3462,10 @@ async def archive_sms_template(self, id: str, *, timeout: float | None = None, b ) async def clone_sms_template(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/notifications/sms/templates/{Id}/clone""" + """POST /{version}/notifications/sms/templates/{Id}/clone + + Answers the new copy's id: ``{"id": "..."}``. + """ return await self._transport.send( target="hub", path="/{version}/notifications/sms/templates/{Id}/clone", From d51361c27240d7c0587ecabed1c259b0c89a982a Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 23 Oct 2026 18:31:15 +0300 Subject: [PATCH 2/2] test(templates): clone returns the new copy's id to the caller One test per clone method (email, SMS, push) and one async: a fake transport answers {"id": "tpl_copy"} and the caller gets it back. Co-Authored-By: Claude Opus 5.5 --- tests/hub/test_template_clone.py | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/hub/test_template_clone.py diff --git a/tests/hub/test_template_clone.py b/tests/hub/test_template_clone.py new file mode 100644 index 0000000..204e8e9 --- /dev/null +++ b/tests/hub/test_template_clone.py @@ -0,0 +1,74 @@ +"""Template clone answers the new copy's id. + +The gateway's clone requests (email, SMS and push templates) answer an +``IdResponse``: ``{"id": ""}``. The SDK hands that answer to +the caller unchanged. Every call goes to a fake transport; no server is +contacted. +""" + +from __future__ import annotations + +import asyncio +from typing import Any + +import httpx + +from norbix_python import AsyncNorbix, Norbix + +SOURCE_ID = "tpl_source" +COPY_ID = "tpl_copy" + + +def _answering(seen: list[httpx.Request]) -> Norbix: + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={"id": COPY_ID}) + + return Norbix( + project_id="test-project", + bearer_token="test-token", + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + + +def _assert_clone(seen: list[httpx.Request], channel: str, answer: Any) -> None: + assert answer == {"id": COPY_ID} + assert len(seen) == 1 + assert seen[0].method == "POST" + assert seen[0].url.path == f"/v3/notifications/{channel}/templates/{SOURCE_ID}/clone" + + +def test_clone_email_template_returns_the_copy_id() -> None: + seen: list[httpx.Request] = [] + answer = _answering(seen).hub.notifications.clone_email_template(id=SOURCE_ID) + _assert_clone(seen, "email", answer) + + +def test_clone_sms_template_returns_the_copy_id() -> None: + seen: list[httpx.Request] = [] + answer = _answering(seen).hub.notifications.clone_sms_template(id=SOURCE_ID) + _assert_clone(seen, "sms", answer) + + +def test_clone_push_template_returns_the_copy_id() -> None: + seen: list[httpx.Request] = [] + answer = _answering(seen).hub.notifications.clone_push_template(id=SOURCE_ID) + _assert_clone(seen, "push", answer) + + +def test_async_clone_returns_the_copy_id() -> None: + seen: list[httpx.Request] = [] + + async def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={"id": COPY_ID}) + + async def run() -> Any: + client = AsyncNorbix( + project_id="test-project", + bearer_token="test-token", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + return await client.hub.notifications.clone_email_template(id=SOURCE_ID) + + _assert_clone(seen, "email", asyncio.run(run()))