diff --git a/.changeset/fix-snapshot-name.md b/.changeset/fix-snapshot-name.md new file mode 100644 index 0000000000..854f495d96 --- /dev/null +++ b/.changeset/fix-snapshot-name.md @@ -0,0 +1,6 @@ +--- +"e2b": patch +"@e2b/python-sdk": patch +--- + +Forward optional `name` parameter in `createSnapshot` / `create_snapshot` and return `names` array in `SnapshotInfo` for both JS and Python SDKs. diff --git a/.gitignore b/.gitignore index 7ccb79cdee..2ffe367001 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ vale-styles .vercel .vscode .DS_Store +.worktrees # Logs logs diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index e67092a01c..fbf1ba7d91 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -62,6 +62,7 @@ export type { SandboxInfoLifecycle, SnapshotInfo, SnapshotListOpts, + SnapshotCreateOpts, SnapshotPaginator, } from './sandbox/sandboxApi' diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 7384dc7af2..0d159a3f36 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -20,8 +20,8 @@ import { SandboxListOpts, SandboxPaginator, SandboxBetaCreateOpts, - SandboxApiOpts, SnapshotListOpts, + SnapshotCreateOpts, SnapshotInfo, SnapshotPaginator, } from './sandboxApi' @@ -621,7 +621,7 @@ export class Sandbox extends SandboxApi { * const newSandbox = await Sandbox.create(snapshot.snapshotId) * ``` */ - async createSnapshot(opts?: SandboxApiOpts): Promise { + async createSnapshot(opts?: SnapshotCreateOpts): Promise { return await SandboxApi.createSnapshot( this.sandboxId, this.resolveApiOpts(opts) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index f1bcf86bf2..337a67711a 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -267,6 +267,22 @@ export interface SnapshotListOpts extends SandboxApiOpts { nextToken?: string } +/** + * Options for creating a snapshot. + */ +export interface SnapshotCreateOpts extends SandboxApiOpts { + /** + * Optional human-readable 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. + * + * The name must be unique within your team. Once assigned, the snapshot can + * be used with {@link Sandbox.create} via `team-slug/name`. + */ + name?: string +} + /** * Information about a snapshot. */ @@ -276,6 +292,13 @@ export interface SnapshotInfo { * Can be used with Sandbox.create() to create a new sandbox from this snapshot. */ snapshotId: string + + /** + * Full namespaced names assigned to this snapshot (e.g. `["team-slug/my-snapshot:default"]`). + * Present when the snapshot was created with a `name` option. + * Use any entry with {@link Sandbox.create} to start a sandbox from this snapshot by name. + */ + names: string[] } /** @@ -687,7 +710,7 @@ export class SandboxApi { */ static async createSnapshot( sandboxId: string, - opts?: SandboxApiOpts + opts?: SnapshotCreateOpts ): Promise { const config = new ConnectionConfig(opts) const client = new ApiClient(config) @@ -698,7 +721,9 @@ export class SandboxApi { sandboxID: sandboxId, }, }, - body: {}, + body: { + ...(opts?.name !== undefined && { name: opts.name }), + }, signal: config.getSignal(opts?.requestTimeoutMs), }) @@ -713,6 +738,7 @@ export class SandboxApi { return { snapshotId: res.data!.snapshotID, + names: res.data!.names, } } @@ -1038,6 +1064,7 @@ export class SnapshotPaginator extends BasePaginator { return (res.data ?? []).map( (snapshot: components['schemas']['SnapshotInfo']) => ({ snapshotId: snapshot.snapshotID, + names: snapshot.names, }) ) } diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index 96e4d46271..9bf5784469 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -297,6 +297,9 @@ 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 namespaced names assigned to this snapshot (e.g. ["team-slug/my-snapshot:default"]). Present when the snapshot was created with a name. Use any entry with Sandbox.create() to start a sandbox from this snapshot by name.""" + class PaginatorBase: def __init__( diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 0fe175de3d..1edaae72a5 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -694,6 +694,7 @@ async def beta_pause( @overload async def create_snapshot( self, + name: Optional[str] = None, **opts: Unpack[ApiParams], ) -> SnapshotInfo: """ @@ -705,7 +706,8 @@ 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 human-readable name for the snapshot template. If a snapshot with this name already exists, a new build is assigned to it instead of creating a new one. + :return: Snapshot information including the snapshot ID and names """ ... @@ -713,6 +715,7 @@ async def create_snapshot( @staticmethod async def create_snapshot( sandbox_id: str, + name: Optional[str] = None, **opts: Unpack[ApiParams], ) -> SnapshotInfo: """ @@ -721,14 +724,15 @@ async def create_snapshot( The sandbox will be paused while the snapshot is being created. :param sandbox_id: Sandbox ID - - :return: Snapshot information including the snapshot ID + :param name: Optional human-readable name for the snapshot template. + :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: """ @@ -740,10 +744,12 @@ 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 human-readable name for the snapshot template. If a snapshot with this name already exists, a new build is assigned to it 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), ) diff --git a/packages/python-sdk/e2b/sandbox_async/paginator.py b/packages/python-sdk/e2b/sandbox_async/paginator.py index de6d31fb63..2b84f41e0e 100644 --- a/packages/python-sdk/e2b/sandbox_async/paginator.py +++ b/packages/python-sdk/e2b/sandbox_async/paginator.py @@ -121,5 +121,6 @@ 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=snapshot.names or []) + for snapshot in res.parsed ] diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 8946b038fa..bc64026d35 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -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) @@ -305,7 +306,9 @@ 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 is not None else UNSET, + ), ) if res.status_code == 404: @@ -320,7 +323,9 @@ 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=res.parsed.names or [] + ) @classmethod async def _cls_delete_snapshot( diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index a52cb4c303..093c251593 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -691,6 +691,7 @@ def beta_pause( @overload def create_snapshot( self, + name: Optional[str] = None, **opts: Unpack[ApiParams], ) -> SnapshotInfo: """ @@ -702,7 +703,8 @@ 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 human-readable name for the snapshot template. If a snapshot with this name already exists, a new build is assigned to it instead of creating a new one. + :return: Snapshot information including the snapshot ID and names """ ... @@ -710,6 +712,7 @@ def create_snapshot( @staticmethod def create_snapshot( sandbox_id: str, + name: Optional[str] = None, **opts: Unpack[ApiParams], ) -> SnapshotInfo: """ @@ -718,14 +721,15 @@ def create_snapshot( The sandbox will be paused while the snapshot is being created. :param sandbox_id: Sandbox ID - - :return: Snapshot information including the snapshot ID + :param name: Optional human-readable name for the snapshot template. + :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: """ @@ -737,10 +741,12 @@ 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 human-readable name for the snapshot template. If a snapshot with this name already exists, a new build is assigned to it 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), ) diff --git a/packages/python-sdk/e2b/sandbox_sync/paginator.py b/packages/python-sdk/e2b/sandbox_sync/paginator.py index 0a5649052b..260b016eca 100644 --- a/packages/python-sdk/e2b/sandbox_sync/paginator.py +++ b/packages/python-sdk/e2b/sandbox_sync/paginator.py @@ -121,5 +121,6 @@ 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=snapshot.names or []) + for snapshot in res.parsed ] diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 4cad1247dc..000f1576f6 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -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) @@ -332,7 +333,9 @@ 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 is not None else UNSET, + ), ) if res.status_code == 404: @@ -347,7 +350,9 @@ 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=res.parsed.names or [] + ) @classmethod def _cls_delete_snapshot(