Skip to content

Commit e83cf86

Browse files
authored
feat: add getTags/get_tags to list all tags for a template (#1132)
## Summary - Add `GET /templates/{templateID}/tags` endpoint to the OpenAPI spec - Add `Template.getTags()` to JS/TS SDK - Add `Template.get_tags()` (sync) and `AsyncTemplate.get_tags()` (async) to Python SDK - Returns a list of `TemplateTag` objects with `tag`, `buildId`, and `createdAt` fields ## Test plan - Added unit tests for JS SDK (`Template.getTags` happy path + 404 error) - Added unit tests for Python SDK (sync + async, happy path + error)
1 parent 0ed060f commit e83cf86

File tree

13 files changed

+386
-5
lines changed

13 files changed

+386
-5
lines changed

.changeset/get-template-tags.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'e2b': patch
3+
'@e2b/python-sdk': patch
4+
---
5+
6+
Add `getTags`/`get_tags` method to list all tags for a template

packages/js-sdk/src/template/buildApi.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApiClient, handleApiError, paths } from '../api'
1+
import { ApiClient, handleApiError, paths, components } from '../api'
22
import { stripAnsi } from '../utils'
33
import { BuildError, FileUploadError, TemplateError } from '../errors'
44
import { LogEntry } from './logger'
@@ -7,6 +7,7 @@ import {
77
BuildStatusReason,
88
TemplateBuildStatus,
99
TemplateBuildStatusResponse,
10+
TemplateTag,
1011
TemplateTagInfo,
1112
} from './types'
1213

@@ -358,3 +359,31 @@ export async function removeTags(
358359
throw error
359360
}
360361
}
362+
363+
export async function getTemplateTags(
364+
client: ApiClient,
365+
{ templateID }: { templateID: string }
366+
): Promise<TemplateTag[]> {
367+
const res = await client.api.GET('/templates/{templateID}/tags', {
368+
params: {
369+
path: {
370+
templateID,
371+
},
372+
},
373+
})
374+
375+
const error = handleApiError(res, TemplateError)
376+
if (error) {
377+
throw error
378+
}
379+
380+
if (!res.data) {
381+
throw new TemplateError('Failed to get template tags')
382+
}
383+
384+
return res.data.map((item: components['schemas']['TemplateTag']) => ({
385+
tag: item.tag,
386+
buildId: item.buildID,
387+
createdAt: new Date(item.createdAt),
388+
}))
389+
}

