From f334321afea493bae76c40a1d1bb3085ccdc61fd Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:16:59 +0200 Subject: [PATCH 1/2] fix(python-sdk): URL-encode namespaced template IDs and aliases Namespaced template IDs and aliases contain a slash, which the generated API client passed through as a path separator instead of encoding it, so e.g. Template.exists("namespace/name") hit /templates/aliases/namespace/name instead of /templates/aliases/namespace%2Fname. Percent-encode the template_id and alias path params in both the sync and async template build APIs, matching the JS SDK's encodeURIComponent behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/python-template-alias-encoding.md | 23 ++++++++++++++++ packages/python-sdk/e2b/template/utils.py | 13 ++++++++++ .../e2b/template_async/build_api.py | 12 ++++----- .../python-sdk/e2b/template_sync/build_api.py | 12 ++++----- .../template/utils/test_encode_path_param.py | 26 +++++++++++++++++++ 5 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 .changeset/python-template-alias-encoding.md create mode 100644 packages/python-sdk/tests/shared/template/utils/test_encode_path_param.py diff --git a/.changeset/python-template-alias-encoding.md b/.changeset/python-template-alias-encoding.md new file mode 100644 index 0000000000..dcd85d4e8b --- /dev/null +++ b/.changeset/python-template-alias-encoding.md @@ -0,0 +1,23 @@ +--- +"@e2b/python-sdk": patch +"e2b": patch +--- + +Fix URL encoding of namespaced template IDs and aliases in the Python SDK. + +Namespaced identifiers contain a slash (e.g. `namespace/name`), but the SDK +interpolated them into the request path without encoding, so a call like +`Template.exists("namespace/name")` hit `/templates/aliases/namespace/name` +instead of `/templates/aliases/namespace%2Fname`. Every template method that +takes a template ID or alias in the path — `exists` / `alias_exists`, +`get_tags`, and the build/upload/status calls — now percent-encodes the value, +matching the JavaScript SDK (which already encodes path parameters via +`encodeURIComponent`). + +```python +from e2b import Template + +# Namespaced templates now resolve correctly +Template.exists("my-team/my-template") +Template.get_tags("my-team/my-template") +``` diff --git a/packages/python-sdk/e2b/template/utils.py b/packages/python-sdk/e2b/template/utils.py index 09a7cfc178..ccb7aebd7d 100644 --- a/packages/python-sdk/e2b/template/utils.py +++ b/packages/python-sdk/e2b/template/utils.py @@ -9,11 +9,24 @@ import inspect from types import TracebackType, FrameType from typing import IO, List, Optional, Union +from urllib.parse import quote from e2b.exceptions import TemplateException from e2b.template.consts import BASE_STEP_NAME, FINALIZE_STEP_NAME +def encode_path_param(value: str) -> str: + """ + Percent-encode a value for use as a single URL path segment. + + Namespaced template IDs and aliases contain a slash (``namespace/name``), + which the generated client would otherwise pass through as a path separator. + Encoding mirrors the JS SDK's ``encodeURIComponent``; httpx preserves the + result without double-encoding. + """ + return quote(value, safe="") + + def make_traceback(caller_frame: Optional[FrameType]) -> Optional[TracebackType]: """ Create a TracebackType from a caller frame for error reporting. diff --git a/packages/python-sdk/e2b/template_async/build_api.py b/packages/python-sdk/e2b/template_async/build_api.py index c9209fa5df..31ab1e8033 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -40,7 +40,7 @@ TemplateTagInfo, ) from e2b.template.consts import FILE_UPLOAD_TIMEOUT_SECONDS -from e2b.template.utils import get_build_step_index, tar_file_stream +from e2b.template.utils import encode_path_param, get_build_step_index, tar_file_stream async def request_build( @@ -79,7 +79,7 @@ async def get_file_upload_link( stack_trace: Optional[TracebackType] = None, ) -> TemplateBuildFileUpload: res = await get_templates_template_id_files_hash.asyncio_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), hash_=files_hash, client=client, ) @@ -169,7 +169,7 @@ async def trigger_build( template_data = TemplateBuildStartV2.from_dict(template) res = await post_v_2_templates_template_id_builds_build_id.asyncio_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), build_id=build_id, client=client, body=template_data, @@ -210,7 +210,7 @@ async def get_build_status( client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int ) -> TemplateBuildStatusResponse: res = await get_templates_template_id_builds_build_id_status.asyncio_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), build_id=build_id, client=client, logs_offset=logs_offset, @@ -307,7 +307,7 @@ async def check_alias_exists(client: AuthenticatedClient, alias: str) -> bool: True if the alias exists, False otherwise """ res = await get_templates_aliases_alias.asyncio_detailed( - alias=alias, + alias=encode_path_param(alias), client=client, ) @@ -396,7 +396,7 @@ async def get_template_tags( template_id: Template ID or name """ res = await get_templates_template_id_tags.asyncio_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), client=client, ) diff --git a/packages/python-sdk/e2b/template_sync/build_api.py b/packages/python-sdk/e2b/template_sync/build_api.py index e838d87887..465c2c11f8 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -38,7 +38,7 @@ TemplateTagInfo, ) from e2b.template.consts import FILE_UPLOAD_TIMEOUT_SECONDS -from e2b.template.utils import get_build_step_index, tar_file_stream +from e2b.template.utils import encode_path_param, get_build_step_index, tar_file_stream def request_build( @@ -77,7 +77,7 @@ def get_file_upload_link( stack_trace: Optional[TracebackType] = None, ) -> TemplateBuildFileUpload: res = get_templates_template_id_files_hash.sync_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), hash_=files_hash, client=client, ) @@ -161,7 +161,7 @@ def trigger_build( template_data = TemplateBuildStartV2.from_dict(template) res = post_v_2_templates_template_id_builds_build_id.sync_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), build_id=build_id, client=client, body=template_data, @@ -202,7 +202,7 @@ def get_build_status( client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int ) -> TemplateBuildStatusResponse: res = get_templates_template_id_builds_build_id_status.sync_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), build_id=build_id, client=client, logs_offset=logs_offset, @@ -297,7 +297,7 @@ def check_alias_exists(client: AuthenticatedClient, alias: str) -> bool: True if the alias exists, False otherwise """ res = get_templates_aliases_alias.sync_detailed( - alias=alias, + alias=encode_path_param(alias), client=client, ) @@ -386,7 +386,7 @@ def get_template_tags( template_id: Template ID or name """ res = get_templates_template_id_tags.sync_detailed( - template_id=template_id, + template_id=encode_path_param(template_id), client=client, ) diff --git a/packages/python-sdk/tests/shared/template/utils/test_encode_path_param.py b/packages/python-sdk/tests/shared/template/utils/test_encode_path_param.py new file mode 100644 index 0000000000..4b93bee632 --- /dev/null +++ b/packages/python-sdk/tests/shared/template/utils/test_encode_path_param.py @@ -0,0 +1,26 @@ +from e2b.template.utils import encode_path_param + + +def test_leaves_simple_names_unchanged(): + assert encode_path_param("my-template") == "my-template" + + +def test_encodes_slash_in_namespaced_id(): + # Namespaced template IDs / aliases must have their slash percent-encoded + # so the whole value stays a single path segment. + assert encode_path_param("namespace/name") == "namespace%2Fname" + + +def test_encodes_every_slash_in_deeply_namespaced_id(): + assert encode_path_param("a/b/c") == "a%2Fb%2Fc" + + +def test_keeps_unreserved_characters_unencoded(): + # RFC 3986 unreserved characters must not be encoded. + assert encode_path_param("a-b_c.d~e") == "a-b_c.d~e" + + +def test_encodes_other_reserved_characters(): + # Mirrors JS `encodeURIComponent`, which also encodes ":" and spaces. + assert encode_path_param("name:tag") == "name%3Atag" + assert encode_path_param("a b") == "a%20b" From b3032916eb7e464a214b6326c0e3c3399a63efbf Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:49:41 +0200 Subject: [PATCH 2/2] fix(python-sdk): drop spurious JS SDK bump from changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a Python-only change, so the changeset should not bump the `e2b` (JS SDK) package — it would cut an empty JS SDK release. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/python-template-alias-encoding.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/python-template-alias-encoding.md b/.changeset/python-template-alias-encoding.md index dcd85d4e8b..e2ac4f99d1 100644 --- a/.changeset/python-template-alias-encoding.md +++ b/.changeset/python-template-alias-encoding.md @@ -1,6 +1,5 @@ --- "@e2b/python-sdk": patch -"e2b": patch --- Fix URL encoding of namespaced template IDs and aliases in the Python SDK.