Skip to content
Open
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
22 changes: 22 additions & 0 deletions .changeset/python-template-alias-encoding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@e2b/python-sdk": patch
---
Comment thread
mishushakov marked this conversation as resolved.

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")
```
13 changes: 13 additions & 0 deletions packages/python-sdk/e2b/template/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="")
Comment thread
mishushakov marked this conversation as resolved.


def make_traceback(caller_frame: Optional[FrameType]) -> Optional[TracebackType]:
"""
Create a TracebackType from a caller frame for error reporting.
Expand Down
12 changes: 6 additions & 6 deletions packages/python-sdk/e2b/template_async/build_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand Down
12 changes: 6 additions & 6 deletions packages/python-sdk/e2b/template_sync/build_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Loading