packages/js-sdk/src/template/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { runtime } from '../utils'
66
import {
77
assignTags,
88
checkAliasExists,
9+
getTemplateTags,
910
removeTags,
1011
getBuildStatus,
1112
getFileUploadLink,
@@ -34,6 +35,7 @@ import {
3435
TemplateFinal,
3536
TemplateFromImage,
3637
TemplateOptions,
38+
TemplateTag,
3739
TemplateTagInfo,
3840
} from './types'
3941
import {
@@ -371,6 +373,30 @@ export class TemplateBase
371373
return removeTags(client, { name, tags: normalizedTags })
372374
}
373375

376+
/**
377+
* Get all tags for a template.
378+
*
379+
* @param templateId Template ID or name
380+
* @param options Authentication options
381+
* @returns Array of tag details including tag name, buildId, and creation date
382+
*
383+
* @example
384+
* ```ts
385+
* const tags = await Template.getTags('my-template')
386+
* for (const tag of tags) {
387+
* console.log(`Tag: ${tag.tag}, Build: ${tag.buildId}, Created: ${tag.createdAt}`)
388+
* }
389+
* ```
390+
*/
391+
static async getTags(
392+
templateId: string,
393+
options?: ConnectionOpts
394+
): Promise<TemplateTag[]> {
395+
const config = new ConnectionConfig(options)
396+
const client = new ApiClient(config)
397+
return getTemplateTags(client, { templateID: templateId })
398+
}
399+
374400
fromDebianImage(variant: string = 'stable'): TemplateBuilder {
375401
return this.fromImage(`debian:${variant}`)
376402
}
@@ -1265,6 +1291,7 @@ Template.exists = TemplateBase.exists
12651291
Template.aliasExists = TemplateBase.aliasExists
12661292
Template.assignTags = TemplateBase.assignTags
12671293
Template.removeTags = TemplateBase.removeTags
1294+
Template.getTags = TemplateBase.getTags
12681295
Template.toJSON = TemplateBase.toJSON
12691296
Template.toDockerfile = TemplateBase.toDockerfile
12701297

@@ -1279,5 +1306,6 @@ export type {
12791306
TemplateBuildStatus,
12801307
TemplateBuildStatusResponse,
12811308
TemplateClass,
1309+
TemplateTag,
12821310
TemplateTagInfo,
12831311
} from './types'

packages/js-sdk/src/template/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,24 @@ export type TemplateTagInfo = {
158158
tags: string[]
159159
}
160160

161+
/**
162+
* Detailed information about a single template tag.
163+
*/
164+
export type TemplateTag = {
165+
/**
166+
* Name of the tag.
167+
*/
168+
tag: string
169+
/**
170+
* Build identifier associated with this tag.
171+
*/
172+
buildId: string
173+
/**
174+
* When this tag was assigned.
175+
*/
176+
createdAt: Date
177+
}
178+
161179
/**
162180
* Types of instructions that can be used in a template.
163181
*/

packages/js-sdk/tests/template/tags.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,28 @@ const mockHandlers = [
1818
tags: tags,
1919
})
2020
}),
21+
// Get template tags endpoint
22+
http.get(apiUrl('/templates/:templateID/tags'), ({ params }) => {
23+
const { templateID } = params
24+
if (templateID === 'nonexistent') {
25+
return HttpResponse.json(
26+
{ message: 'Template not found' },
27+
{ status: 404 }
28+
)
29+
}
30+
return HttpResponse.json([
31+
{
32+
tag: 'v1.0',
33+
buildID: '00000000-0000-0000-0000-000000000000',
34+
createdAt: '2024-01-15T10:30:00Z',
35+
},
36+
{
37+
tag: 'latest',
38+
buildID: '11111111-1111-1111-1111-111111111111',
39+
createdAt: '2024-01-16T12:00:00Z',
40+
},
41+
])
42+
}),
2143
// Bulk delete endpoint
2244
http.delete(apiUrl('/templates/tags'), async ({ request }) => {
2345
const { name } = (await request.clone().json()) as {
@@ -81,6 +103,23 @@ describe('Template tags unit tests', () => {
81103
).rejects.toThrow()
82104
})
83105
})
106+
107+
describe('Template.getTags', () => {
108+
test('returns tags for a template', async () => {
109+
const tags = await Template.getTags('my-template-id')
110+
expect(tags).toHaveLength(2)
111+
expect(tags[0].tag).toBe('v1.0')
112+
expect(tags[0].buildId).toBe('00000000-0000-0000-0000-000000000000')
113+
expect(tags[0].createdAt).toBeInstanceOf(Date)
114+
expect(tags[1].tag).toBe('latest')
115+
expect(tags[1].buildId).toBe('11111111-1111-1111-1111-111111111111')
116+
expect(tags[1].createdAt).toBeInstanceOf(Date)
117+
})
118+
119+
test('handles 404 for nonexistent template', async () => {
120+
await expect(Template.getTags('nonexistent')).rejects.toThrow()
121+
})
122+
})
84123
})
85124

86125
// Integration tests

packages/python-sdk/e2b/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
CopyItem,
105105
TemplateBuildStatus,
106106
TemplateBuildStatusResponse,
107+
TemplateTag,
107108
TemplateTagInfo,
108109
)
109110
from .template_async.main import AsyncTemplate
@@ -179,6 +180,7 @@
179180
"BuildStatusReason",
180181
"TemplateBuildStatus",
181182
"TemplateBuildStatusResponse",
183+
"TemplateTag",
182184
"TemplateTagInfo",
183185
"ReadyCmd",
184186
"wait_for_file",

packages/python-sdk/e2b/template/types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass, field
2+
from datetime import datetime
23
from enum import Enum
34
from pathlib import Path
45
from typing import List, Literal, Optional, TypedDict, Union
@@ -73,6 +74,22 @@ class TemplateTagInfo:
7374
"""Assigned tags of the template."""
7475

7576

