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
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 @@ -610,7 +610,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 @@ -620,17 +620,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 @@ -283,6 +283,23 @@ export interface SnapshotInfo {
* 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[]
}
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 @@ export class SandboxApi {
* 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 @@ export class SandboxApi {
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 @@ export class SandboxApi {

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

Expand Down Expand Up @@ -1045,6 +1063,7 @@ export class SnapshotPaginator extends BasePaginator<SnapshotInfo> {
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 @@ -695,6 +695,7 @@ async def beta_pause(
@overload
async def create_snapshot(
self,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -706,14 +707,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 @@ -722,14 +726,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 @@ -741,10 +747,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 @@ -692,6 +692,7 @@ def beta_pause(
@overload
def create_snapshot(
self,
name: Optional[str] = None,
**opts: Unpack[ApiParams],
) -> SnapshotInfo:
"""
Expand All @@ -703,14 +704,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 @@ -719,14 +723,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 @@ -738,10 +744,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