Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions .changeset/snapshot-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': patch
'e2b': patch
---

add optional name parameter to createSnapshot and return snapshot names
1 change: 1 addition & 0 deletions packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type {
SnapshotInfo,
SnapshotListOpts,
SnapshotPaginator,
CreateSnapshotOpts,
} from './sandbox/sandboxApi'

export type { McpServer } from './sandbox/mcp'
Expand Down
16 changes: 8 additions & 8 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import {
SandboxListOpts,
SandboxPaginator,
SandboxBetaCreateOpts,
SandboxApiOpts,
SnapshotListOpts,
SnapshotInfo,
SnapshotPaginator,
CreateSnapshotOpts,
} from './sandboxApi'
import { getSignature } from './signature'
import { compareVersions } from 'compare-versions'
Expand Down Expand Up @@ -609,7 +609,7 @@ export class Sandbox extends SandboxApi {
*
* Use the returned `snapshotId` with `Sandbox.create(snapshotId)` to create a new sandbox from the snapshot.
*
* @param opts connection options.
* @param opts snapshot creation options including optional name and connection options.
*
* @returns snapshot information including the snapshot ID.
*
Expand All @@ -619,17 +619,17 @@ export class Sandbox extends SandboxApi {
* await sandbox.files.write('/app/state.json', '{"step": 1}')
*
* // Create a snapshot
* const snapshot = await sandbox.createSnapshot()
* const snapshot = await sandbox.createSnapshot({ name: 'my-snapshot' })
*
* // Create a new sandbox from the snapshot
* const newSandbox = await Sandbox.create(snapshot.snapshotId)
* ```
*/
async createSnapshot(opts?: SandboxApiOpts): Promise<SnapshotInfo> {
return await SandboxApi.createSnapshot(
this.sandboxId,
this.resolveApiOpts(opts)
)
async createSnapshot(opts?: CreateSnapshotOpts): Promise<SnapshotInfo> {
return await SandboxApi.createSnapshot(this.sandboxId, {
...this.resolveApiOpts(opts),
name: opts?.name,
})
}

/**
Expand Down
25 changes: 22 additions & 3 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,26 @@
export interface SnapshotInfo {
/**
* Snapshot identifier — template ID with tag, or namespaced name with tag (e.g. my-snapshot:latest).
* Can be used with Sandbox.create() to create a new sandbox from this snapshot.
*/
snapshotId: string

/**
* Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2).
*/
names: string[]
}

Check warning on line 291 in packages/js-sdk/src/sandbox/sandboxApi.ts

View check run for this annotation

Claude / Claude Code Review

Breaking change: SnapshotInfo.names is required in a patch release

The PR adds `names: string[]` as a required field to the exported `SnapshotInfo` interface, which is a breaking TypeScript change for any consumer who constructs or type-annotates `SnapshotInfo` objects directly (e.g., mocks, test helpers, or adapters). Since this ships as a patch version bump, it is an unannounced semver violation; making it `names?: string[]` would be backward-compatible because the SDK already always populates the field with `?? []`.
Comment thread
mishushakov marked this conversation as resolved.

/**
* Options for creating a snapshot.
*/
export interface CreateSnapshotOpts extends SandboxApiOpts {
/**
* Optional name for the snapshot template.
* If a snapshot template with this name already exists, a new build will be assigned
* to the existing template instead of creating a new one.
*/
name?: string
}

/**
Expand Down Expand Up @@ -688,13 +705,13 @@
* The snapshot is a persistent image that survives sandbox deletion.
*
* @param sandboxId sandbox ID to create snapshot from.
* @param opts connection options.
* @param opts snapshot creation options including optional name and connection options.
*
* @returns snapshot information including the snapshot name that can be used with Sandbox.create().
*/
static async createSnapshot(
sandboxId: string,
opts?: SandboxApiOpts
opts?: CreateSnapshotOpts
): Promise<SnapshotInfo> {
const config = new ConnectionConfig(opts)
const client = new ApiClient(config)
Expand All @@ -705,7 +722,7 @@
sandboxID: sandboxId,
},
},
body: {},
body: opts?.name ? { name: opts.name } : {},
Comment thread
mishushakov marked this conversation as resolved.
signal: config.getSignal(opts?.requestTimeoutMs),
})

Expand All @@ -720,6 +737,7 @@

return {
snapshotId: res.data!.snapshotID,
names: res.data!.names ?? [],
}
}

Expand Down Expand Up @@ -1045,6 +1063,7 @@
return (res.data ?? []).map(
(snapshot: components['schemas']['SnapshotInfo']) => ({
snapshotId: snapshot.snapshotID,
names: snapshot.names ?? [],
})
)
}
Expand Down
18 changes: 18 additions & 0 deletions packages/js-sdk/tests/sandbox/snapshot-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ sandboxTest.skipIf(isDebug)(
}
)

sandboxTest.skipIf(isDebug)(
'create a named snapshot',
async ({ sandbox, sandboxTestId }) => {
const snapshotName = `snap-${sandboxTestId}`

const snapshot = await sandbox.createSnapshot({ name: snapshotName })

try {
assert.isString(snapshot.snapshotId)
assert.isArray(snapshot.names)
assert.isTrue(snapshot.names.length > 0)
assert.isTrue(snapshot.names.some((n) => n.includes(snapshotName)))
} finally {
await Sandbox.deleteSnapshot(snapshot.snapshotId)
}
}
)

sandboxTest.skipIf(isDebug)('delete snapshot', async ({ sandbox }) => {
const snapshot = await sandbox.createSnapshot()

Expand Down
2 changes: 2 additions & 0 deletions packages/python-sdk/e2b/sandbox/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ class SnapshotInfo:

snapshot_id: str
"""Snapshot identifier — template ID with tag, or namespaced name with tag (e.g. my-snapshot:latest). Can be used with Sandbox.create() to create a new sandbox from this snapshot."""
names: List[str] = field(default_factory=list)
"""Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2)."""


class PaginatorBase:
Expand Down
15 changes: 12 additions & 3 deletions packages/python-sdk/e2b/sandbox_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ async def beta_pause(
@overload
async def create_snapshot(
self,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -705,14 +706,17 @@ async def create_snapshot(

Use the returned `snapshot_id` with `AsyncSandbox.create(snapshot_id)` to create a new sandbox from the snapshot.

:return: Snapshot information including the snapshot ID
:param name: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one.

:return: Snapshot information including the snapshot ID and names
"""
...

@overload
@staticmethod
async def create_snapshot(
sandbox_id: str,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -721,14 +725,16 @@ async def create_snapshot(
The sandbox will be paused while the snapshot is being created.

:param sandbox_id: Sandbox ID
:param name: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one.

:return: Snapshot information including the snapshot ID
:return: Snapshot information including the snapshot ID and names
"""
...

@class_method_variant("_cls_create_snapshot")
async def create_snapshot(
self,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -740,10 +746,13 @@ async def create_snapshot(

Use the returned `snapshot_id` with `AsyncSandbox.create(snapshot_id)` to create a new sandbox from the snapshot.

:return: Snapshot information including the snapshot ID
:param name: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one.

:return: Snapshot information including the snapshot ID and names
"""
return await SandboxApi._cls_create_snapshot(
sandbox_id=self.sandbox_id,
name=name,
**self.connection_config.get_api_params(**opts),
)

Expand Down
6 changes: 5 additions & 1 deletion packages/python-sdk/e2b/sandbox_async/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,9 @@ async def next_items(self) -> List[SnapshotInfo]:
raise SandboxException(f"{res.parsed.message}: Request failed")

return [
SnapshotInfo(snapshot_id=snapshot.snapshot_id) for snapshot in res.parsed
SnapshotInfo(
snapshot_id=snapshot.snapshot_id,
names=list(snapshot.names) if snapshot.names else [],
)
for snapshot in res.parsed
]
8 changes: 6 additions & 2 deletions packages/python-sdk/e2b/sandbox_async/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ async def _cls_get_metrics(
async def _cls_create_snapshot(
cls,
sandbox_id: str,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
config = ConnectionConfig(**opts)
Expand All @@ -305,7 +306,7 @@ async def _cls_create_snapshot(
res = await post_sandboxes_sandbox_id_snapshots.asyncio_detailed(
sandbox_id,
client=api_client,
body=PostSandboxesSandboxIDSnapshotsBody(),
body=PostSandboxesSandboxIDSnapshotsBody(name=name if name else UNSET),
)

if res.status_code == 404:
Expand All @@ -320,7 +321,10 @@ async def _cls_create_snapshot(
if isinstance(res.parsed, Error):
raise SandboxException(f"{res.parsed.message}: Request failed")

return SnapshotInfo(snapshot_id=res.parsed.snapshot_id)
return SnapshotInfo(
snapshot_id=res.parsed.snapshot_id,
names=list(res.parsed.names) if res.parsed.names else [],
)

@classmethod
async def _cls_delete_snapshot(
Expand Down
15 changes: 12 additions & 3 deletions packages/python-sdk/e2b/sandbox_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ def beta_pause(
@overload
def create_snapshot(
self,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -702,14 +703,17 @@ def create_snapshot(

Use the returned `snapshot_id` with `Sandbox.create(snapshot_id)` to create a new sandbox from the snapshot.

:return: Snapshot information including the snapshot ID
:param name: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one.

:return: Snapshot information including the snapshot ID and names
"""
...

@overload
@staticmethod
def create_snapshot(
sandbox_id: str,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -718,14 +722,16 @@ def create_snapshot(
The sandbox will be paused while the snapshot is being created.

:param sandbox_id: Sandbox ID
:param name: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one.

:return: Snapshot information including the snapshot ID
:return: Snapshot information including the snapshot ID and names
"""
...

@class_method_variant("_cls_create_snapshot")
def create_snapshot(
self,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -737,10 +743,13 @@ def create_snapshot(

Use the returned `snapshot_id` with `Sandbox.create(snapshot_id)` to create a new sandbox from the snapshot.

:return: Snapshot information including the snapshot ID
:param name: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one.

:return: Snapshot information including the snapshot ID and names
"""
return SandboxApi._cls_create_snapshot(
sandbox_id=self.sandbox_id,
name=name,
**self.connection_config.get_api_params(**opts),
)

Expand Down
6 changes: 5 additions & 1 deletion packages/python-sdk/e2b/sandbox_sync/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,9 @@ def next_items(self) -> List[SnapshotInfo]:
raise SandboxException(f"{res.parsed.message}: Request failed")

return [
SnapshotInfo(snapshot_id=snapshot.snapshot_id) for snapshot in res.parsed
SnapshotInfo(
snapshot_id=snapshot.snapshot_id,
names=list(snapshot.names) if snapshot.names else [],
)
for snapshot in res.parsed
]
8 changes: 6 additions & 2 deletions packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ def _cls_connect(
def _cls_create_snapshot(
cls,
sandbox_id: str,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
config = ConnectionConfig(**opts)
Expand All @@ -332,7 +333,7 @@ def _cls_create_snapshot(
res = post_sandboxes_sandbox_id_snapshots.sync_detailed(
sandbox_id,
client=api_client,
body=PostSandboxesSandboxIDSnapshotsBody(),
body=PostSandboxesSandboxIDSnapshotsBody(name=name if name else UNSET),
Comment thread
mishushakov marked this conversation as resolved.
)

if res.status_code == 404:
Expand All @@ -347,7 +348,10 @@ def _cls_create_snapshot(
if isinstance(res.parsed, Error):
raise SandboxException(f"{res.parsed.message}: Request failed")

return SnapshotInfo(snapshot_id=res.parsed.snapshot_id)
return SnapshotInfo(
snapshot_id=res.parsed.snapshot_id,
names=list(res.parsed.names) if res.parsed.names else [],
)

@classmethod
def _cls_delete_snapshot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ async def test_list_snapshots_for_sandbox(async_sandbox: AsyncSandbox):
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)


@pytest.mark.skip_debug()
async def test_create_named_snapshot(async_sandbox: AsyncSandbox, sandbox_test_id: str):
snapshot_name = f"snap-{sandbox_test_id}"

snapshot = await async_sandbox.create_snapshot(name=snapshot_name)

try:
assert snapshot.snapshot_id
assert isinstance(snapshot.names, list)
assert len(snapshot.names) > 0
assert any(snapshot_name in n for n in snapshot.names)
finally:
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)


@pytest.mark.skip_debug()
async def test_delete_snapshot(async_sandbox: AsyncSandbox):
snapshot = await async_sandbox.create_snapshot()
Expand Down
Loading
Loading