77+
@dataclass
78+
class TemplateTag:
79+
"""
80+
Detailed information about a single template tag.
81+
"""
82+
83+
tag: str
84+
"""Name of the tag."""
85+
86+
build_id: str
87+
"""Build identifier associated with this tag."""
88+
89+
created_at: datetime
90+
"""When this tag was assigned."""
91+
92+
7693
class InstructionType(str, Enum):
7794
"""
7895
Types of instructions that can be used in a template.

packages/python-sdk/e2b/template_async/build_api.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from e2b.api.client.api.tags import (
1616
post_templates_tags,
1717
delete_templates_tags,
18+
get_templates_template_id_tags,
1819
)
1920
from e2b.api.client.client import AuthenticatedClient
2021
from e2b.api.client.models import (
@@ -33,6 +34,7 @@
3334
BuildStatusReason,
3435
TemplateBuildStatus,
3536
TemplateBuildStatusResponse,
37+
TemplateTag,
3638
TemplateTagInfo,
3739
)
3840
from e2b.template.utils import get_build_step_index, tar_file_stream
@@ -336,3 +338,37 @@ async def remove_tags(client: AuthenticatedClient, name: str, tags: List[str]) -
336338

337339
if res.status_code >= 300:
338340
raise handle_api_exception(res, TemplateException)
341+
342+
343+
async def get_template_tags(
344+
client: AuthenticatedClient, template_id: str
345+
) -> List[TemplateTag]:
346+
"""
347+
Get all tags for a template.
348+
349+
Args:
350+
client: Authenticated API client
351+
template_id: Template ID or name
352+
"""
353+
res = await get_templates_template_id_tags.asyncio_detailed(
354+
template_id=template_id,
355+
client=client,
356+
)
357+
358+
if res.status_code >= 300:
359+
raise handle_api_exception(res, TemplateException)
360+
361+
if isinstance(res.parsed, Error):
362+
raise TemplateException(f"API error: {res.parsed.message}")
363+
364+
if res.parsed is None:
365+
raise TemplateException("Failed to get template tags")
366+
367+
return [
368+
TemplateTag(
369+
tag=item.tag,
370+
build_id=str(item.build_id),
371+
created_at=item.created_at,
372+
)
373+
for item in res.parsed
374+
]

packages/python-sdk/e2b/template_async/main.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
from e2b.template.consts import RESOLVE_SYMLINKS
99
from e2b.template.logger import LogEntry, LogEntryEnd, LogEntryStart
1010
from e2b.template.main import TemplateBase, TemplateClass
11-
from e2b.template.types import BuildInfo, InstructionType, TemplateTagInfo
11+
from e2b.template.types import BuildInfo, InstructionType, TemplateTag, TemplateTagInfo
1212
from e2b.template.utils import normalize_build_arguments, read_dockerignore
1313

1414
from .build_api import (
1515
assign_tags,
1616
check_alias_exists,
17+
get_template_tags,
1718
remove_tags,
1819
get_build_status,
1920
get_file_upload_link,
@@ -496,3 +497,32 @@ async def remove_tags(
496497

497498
normalized_tags = [tags] if isinstance(tags, str) else tags
498499
await remove_tags(api_client, name, normalized_tags)
500+
501+
@staticmethod
502+
async def get_tags(
503+
template_id: str,
504+
**opts: Unpack[ApiParams],
505+
) -> List[TemplateTag]:
506+
"""
507+
Get all tags for a template.
508+
509+
:param template_id: Template ID or name
510+
:return: List of TemplateTag with tag name, build_id, and created_at
511+
512+
Example
513+
```python
514+
from e2b import AsyncTemplate
515+
516+
tags = await AsyncTemplate.get_tags('my-template')
517+
for tag in tags:
518+
print(f"Tag: {tag.tag}, Build: {tag.build_id}, Created: {tag.created_at}")
519+
```
520+
"""
521+
config = ConnectionConfig(**opts)
522+
api_client = get_api_client(
523+
config,
524+
require_api_key=True,
525+
require_access_token=False,
526+
)
527+
528+
return await get_template_tags(api_client, template_id)

packages/python-sdk/e2b/template_sync/build_api.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from e2b.api.client.api.tags import (
1616
post_templates_tags,
1717
delete_templates_tags,
18+
get_templates_template_id_tags,
1819
)
1920
from e2b.api.client.client import AuthenticatedClient
2021
from e2b.api.client.models import (
@@ -33,6 +34,7 @@
3334
BuildStatusReason,
3435
TemplateBuildStatus,
3536
TemplateBuildStatusResponse,
37+
TemplateTag,
3638
TemplateTagInfo,
3739
)
3840
from e2b.template.utils import get_build_step_index, tar_file_stream
@@ -333,3 +335,37 @@ def remove_tags(client: AuthenticatedClient, name: str, tags: List[str]) -> None
333335

334336
if res.status_code >= 300:
335337
raise handle_api_exception(res, TemplateException)
338+
339+
340+
def get_template_tags(
341+
client: AuthenticatedClient, template_id: str
342+
) -> List[TemplateTag]:
343+
"""
344+
Get all tags for a template.
345+
346+
Args:
347+
client: Authenticated API client
348+
template_id: Template ID or name
349+
"""
350+
res = get_templates_template_id_tags.sync_detailed(
351+
template_id=template_id,
352+
client=client,
353+
)
354+
355+
if res.status_code >= 300:
356+
raise handle_api_exception(res, TemplateException)
357+
358+
if isinstance(res.parsed, Error):
359+
raise TemplateException(f"API error: {res.parsed.message}")
360+
361+
if res.parsed is None:
362+
raise TemplateException("Failed to get template tags")
363+
364+
return [
365+
TemplateTag(
366+
tag=item.tag,
367+
build_id=str(item.build_id),
368+
created_at=item.created_at,
369+
)
370+
for item in res.parsed
371+
]

0 commit comments

Comments
 (0)