From 8c80a388d24bc6856d7b7bb5dcd286889a807286 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 29 Jan 2026 11:56:00 +0100 Subject: [PATCH 1/9] feat: snapshots --- packages/js-sdk/src/api/schema.gen.ts | 461 ++++++++---------- packages/js-sdk/src/index.ts | 4 + packages/js-sdk/src/sandbox/commands/index.ts | 5 +- packages/js-sdk/src/sandbox/commands/pty.ts | 6 +- .../js-sdk/src/sandbox/filesystem/index.ts | 5 +- packages/js-sdk/src/sandbox/index.ts | 50 ++ packages/js-sdk/src/sandbox/sandboxApi.ts | 275 ++++++++++- .../js-sdk/tests/sandbox/snapshot-api.test.ts | 189 +++++++ .../post_sandboxes_sandbox_id_snapshots.py | 171 +++++++ .../e2b/api/client/models/__init__.py | 2 + .../e2b/api/client/models/snapshot_info.py | 59 +++ spec/openapi.yml | 73 +++ 12 files changed, 1033 insertions(+), 267 deletions(-) create mode 100644 packages/js-sdk/tests/sandbox/snapshot-api.test.ts create mode 100644 packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py create mode 100644 packages/python-sdk/e2b/api/client/models/snapshot_info.py diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 70c98a1919..a32085d7d5 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -73,6 +73,48 @@ export interface paths { patch?: never; trace?: never; }; + "/sandboxes/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List metrics for given sandboxes */ + get: { + parameters: { + query: { + /** @description Comma-separated list of sandbox IDs to get metrics for */ + sandbox_ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all running sandboxes with metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxesWithMetrics"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/sandboxes/{sandboxID}": { parameters: { query?: never; @@ -203,10 +245,10 @@ export interface paths { get: { parameters: { query?: { - /** @description Maximum number of logs that should be returned */ - limit?: number; /** @description Starting timestamp of the logs that should be returned in milliseconds */ start?: number; + /** @description Maximum number of logs that should be returned */ + limit?: number; }; header?: never; path: { @@ -249,9 +291,9 @@ export interface paths { get: { parameters: { query?: { - end?: number; /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ start?: number; + end?: number; }; header?: never; path: { @@ -418,6 +460,48 @@ export interface paths { patch?: never; trace?: never; }; + "/sandboxes/{sandboxID}/snapshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Snapshot created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SnapshotInfo"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/sandboxes/{sandboxID}/timeout": { parameters: { query?: never; @@ -467,19 +551,22 @@ export interface paths { patch?: never; trace?: never; }; - "/sandboxes/metrics": { + "/snapshots": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description List metrics for given sandboxes */ + /** @description List all snapshots for the team */ get: { parameters: { - query: { - /** @description Comma-separated list of sandbox IDs to get metrics for */ - sandbox_ids: string[]; + query?: { + sandboxID?: string; + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; + /** @description Cursor to start the list from */ + nextToken?: components["parameters"]["paginationNextToken"]; }; header?: never; path?: never; @@ -487,16 +574,15 @@ export interface paths { }; requestBody?: never; responses: { - /** @description Successfully returned all running sandboxes with metrics */ + /** @description Successfully returned snapshots */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SandboxesWithMetrics"]; + "application/json": components["schemas"]["SnapshotInfo"][]; }; }; - 400: components["responses"]["400"]; 401: components["responses"]["401"]; 500: components["responses"]["500"]; }; @@ -509,30 +595,32 @@ export interface paths { patch?: never; trace?: never; }; - "/teams": { + "/templates": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description List all teams */ + /** @description List all templates */ get: { parameters: { - query?: never; + query?: { + teamID?: string; + }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Successfully returned all teams */ + /** @description Successfully returned all templates */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Team"][]; + "application/json": components["schemas"]["Template"][]; }; }; 401: components["responses"]["401"]; @@ -540,96 +628,74 @@ export interface paths { }; }; put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamID}/metrics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get metrics for the team */ - get: { + /** + * @deprecated + * @description Create a new template + */ + post: { parameters: { - query?: { - end?: number; - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number; - }; + query?: never; header?: never; - path: { - teamID: components["parameters"]["teamID"]; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateBuildRequest"]; + }; + }; responses: { - /** @description Successfully returned the team metrics */ - 200: { + /** @description The build was accepted */ + 202: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TeamMetric"][]; + "application/json": components["schemas"]["TemplateLegacy"]; }; }; 400: components["responses"]["400"]; 401: components["responses"]["401"]; - 403: components["responses"]["403"]; 500: components["responses"]["500"]; }; }; - put?: never; - post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/teams/{teamID}/metrics/max": { + "/templates/aliases/{alias}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description Get the maximum metrics for the team in the given interval */ + /** @description Check if template with given alias exists */ get: { parameters: { - query: { - end?: number; - /** @description Metric to retrieve the maximum value for */ - metric: "concurrent_sandboxes" | "sandbox_start_rate"; - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number; - }; + query?: never; header?: never; path: { - teamID: components["parameters"]["teamID"]; + alias: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Successfully returned the team metrics */ + /** @description Successfully queried template by alias */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MaxTeamMetric"]; + "application/json": components["schemas"]["TemplateAliasResponse"]; }; }; 400: components["responses"]["400"]; - 401: components["responses"]["401"]; 403: components["responses"]["403"]; + 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; @@ -641,44 +707,46 @@ export interface paths { patch?: never; trace?: never; }; - "/templates": { + "/templates/tags": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description List all templates */ - get: { + get?: never; + put?: never; + /** @description Assign tag(s) to a template build */ + post: { parameters: { - query?: { - teamID?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AssignTemplateTagsRequest"]; + }; + }; responses: { - /** @description Successfully returned all templates */ - 200: { + /** @description Tag assigned successfully */ + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Template"][]; + "application/json": components["schemas"]["AssignedTemplateTags"]; }; }; + 400: components["responses"]["400"]; 401: components["responses"]["401"]; + 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; - put?: never; - /** - * @deprecated - * @description Create a new template - */ - post: { + /** @description Delete multiple tags from templates */ + delete: { parameters: { query?: never; header?: never; @@ -687,25 +755,23 @@ export interface paths { }; requestBody: { content: { - "application/json": components["schemas"]["TemplateBuildRequest"]; + "application/json": components["schemas"]["DeleteTemplateTagsRequest"]; }; }; responses: { - /** @description The build was accepted */ - 202: { + /** @description Tags deleted successfully */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["TemplateLegacy"]; - }; + content?: never; }; 400: components["responses"]["400"]; 401: components["responses"]["401"]; + 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; - delete?: never; options?: never; head?: never; patch?: never; @@ -722,10 +788,10 @@ export interface paths { get: { parameters: { query?: { - /** @description Maximum number of items to return per page */ - limit?: components["parameters"]["paginationLimit"]; /** @description Cursor to start the list from */ nextToken?: components["parameters"]["paginationNextToken"]; + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; }; header?: never; path: { @@ -857,8 +923,8 @@ export interface paths { query?: never; header?: never; path: { - buildID: components["parameters"]["buildID"]; templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; }; cookie?: never; }; @@ -894,17 +960,17 @@ export interface paths { query?: { /** @description Starting timestamp of the logs that should be returned in milliseconds */ cursor?: number; - direction?: components["schemas"]["LogsDirection"]; - level?: components["schemas"]["LogLevel"]; /** @description Maximum number of logs that should be returned */ limit?: number; + direction?: components["schemas"]["LogsDirection"]; + level?: components["schemas"]["LogLevel"]; /** @description Source of the logs that should be returned from */ source?: components["schemas"]["LogsSource"]; }; header?: never; path: { - buildID: components["parameters"]["buildID"]; templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; }; cookie?: never; }; @@ -943,16 +1009,16 @@ export interface paths { get: { parameters: { query?: { - level?: components["schemas"]["LogLevel"]; - /** @description Maximum number of logs that should be returned */ - limit?: number; /** @description Index of the starting build log that should be returned with the template */ logsOffset?: number; + /** @description Maximum number of logs that should be returned */ + limit?: number; + level?: components["schemas"]["LogLevel"]; }; header?: never; path: { - buildID: components["parameters"]["buildID"]; templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; }; cookie?: never; }; @@ -993,8 +1059,8 @@ export interface paths { query?: never; header?: never; path: { - hash: string; templateID: components["parameters"]["templateID"]; + hash: string; }; cookie?: never; }; @@ -1023,118 +1089,6 @@ export interface paths { patch?: never; trace?: never; }; - "/templates/aliases/{alias}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Check if template with given alias exists */ - get: { - parameters: { - query?: never; - header?: never; - path: { - alias: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully queried template by alias */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateAliasResponse"]; - }; - }; - 400: components["responses"]["400"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/tags": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Assign tag(s) to a template build */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AssignTemplateTagsRequest"]; - }; - }; - responses: { - /** @description Tag assigned successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AssignedTemplateTags"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - /** @description Delete multiple tags from templates */ - delete: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DeleteTemplateTagsRequest"]; - }; - }; - responses: { - /** @description Tags deleted successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/v2/sandboxes": { parameters: { query?: never; @@ -1146,14 +1100,14 @@ export interface paths { get: { parameters: { query?: { - /** @description Maximum number of items to return per page */ - limit?: components["parameters"]["paginationLimit"]; /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ metadata?: string; - /** @description Cursor to start the list from */ - nextToken?: components["parameters"]["paginationNextToken"]; /** @description Filter sandboxes by one or more states */ state?: components["schemas"]["SandboxState"][]; + /** @description Cursor to start the list from */ + nextToken?: components["parameters"]["paginationNextToken"]; + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; }; header?: never; path?: never; @@ -1289,8 +1243,8 @@ export interface paths { query?: never; header?: never; path: { - buildID: components["parameters"]["buildID"]; templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; }; cookie?: never; }; @@ -1364,39 +1318,39 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { + AWSRegistry: { + /** @description AWS Access Key ID for ECR authentication */ + awsAccessKeyId: string; + /** @description AWS Region where the ECR registry is located */ + awsRegion: string; + /** @description AWS Secret Access Key for ECR authentication */ + awsSecretAccessKey: string; + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: "aws"; + }; AdminSandboxKillResult: { /** @description Number of sandboxes that failed to kill */ failedCount: number; /** @description Number of sandboxes successfully killed */ killedCount: number; }; - AssignedTemplateTags: { - /** - * Format: uuid - * @description Identifier of the build associated with these tags - */ - buildID: string; - /** @description Assigned tags of the template */ - tags: string[]; - }; AssignTemplateTagsRequest: { /** @description Tags to assign to the template */ tags: string[]; /** @description Target template in "name:tag" format */ target: string; }; - AWSRegistry: { - /** @description AWS Access Key ID for ECR authentication */ - awsAccessKeyId: string; - /** @description AWS Region where the ECR registry is located */ - awsRegion: string; - /** @description AWS Secret Access Key for ECR authentication */ - awsSecretAccessKey: string; + AssignedTemplateTags: { /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} + * Format: uuid + * @description Identifier of the build associated with these tags */ - type: "aws"; + buildID: string; + /** @description Assigned tags of the template */ + tags: string[]; }; BuildLogEntry: { level: components["schemas"]["LogLevel"]; @@ -1415,12 +1369,17 @@ export interface components { * @description Log entries related to the status reason * @default [] */ - logEntries?: components["schemas"]["BuildLogEntry"][]; + logEntries: components["schemas"]["BuildLogEntry"][]; /** @description Message with the status reason, currently reporting only for error status */ message: string; /** @description Step that failed */ step?: string; }; + /** + * Format: int32 + * @description CPU cores for the sandbox + */ + CPUCount: number; ConnectSandbox: { /** * Format: int32 @@ -1428,11 +1387,6 @@ export interface components { */ timeout: number; }; - /** - * Format: int32 - * @description CPU cores for the sandbox - */ - CPUCount: number; CreatedAccessToken: { /** * Format: date-time @@ -1502,11 +1456,11 @@ export interface components { * @description Disk size for the sandbox in MiB */ DiskSizeMB: number; - /** @description Version of the envd running in the sandbox */ - EnvdVersion: string; EnvVars: { [key: string]: string; }; + /** @description Version of the envd running in the sandbox */ + EnvdVersion: string; Error: { /** * Format: int32 @@ -1637,7 +1591,7 @@ export interface components { * @description Automatically pauses the sandbox after the timeout * @default false */ - autoPause?: boolean; + autoPause: boolean; envVars?: components["schemas"]["EnvVars"]; mcp?: components["schemas"]["Mcp"]; metadata?: components["schemas"]["SandboxMetadata"]; @@ -1651,7 +1605,7 @@ export interface components { * @description Time to live for the sandbox in seconds. * @default 15 */ - timeout?: number; + timeout: number; }; NewTeamAPIKey: { /** @description Name of the API key */ @@ -1790,7 +1744,7 @@ export interface components { * @description Time to live for the sandbox in seconds. * @default 15 */ - timeout?: number; + timeout: number; }; Sandbox: { /** @description Alias of the template */ @@ -1845,11 +1799,6 @@ export interface components { /** @description Identifier of the template from which is the sandbox created */ templateID: string; }; - SandboxesWithMetrics: { - sandboxes: { - [key: string]: components["schemas"]["SandboxMetric"]; - }; - }; /** @description Log entry with timestamp and line */ SandboxLog: { /** @description Log line content */ @@ -1933,7 +1882,7 @@ export interface components { * @description Specify if the sandbox URLs should be accessible only with authentication. * @default true */ - allowPublicTraffic?: boolean; + allowPublicTraffic: boolean; /** @description List of denied CIDR blocks or IP addresses for egress traffic */ denyOut?: string[]; /** @description Specify host mask which will be used for all sandbox requests */ @@ -1944,6 +1893,15 @@ export interface components { * @enum {string} */ SandboxState: "running" | "paused"; + SandboxesWithMetrics: { + sandboxes: { + [key: string]: components["schemas"]["SandboxMetric"]; + }; + }; + SnapshotInfo: { + /** @description Unique identifier for the snapshot, can be used as template name */ + snapshotID: string; + }; Team: { /** @description API key for the team */ apiKey: string; @@ -2166,7 +2124,7 @@ export interface components { * @description Whether the whole build should be forced to run regardless of the cache * @default false */ - force?: boolean; + force: boolean; /** @description Image to use as a base for the template build */ fromImage?: string; fromImageRegistry?: components["schemas"]["FromImageRegistry"]; @@ -2180,7 +2138,7 @@ export interface components { * @description List of steps to execute in the template build * @default [] */ - steps?: components["schemas"]["TemplateStep"][]; + steps: components["schemas"]["TemplateStep"][]; }; /** * @description Status of the template build @@ -2250,14 +2208,14 @@ export interface components { * @description Arguments for the step * @default [] */ - args?: string[]; + args: string[]; /** @description Hash of the files used in the step */ filesHash?: string; /** * @description Whether the step should be forced to run regardless of the cache * @default false */ - force?: boolean; + force: boolean; /** @description Type of the step */ type: string; }; @@ -2375,6 +2333,7 @@ export interface components { /** @description Cursor to start the list from */ paginationNextToken: string; sandboxID: string; + snapshotID: string; tag: string; teamID: string; templateID: string; diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 0be376dbf9..1bce78804c 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -49,6 +49,10 @@ export type { SandboxListOpts, SandboxPaginator, SandboxNetworkOpts, + SnapshotInfo, + SnapshotOpts, + SnapshotListOpts, + SnapshotPaginator, } from './sandbox/sandboxApi' export type { McpServer } from './sandbox/mcp' diff --git a/packages/js-sdk/src/sandbox/commands/index.ts b/packages/js-sdk/src/sandbox/commands/index.ts index 61cb9e283c..de417fbf10 100644 --- a/packages/js-sdk/src/sandbox/commands/index.ts +++ b/packages/js-sdk/src/sandbox/commands/index.ts @@ -28,8 +28,9 @@ export { Pty } from './pty' /** * Options for sending a command request. */ -export interface CommandRequestOpts - extends Partial> {} +export interface CommandRequestOpts extends Partial< + Pick +> {} /** * Options for starting a new command. diff --git a/packages/js-sdk/src/sandbox/commands/pty.ts b/packages/js-sdk/src/sandbox/commands/pty.ts index 714651c4e5..5cb00a9b89 100644 --- a/packages/js-sdk/src/sandbox/commands/pty.ts +++ b/packages/js-sdk/src/sandbox/commands/pty.ts @@ -21,8 +21,10 @@ import { CommandHandle } from './commandHandle' import { authenticationHeader, handleRpcError } from '../../envd/rpc' import { handleProcessStartEvent } from '../../envd/api' -export interface PtyCreateOpts - extends Pick { +export interface PtyCreateOpts extends Pick< + ConnectionOpts, + 'requestTimeoutMs' +> { /** * Number of columns for the PTY. */ diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts index e57bb8d122..096d2d5318 100644 --- a/packages/js-sdk/src/sandbox/filesystem/index.ts +++ b/packages/js-sdk/src/sandbox/filesystem/index.ts @@ -129,8 +129,9 @@ function mapModifiedTime(modifiedTime: Timestamp | undefined) { /** * Options for the sandbox filesystem operations. */ -export interface FilesystemRequestOpts - extends Partial> { +export interface FilesystemRequestOpts extends Partial< + Pick +> { /** * User to use for the operation in the sandbox. * This affects the resolution of relative paths and ownership of the created filesystem objects. diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 1865cc1985..388614d440 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -20,6 +20,10 @@ import { SandboxListOpts, SandboxPaginator, SandboxBetaCreateOpts, + SnapshotOpts, + SnapshotListOpts, + SnapshotInfo, + SnapshotPaginator, } from './sandboxApi' import { getSignature } from './signature' import { compareVersions } from 'compare-versions' @@ -579,6 +583,52 @@ export class Sandbox extends SandboxApi { return await SandboxApi.betaPause(this.sandboxId, opts) } + /** + * Create a snapshot of the sandbox's current state. + * + * The snapshot can be used to create new sandboxes with the same filesystem and state. + * Snapshots are persistent and survive sandbox deletion. + * + * Use the returned `snapshotId` with `Sandbox.create(snapshotId)` to create a new sandbox from the snapshot. + * + * @param opts snapshot options. + * + * @returns snapshot information including the snapshot ID. + * + * @example + * ```ts + * const sandbox = await Sandbox.create() + * await sandbox.files.write('/app/state.json', '{"step": 1}') + * + * // Create a snapshot + * const snapshot = await sandbox.snapshot() + * + * // Create a new sandbox from the snapshot + * const newSandbox = await Sandbox.create(snapshot.snapshotId) + * ``` + */ + async snapshot(opts?: SnapshotOpts): Promise { + return await SandboxApi.createSnapshot(this.sandboxId, { + ...this.connectionConfig, + ...opts, + }) + } + + /** + * List all snapshots created from this sandbox. + * + * @param opts list options. + * + * @returns paginator for listing snapshots from this sandbox. + */ + listSnapshots(opts?: Omit): SnapshotPaginator { + return SandboxApi.listSnapshots({ + ...this.connectionConfig, + ...opts, + sandboxId: this.sandboxId, + }) + } + /** * * Get the MCP URL for the sandbox. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 4878e8b736..dbac0d840d 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -5,7 +5,7 @@ import { DEFAULT_SANDBOX_TIMEOUT_MS, } from '../connectionConfig' import { compareVersions } from 'compare-versions' -import { NotFoundError, TemplateError } from '../errors' +import { NotFoundError, SandboxError, TemplateError } from '../errors' import { timeoutToSeconds } from '../utils' import type { McpServer as BaseMcpServer } from './mcp' @@ -67,13 +67,12 @@ export type SandboxNetworkOpts = { /** * Options for request to the Sandbox API. */ -export interface SandboxApiOpts - extends Partial< - Pick< - ConnectionOpts, - 'apiKey' | 'headers' | 'debug' | 'domain' | 'requestTimeoutMs' - > - > {} +export interface SandboxApiOpts extends Partial< + Pick< + ConnectionOpts, + 'apiKey' | 'headers' | 'debug' | 'domain' | 'requestTimeoutMs' + > +> {} /** * Options for creating a new Sandbox. @@ -200,6 +199,44 @@ export interface SandboxMetricsOpts extends SandboxApiOpts { end?: Date } +/** + * Options for creating a snapshot. + */ +export interface SnapshotOpts extends SandboxApiOpts {} + +/** + * Options for listing snapshots. + */ +export interface SnapshotListOpts extends SandboxApiOpts { + /** + * Filter snapshots by source sandbox ID. + */ + sandboxId?: string + + /** + * Number of snapshots to return per page. + * + * @default 100 + */ + limit?: number + + /** + * Token to the next page. + */ + nextToken?: string +} + +/** + * Information about a snapshot. + */ +export interface SnapshotInfo { + /** + * Unique identifier for the snapshot. + * Can be used as templateId in Sandbox.create() to create a new sandbox from this snapshot. + */ + snapshotId: string +} + /** * Information about a sandbox. */ @@ -529,6 +566,100 @@ export class SandboxApi { return true } + /** + * Create a snapshot from a sandbox. + * + * The snapshot can be used to create new sandboxes with the same state. + * The snapshot is a persistent image that survives sandbox deletion. + * + * @param sandboxId sandbox ID to create snapshot from. + * @param opts snapshot options. + * + * @returns snapshot information including the snapshot ID that can be used with Sandbox.create(). + */ + static async createSnapshot( + sandboxId: string, + opts?: SnapshotOpts + ): Promise { + const config = new ConnectionConfig(opts) + const client = new ApiClient(config) + + const res = await client.api.POST('/sandboxes/{sandboxID}/snapshots', { + params: { + path: { + sandboxID: sandboxId, + }, + }, + signal: config.getSignal(opts?.requestTimeoutMs), + }) + + if (res.error?.code === 404 || res.response.status === 404) { + throw new NotFoundError(`Sandbox ${sandboxId} not found`) + } + + const err = handleApiError(res) + if (err) { + throw err + } + + // Additional check for non-success status codes that might not have error body + if (!res.data || res.response.status >= 400) { + const errorMessage = res.error?.message || `HTTP ${res.response.status}` + throw new SandboxError(`Failed to create snapshot: ${errorMessage}`) + } + + return { + snapshotId: res.data.snapshotID, + } + } + + /** + * List all snapshots. + * + * @param opts list options including filters and pagination. + * + * @returns paginator for listing snapshots. + */ + static listSnapshots(opts?: SnapshotListOpts): SnapshotPaginator { + return new SnapshotPaginator(opts) + } + + /** + * Delete a snapshot. + * + * @param snapshotId snapshot ID. + * @param opts connection options. + * + * @returns `true` if the snapshot was deleted, `false` if it was not found. + */ + static async deleteSnapshot( + snapshotId: string, + opts?: SandboxApiOpts + ): Promise { + const config = new ConnectionConfig(opts) + const client = new ApiClient(config) + + const res = await client.api.DELETE('/templates/{templateID}', { + params: { + path: { + templateID: snapshotId, + }, + }, + signal: config.getSignal(opts?.requestTimeoutMs), + }) + + if (res.error?.code === 404 || res.response.status === 404) { + return false + } + + const err = handleApiError(res) + if (err) { + throw err + } + + return true + } + protected static async createSandbox( template: string, timeoutMs: number, @@ -547,7 +678,10 @@ export class SandboxApi { timeout: timeoutToSeconds(timeoutMs), secure: opts?.secure ?? true, allow_internet_access: opts?.allowInternetAccess ?? true, - network: opts?.network, + network: opts?.network && { + ...opts.network, + allowPublicTraffic: opts.network.allowPublicTraffic ?? true, + }, }, signal: config.getSignal(opts?.requestTimeoutMs), }) @@ -619,8 +753,16 @@ export class SandboxApi { * * @example * ```ts - * const paginator = Sandbox.list() + * // Using async iterator + * for await (const sandbox of Sandbox.list()) { + * console.log(sandbox.sandboxId) + * } * + * // Or collect all to array + * const sandboxes = await Array.fromAsync(Sandbox.list()) + * + * // Manual pagination + * const paginator = Sandbox.list() * while (paginator.hasNext) { * const sandboxes = await paginator.nextItems() * console.log(sandboxes) @@ -722,4 +864,117 @@ export class SandboxPaginator { }) ) } + + async *[Symbol.asyncIterator](): AsyncIterableIterator { + while (this.hasNext) { + const items = await this.nextItems() + for (const item of items) { + yield item + } + } + } +} + +/** + * Paginator for listing snapshots. + * + * @example + * ```ts + * // Using async iterator + * for await (const snapshot of Sandbox.listSnapshots()) { + * console.log(snapshot.snapshotId) + * } + * + * // Or collect all to array + * const snapshots = await Array.fromAsync(Sandbox.listSnapshots()) + * + * // Manual pagination + * const paginator = Sandbox.listSnapshots() + * while (paginator.hasNext) { + * const snapshots = await paginator.nextItems() + * console.log(snapshots) + * } + * ``` + */ +export class SnapshotPaginator { + private _hasNext: boolean + private _nextToken?: string + + private readonly config: ConnectionConfig + private client: ApiClient + + private readonly sandboxId?: string + private readonly limit?: number + + constructor(opts?: SnapshotListOpts) { + this.config = new ConnectionConfig(opts) + this.client = new ApiClient(this.config) + + this._hasNext = true + this._nextToken = opts?.nextToken + + this.sandboxId = opts?.sandboxId + this.limit = opts?.limit + } + + /** + * Returns True if there are more items to fetch. + */ + get hasNext(): boolean { + return this._hasNext + } + + /** + * Returns the next token to use for pagination. + */ + get nextToken(): string | undefined { + return this._nextToken + } + + /** + * Get the next page of snapshots. + * + * @throws Error if there are no more items to fetch. Call this method only if `hasNext` is `true`. + * + * @returns List of snapshots + */ + async nextItems(): Promise { + if (!this.hasNext) { + throw new Error('No more items to fetch') + } + + const res = await this.client.api.GET('/snapshots', { + params: { + query: { + sandboxID: this.sandboxId, + limit: this.limit, + nextToken: this.nextToken, + }, + }, + signal: this.config.getSignal(), + }) + + const err = handleApiError(res) + if (err) { + throw err + } + + this._nextToken = res.response.headers.get('x-next-token') || undefined + this._hasNext = !!this._nextToken + + return (res.data ?? []).map( + (snapshot: components['schemas']['SnapshotInfo']) => ({ + snapshotId: snapshot.snapshotID, + }) + ) + } + + async *[Symbol.asyncIterator](): AsyncIterableIterator { + while (this.hasNext) { + const items = await this.nextItems() + for (const item of items) { + yield item + } + } + } } diff --git a/packages/js-sdk/tests/sandbox/snapshot-api.test.ts b/packages/js-sdk/tests/sandbox/snapshot-api.test.ts new file mode 100644 index 0000000000..2924702d50 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/snapshot-api.test.ts @@ -0,0 +1,189 @@ +import { assert } from 'vitest' + +import { sandboxTest, isDebug } from '../setup.js' +import { Sandbox } from '../../src' + +sandboxTest.skipIf(isDebug)( + 'create a snapshot from sandbox', + async ({ sandbox }) => { + // Write a file to the sandbox + await sandbox.files.write('/home/user/test.txt', 'snapshot test content') + + // Create a snapshot + const snapshot = await sandbox.snapshot() + + assert.isString(snapshot.snapshotId) + assert.isTrue(snapshot.snapshotId.length > 0) + + // Cleanup + await Sandbox.deleteSnapshot(snapshot.snapshotId) + } +) + +sandboxTest.skipIf(isDebug)( + 'create sandbox from snapshot', + async ({ sandbox, sandboxTestId }) => { + const testContent = 'content from original sandbox' + + // Write a file to the sandbox + await sandbox.files.write('/home/user/test.txt', testContent) + + // Create a snapshot + const snapshot = await sandbox.snapshot() + + try { + // Create a new sandbox from the snapshot + const newSandbox = await Sandbox.create(snapshot.snapshotId, { + metadata: { sandboxTestId: `${sandboxTestId}-from-snapshot` }, + }) + + try { + // Verify the file exists in the new sandbox + const content = await newSandbox.files.read('/home/user/test.txt') + assert.equal(content, testContent) + } finally { + await newSandbox.kill() + } + } finally { + await Sandbox.deleteSnapshot(snapshot.snapshotId) + } + } +) + +sandboxTest.skipIf(isDebug)( + 'create multiple sandboxes from same snapshot', + async ({ sandbox, sandboxTestId }) => { + const testContent = 'shared snapshot content' + + await sandbox.files.write('/home/user/shared.txt', testContent) + + const snapshot = await sandbox.snapshot() + + try { + // Create two sandboxes from the same snapshot + const sandbox1 = await Sandbox.create(snapshot.snapshotId, { + metadata: { sandboxTestId: `${sandboxTestId}-branch-1` }, + }) + const sandbox2 = await Sandbox.create(snapshot.snapshotId, { + metadata: { sandboxTestId: `${sandboxTestId}-branch-2` }, + }) + + try { + // Both should have the same initial content + const content1 = await sandbox1.files.read('/home/user/shared.txt') + const content2 = await sandbox2.files.read('/home/user/shared.txt') + + assert.equal(content1, testContent) + assert.equal(content2, testContent) + + // Modify one sandbox - should not affect the other + await sandbox1.files.write( + '/home/user/shared.txt', + 'modified in sandbox1' + ) + + const modifiedContent = await sandbox1.files.read( + '/home/user/shared.txt' + ) + const unchangedContent = await sandbox2.files.read( + '/home/user/shared.txt' + ) + + assert.equal(modifiedContent, 'modified in sandbox1') + assert.equal(unchangedContent, testContent) + } finally { + await sandbox1.kill() + await sandbox2.kill() + } + } finally { + await Sandbox.deleteSnapshot(snapshot.snapshotId) + } + } +) + +sandboxTest.skipIf(isDebug)('list snapshots', async ({ sandbox }) => { + // Create a snapshot + const snapshot = await sandbox.snapshot() + + try { + // List all snapshots + const paginator = Sandbox.listSnapshots() + assert.isTrue(paginator.hasNext) + + const snapshots = await paginator.nextItems() + assert.isArray(snapshots) + + // Find our snapshot in the list + const found = snapshots.find((s) => s.snapshotId === snapshot.snapshotId) + assert.isDefined(found) + } finally { + await Sandbox.deleteSnapshot(snapshot.snapshotId) + } +}) + +sandboxTest.skipIf(isDebug)( + 'list snapshots for specific sandbox', + async ({ sandbox }) => { + // Create a snapshot + const snapshot = await sandbox.snapshot() + + try { + // List snapshots for this sandbox using instance method + const paginator = sandbox.listSnapshots() + const snapshots = await paginator.nextItems() + + // Should find our snapshot + const found = snapshots.find((s) => s.snapshotId === snapshot.snapshotId) + assert.isDefined(found) + } finally { + await Sandbox.deleteSnapshot(snapshot.snapshotId) + } + } +) + +sandboxTest.skipIf(isDebug)('delete snapshot', async ({ sandbox }) => { + const snapshot = await sandbox.snapshot() + + // Delete should succeed + const deleted = await Sandbox.deleteSnapshot(snapshot.snapshotId) + assert.isTrue(deleted) + + // Second delete should return false (not found) + const deletedAgain = await Sandbox.deleteSnapshot(snapshot.snapshotId) + assert.isFalse(deletedAgain) +}) + +sandboxTest.skipIf(isDebug)( + 'snapshot preserves file system state', + async ({ sandbox, sandboxTestId }) => { + // Create directory structure + await sandbox.files.makeDir('/home/user/app') + await sandbox.files.write('/home/user/app/config.json', '{"env": "test"}') + await sandbox.files.write('/home/user/app/data.txt', 'important data') + + const snapshot = await sandbox.snapshot() + + try { + const newSandbox = await Sandbox.create(snapshot.snapshotId, { + metadata: { sandboxTestId: `${sandboxTestId}-fs-test` }, + }) + + try { + // Verify directory exists + const dirExists = await newSandbox.files.exists('/home/user/app') + assert.isTrue(dirExists) + + // Verify files exist with correct content + const config = await newSandbox.files.read('/home/user/app/config.json') + const data = await newSandbox.files.read('/home/user/app/data.txt') + + assert.equal(config, '{"env": "test"}') + assert.equal(data, 'important data') + } finally { + await newSandbox.kill() + } + } finally { + await Sandbox.deleteSnapshot(snapshot.snapshotId) + } + } +) diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py new file mode 100644 index 0000000000..3fd0a4aacf --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py @@ -0,0 +1,171 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...models.snapshot_info import SnapshotInfo +from ...types import Response + + +def _get_kwargs( + sandbox_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "post", + "url": f"/sandboxes/{sandbox_id}/snapshots", + } + + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, SnapshotInfo]]: + if response.status_code == 201: + response_201 = SnapshotInfo.from_dict(response.json()) + + return response_201 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if response.status_code == 500: + response_500 = Error.from_dict(response.json()) + + return response_500 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, SnapshotInfo]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, +) -> Response[Union[Error, SnapshotInfo]]: + """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new + sandboxes and persist beyond the original sandbox's lifetime. + + Args: + sandbox_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, SnapshotInfo]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + sandbox_id: str, + *, + client: AuthenticatedClient, +) -> Optional[Union[Error, SnapshotInfo]]: + """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new + sandboxes and persist beyond the original sandbox's lifetime. + + Args: + sandbox_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, SnapshotInfo] + """ + + return sync_detailed( + sandbox_id=sandbox_id, + client=client, + ).parsed + + +async def asyncio_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, +) -> Response[Union[Error, SnapshotInfo]]: + """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new + sandboxes and persist beyond the original sandbox's lifetime. + + Args: + sandbox_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, SnapshotInfo]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + sandbox_id: str, + *, + client: AuthenticatedClient, +) -> Optional[Union[Error, SnapshotInfo]]: + """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new + sandboxes and persist beyond the original sandbox's lifetime. + + Args: + sandbox_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, SnapshotInfo] + """ + + return ( + await asyncio_detailed( + sandbox_id=sandbox_id, + client=client, + ) + ).parsed diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index 163e03f977..21ee10cdeb 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -48,6 +48,7 @@ from .sandbox_network_config import SandboxNetworkConfig from .sandbox_state import SandboxState from .sandboxes_with_metrics import SandboxesWithMetrics +from .snapshot_info import SnapshotInfo from .team import Team from .team_api_key import TeamAPIKey from .team_metric import TeamMetric @@ -118,6 +119,7 @@ "SandboxMetric", "SandboxNetworkConfig", "SandboxState", + "SnapshotInfo", "Team", "TeamAPIKey", "TeamMetric", diff --git a/packages/python-sdk/e2b/api/client/models/snapshot_info.py b/packages/python-sdk/e2b/api/client/models/snapshot_info.py new file mode 100644 index 0000000000..ff40bc2689 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/snapshot_info.py @@ -0,0 +1,59 @@ +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SnapshotInfo") + + +@_attrs_define +class SnapshotInfo: + """ + Attributes: + snapshot_id (str): Unique identifier for the snapshot, can be used as template name + """ + + snapshot_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + snapshot_id = self.snapshot_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "snapshotID": snapshot_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + snapshot_id = d.pop("snapshotID") + + snapshot_info = cls( + snapshot_id=snapshot_id, + ) + + snapshot_info.additional_properties = d + return snapshot_info + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/spec/openapi.yml b/spec/openapi.yml index dab7c95535..2db488e8d6 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -75,6 +75,13 @@ components: required: true schema: type: string + snapshotID: + name: snapshotID + in: path + required: true + schema: + type: string + description: Identifier of the snapshot (template ID) tag: name: tag in: path @@ -223,6 +230,15 @@ components: - running - paused + SnapshotInfo: + type: object + required: + - snapshotID + properties: + snapshotID: + type: string + description: Unique identifier for the snapshot, can be used as template name + EnvVars: additionalProperties: type: string @@ -2156,6 +2172,63 @@ paths: "404": $ref: "#/components/responses/404" + /sandboxes/{sandboxID}/snapshots: + post: + description: Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. + tags: [sandboxes] + security: + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + responses: + "201": + description: Snapshot created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/SnapshotInfo" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + + /snapshots: + get: + description: List all snapshots for the team + tags: [snapshots] + security: + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - name: sandboxID + in: query + required: false + schema: + type: string + description: Filter snapshots by source sandbox ID + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationNextToken" + responses: + "200": + description: Successfully returned snapshots + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SnapshotInfo" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + /v3/templates: post: description: Create a new template From a538f7aa9fe844f78f1519c084c63989268975e9 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Mon, 16 Feb 2026 14:00:09 -0800 Subject: [PATCH 2/9] Add snapshot API to JS and Python SDKs Adds create, list, delete snapshot methods to both Sandbox and AsyncSandbox classes with paginated listing support. Renames JS SDK's sandbox.snapshot() to sandbox.createSnapshot() for consistency. Includes OpenAPI spec updates for snapshots, volumes, auto-resume, and v2 logs endpoint. --- packages/js-sdk/src/sandbox/index.ts | 4 +- .../js-sdk/tests/sandbox/snapshot-api.test.ts | 14 +- packages/python-sdk/e2b/__init__.py | 9 +- .../e2b/api/client/api/snapshots/__init__.py | 1 + .../api/client/api/snapshots/get_snapshots.py | 202 ++++++++++++ .../python-sdk/e2b/sandbox/sandbox_api.py | 39 +++ packages/python-sdk/e2b/sandbox_async/main.py | 99 +++++- .../python-sdk/e2b/sandbox_async/paginator.py | 58 +++- .../e2b/sandbox_async/sandbox_api.py | 53 ++++ packages/python-sdk/e2b/sandbox_sync/main.py | 99 +++++- .../python-sdk/e2b/sandbox_sync/paginator.py | 58 +++- .../e2b/sandbox_sync/sandbox_api.py | 53 ++++ .../async/sandbox_async/test_snapshot_api.py | 143 +++++++++ .../sync/sandbox_sync/test_snapshot_api.py | 141 +++++++++ spec/openapi.yml | 295 +++++++++++++++++- 15 files changed, 1251 insertions(+), 17 deletions(-) create mode 100644 packages/python-sdk/e2b/api/client/api/snapshots/__init__.py create mode 100644 packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py create mode 100644 packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py create mode 100644 packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 388614d440..0e9f5dda48 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -601,13 +601,13 @@ export class Sandbox extends SandboxApi { * await sandbox.files.write('/app/state.json', '{"step": 1}') * * // Create a snapshot - * const snapshot = await sandbox.snapshot() + * const snapshot = await sandbox.createSnapshot() * * // Create a new sandbox from the snapshot * const newSandbox = await Sandbox.create(snapshot.snapshotId) * ``` */ - async snapshot(opts?: SnapshotOpts): Promise { + async createSnapshot(opts?: SnapshotOpts): Promise { return await SandboxApi.createSnapshot(this.sandboxId, { ...this.connectionConfig, ...opts, diff --git a/packages/js-sdk/tests/sandbox/snapshot-api.test.ts b/packages/js-sdk/tests/sandbox/snapshot-api.test.ts index 2924702d50..ffffe3bc51 100644 --- a/packages/js-sdk/tests/sandbox/snapshot-api.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot-api.test.ts @@ -10,7 +10,7 @@ sandboxTest.skipIf(isDebug)( await sandbox.files.write('/home/user/test.txt', 'snapshot test content') // Create a snapshot - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() assert.isString(snapshot.snapshotId) assert.isTrue(snapshot.snapshotId.length > 0) @@ -29,7 +29,7 @@ sandboxTest.skipIf(isDebug)( await sandbox.files.write('/home/user/test.txt', testContent) // Create a snapshot - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() try { // Create a new sandbox from the snapshot @@ -57,7 +57,7 @@ sandboxTest.skipIf(isDebug)( await sandbox.files.write('/home/user/shared.txt', testContent) - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() try { // Create two sandboxes from the same snapshot @@ -103,7 +103,7 @@ sandboxTest.skipIf(isDebug)( sandboxTest.skipIf(isDebug)('list snapshots', async ({ sandbox }) => { // Create a snapshot - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() try { // List all snapshots @@ -125,7 +125,7 @@ sandboxTest.skipIf(isDebug)( 'list snapshots for specific sandbox', async ({ sandbox }) => { // Create a snapshot - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() try { // List snapshots for this sandbox using instance method @@ -142,7 +142,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('delete snapshot', async ({ sandbox }) => { - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() // Delete should succeed const deleted = await Sandbox.deleteSnapshot(snapshot.snapshotId) @@ -161,7 +161,7 @@ sandboxTest.skipIf(isDebug)( await sandbox.files.write('/home/user/app/config.json', '{"env": "test"}') await sandbox.files.write('/home/user/app/data.txt', 'important data') - const snapshot = await sandbox.snapshot() + const snapshot = await sandbox.createSnapshot() try { const newSandbox = await Sandbox.create(snapshot.snapshotId, { diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index c0a9316120..e290261e59 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -71,16 +71,17 @@ SandboxNetworkOpts, SandboxQuery, SandboxState, + SnapshotInfo, ) from .sandbox_async.commands.command_handle import AsyncCommandHandle from .sandbox_async.filesystem.watch_handle import AsyncWatchHandle from .sandbox_async.main import AsyncSandbox -from .sandbox_async.paginator import AsyncSandboxPaginator +from .sandbox_async.paginator import AsyncSandboxPaginator, AsyncSnapshotPaginator from .sandbox_async.utils import OutputHandler from .sandbox_sync.commands.command_handle import CommandHandle from .sandbox_sync.filesystem.watch_handle import WatchHandle from .sandbox_sync.main import Sandbox -from .sandbox_sync.paginator import SandboxPaginator +from .sandbox_sync.paginator import SandboxPaginator, SnapshotPaginator from .template.logger import ( LogEntry, LogEntryEnd, @@ -153,6 +154,10 @@ # Network "SandboxNetworkOpts", "ALL_TRAFFIC", + # Snapshot + "SnapshotInfo", + "SnapshotPaginator", + "AsyncSnapshotPaginator", # Sync sandbox "Sandbox", "SandboxPaginator", diff --git a/packages/python-sdk/e2b/api/client/api/snapshots/__init__.py b/packages/python-sdk/e2b/api/client/api/snapshots/__init__.py new file mode 100644 index 0000000000..2d7c0b23da --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/snapshots/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py b/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py new file mode 100644 index 0000000000..88b118507d --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py @@ -0,0 +1,202 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...models.snapshot_info import SnapshotInfo +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + sandbox_id: Union[Unset, str] = UNSET, + limit: Union[Unset, int] = UNSET, + next_token: Union[Unset, str] = UNSET, +) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["sandboxID"] = sandbox_id + + params["limit"] = limit + + params["nextToken"] = next_token + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/snapshots", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, list["SnapshotInfo"]]]: + if response.status_code == 200: + response_200 = [] + _response_200 = response.json() + for response_200_item_data in _response_200: + response_200_item = SnapshotInfo.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == 500: + response_500 = Error.from_dict(response.json()) + + return response_500 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, list["SnapshotInfo"]]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + sandbox_id: Union[Unset, str] = UNSET, + limit: Union[Unset, int] = UNSET, + next_token: Union[Unset, str] = UNSET, +) -> Response[Union[Error, list["SnapshotInfo"]]]: + """List all snapshots for the team + + Args: + sandbox_id (Union[Unset, str]): + limit (Union[Unset, int]): + next_token (Union[Unset, str]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, list['SnapshotInfo']]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + limit=limit, + next_token=next_token, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + sandbox_id: Union[Unset, str] = UNSET, + limit: Union[Unset, int] = UNSET, + next_token: Union[Unset, str] = UNSET, +) -> Optional[Union[Error, list["SnapshotInfo"]]]: + """List all snapshots for the team + + Args: + sandbox_id (Union[Unset, str]): + limit (Union[Unset, int]): + next_token (Union[Unset, str]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, list['SnapshotInfo']] + """ + + return sync_detailed( + client=client, + sandbox_id=sandbox_id, + limit=limit, + next_token=next_token, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + sandbox_id: Union[Unset, str] = UNSET, + limit: Union[Unset, int] = UNSET, + next_token: Union[Unset, str] = UNSET, +) -> Response[Union[Error, list["SnapshotInfo"]]]: + """List all snapshots for the team + + Args: + sandbox_id (Union[Unset, str]): + limit (Union[Unset, int]): + next_token (Union[Unset, str]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, list['SnapshotInfo']]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + limit=limit, + next_token=next_token, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + sandbox_id: Union[Unset, str] = UNSET, + limit: Union[Unset, int] = UNSET, + next_token: Union[Unset, str] = UNSET, +) -> Optional[Union[Error, list["SnapshotInfo"]]]: + """List all snapshots for the team + + Args: + sandbox_id (Union[Unset, str]): + limit (Union[Unset, int]): + next_token (Union[Unset, str]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, list['SnapshotInfo']] + """ + + return ( + await asyncio_detailed( + client=client, + sandbox_id=sandbox_id, + limit=limit, + next_token=next_token, + ) + ).parsed diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index 961d853d0a..4210732cb2 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -182,6 +182,45 @@ class SandboxMetrics: """Timestamp of the metric entry.""" +@dataclass +class SnapshotInfo: + """Information about a snapshot.""" + + snapshot_id: str + """Unique identifier for the snapshot. Can be used as template ID in Sandbox.create() to create a new sandbox from this snapshot.""" + + +class SnapshotPaginatorBase: + def __init__( + self, + sandbox_id: Optional[str] = None, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ): + self._config = ConnectionConfig(**opts) + + self.sandbox_id = sandbox_id + self.limit = limit + + self._has_next = True + self._next_token = next_token + + @property + def has_next(self) -> bool: + """ + Returns True if there are more items to fetch. + """ + return self._has_next + + @property + def next_token(self) -> Optional[str]: + """ + Returns the next token to use for pagination. + """ + return self._next_token + + class SandboxPaginatorBase: def __init__( self, diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 42df814e62..d3eb54646b 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -15,13 +15,19 @@ from e2b.envd.versions import ENVD_DEBUG_FALLBACK from e2b.exceptions import SandboxException, format_request_timeout_error from e2b.sandbox.main import SandboxOpts -from e2b.sandbox.sandbox_api import McpServer, SandboxMetrics, SandboxNetworkOpts +from e2b.sandbox.sandbox_api import ( + McpServer, + SandboxMetrics, + SandboxNetworkOpts, + SnapshotInfo, +) from e2b.sandbox.utils import class_method_variant from e2b.sandbox_async.commands.command import Commands from e2b.sandbox_async.commands.pty import Pty from e2b.sandbox_async.filesystem.filesystem import Filesystem from e2b.sandbox_async.git import Git from e2b.sandbox_async.sandbox_api import SandboxApi, SandboxInfo +from e2b.sandbox_async.paginator import AsyncSnapshotPaginator logger = logging.getLogger(__name__) @@ -629,6 +635,97 @@ async def beta_pause( **opts, ) + @overload + async def create_snapshot( + self, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + """ + Create a snapshot of the sandbox's current state. + + The snapshot can be used to create new sandboxes with the same filesystem and state. + Snapshots are persistent and survive sandbox deletion. + + 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 + """ + ... + + @overload + @staticmethod + async def create_snapshot( + sandbox_id: str, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + """ + Create a snapshot from the sandbox specified by sandbox ID. + + :param sandbox_id: Sandbox ID + + :return: Snapshot information including the snapshot ID + """ + ... + + @class_method_variant("_cls_create_snapshot") + async def create_snapshot( + self, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + """ + Create a snapshot of the sandbox's current state. + + The snapshot can be used to create new sandboxes with the same filesystem and state. + Snapshots are persistent and survive sandbox deletion. + + 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 + """ + return await SandboxApi._cls_create_snapshot( + sandbox_id=self.sandbox_id, + **self.connection_config.get_api_params(**opts), + ) + + @staticmethod + def list_snapshots( + sandbox_id: Optional[str] = None, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> AsyncSnapshotPaginator: + """ + List all snapshots. + + :param sandbox_id: Filter snapshots by source sandbox ID + :param limit: Maximum number of snapshots to return per page + :param next_token: Token for pagination + + :return: Paginator for listing snapshots + """ + return AsyncSnapshotPaginator( + sandbox_id=sandbox_id, + limit=limit, + next_token=next_token, + **opts, + ) + + @staticmethod + async def delete_snapshot( + snapshot_id: str, + **opts: Unpack[ApiParams], + ) -> bool: + """ + Delete a snapshot. + + :param snapshot_id: Snapshot ID + :return: `True` if the snapshot was deleted, `False` if it was not found + """ + return await SandboxApi._cls_delete_snapshot( + snapshot_id=snapshot_id, + **opts, + ) + async def get_mcp_token(self) -> Optional[str]: """ Get the MCP token for the sandbox. diff --git a/packages/python-sdk/e2b/sandbox_async/paginator.py b/packages/python-sdk/e2b/sandbox_async/paginator.py index 8bfe15b2ee..b67a177a04 100644 --- a/packages/python-sdk/e2b/sandbox_async/paginator.py +++ b/packages/python-sdk/e2b/sandbox_async/paginator.py @@ -2,9 +2,15 @@ from typing import Optional, List from e2b.api.client.api.sandboxes import get_v2_sandboxes +from e2b.api.client.api.snapshots import get_snapshots from e2b.api.client.types import UNSET from e2b.exceptions import SandboxException -from e2b.sandbox.sandbox_api import SandboxPaginatorBase, SandboxInfo +from e2b.sandbox.sandbox_api import ( + SandboxPaginatorBase, + SandboxInfo, + SnapshotPaginatorBase, + SnapshotInfo, +) from e2b.api import handle_api_exception from e2b.api.client.models.error import Error from e2b.api.client_async import get_api_client @@ -67,3 +73,53 @@ async def next_items(self) -> List[SandboxInfo]: raise SandboxException(f"{res.parsed.message}: Request failed") return [SandboxInfo._from_listed_sandbox(sandbox) for sandbox in res.parsed] + + +class AsyncSnapshotPaginator(SnapshotPaginatorBase): + """ + Paginator for listing snapshots. + + Example: + ```python + paginator = AsyncSandbox.list_snapshots() + + while paginator.has_next: + snapshots = await paginator.next_items() + print(snapshots) + ``` + """ + + async def next_items(self) -> List[SnapshotInfo]: + """ + Returns the next page of snapshots. + + Call this method only if `has_next` is `True`, otherwise it will raise an exception. + + :returns: List of snapshots + """ + if not self.has_next: + raise Exception("No more items to fetch") + + api_client = get_api_client(self._config) + res = await get_snapshots.asyncio_detailed( + client=api_client, + sandbox_id=self.sandbox_id if self.sandbox_id else UNSET, + limit=self.limit if self.limit else UNSET, + next_token=self._next_token if self._next_token else UNSET, + ) + + if res.status_code >= 300: + raise handle_api_exception(res) + + self._next_token = res.headers.get("x-next-token") + self._has_next = bool(self._next_token) + + if res.parsed is None: + return [] + + if isinstance(res.parsed, Error): + raise SandboxException(f"{res.parsed.message}: Request failed") + + return [ + SnapshotInfo(snapshot_id=snapshot.snapshot_id) 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 5f842c86f9..47b03f6d09 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -12,8 +12,10 @@ post_sandboxes, post_sandboxes_sandbox_id_connect, post_sandboxes_sandbox_id_pause, + post_sandboxes_sandbox_id_snapshots, post_sandboxes_sandbox_id_timeout, ) +from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( ConnectSandbox, Error, @@ -33,6 +35,7 @@ SandboxMetrics, SandboxNetworkOpts, SandboxQuery, + SnapshotInfo, ) from e2b.sandbox_async.paginator import AsyncSandboxPaginator @@ -270,6 +273,56 @@ async def _cls_get_metrics( for metric in res.parsed ] + @classmethod + async def _cls_create_snapshot( + cls, + sandbox_id: str, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + config = ConnectionConfig(**opts) + + api_client = get_api_client(config) + res = await post_sandboxes_sandbox_id_snapshots.asyncio_detailed( + sandbox_id, + client=api_client, + ) + + if res.status_code == 404: + raise NotFoundException(f"Sandbox {sandbox_id} not found") + + if res.status_code >= 300: + raise handle_api_exception(res) + + if res.parsed is None: + raise SandboxException("Body of the request is None") + + if isinstance(res.parsed, Error): + raise SandboxException(f"{res.parsed.message}: Request failed") + + return SnapshotInfo(snapshot_id=res.parsed.snapshot_id) + + @classmethod + async def _cls_delete_snapshot( + cls, + snapshot_id: str, + **opts: Unpack[ApiParams], + ) -> bool: + config = ConnectionConfig(**opts) + + api_client = get_api_client(config) + res = await delete_templates_template_id.asyncio_detailed( + snapshot_id, + client=api_client, + ) + + if res.status_code == 404: + return False + + if res.status_code >= 300: + raise handle_api_exception(res) + + return True + @classmethod async def _cls_pause( cls, diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index dcc056232f..f3fd1a10e2 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -15,13 +15,19 @@ from e2b.envd.versions import ENVD_DEBUG_FALLBACK from e2b.exceptions import SandboxException, format_request_timeout_error from e2b.sandbox.main import SandboxOpts -from e2b.sandbox.sandbox_api import McpServer, SandboxMetrics, SandboxNetworkOpts +from e2b.sandbox.sandbox_api import ( + McpServer, + SandboxMetrics, + SandboxNetworkOpts, + SnapshotInfo, +) from e2b.sandbox.utils import class_method_variant from e2b.sandbox_sync.commands.command import Commands from e2b.sandbox_sync.commands.pty import Pty from e2b.sandbox_sync.filesystem.filesystem import Filesystem from e2b.sandbox_sync.git import Git from e2b.sandbox_sync.sandbox_api import SandboxApi, SandboxInfo +from e2b.sandbox_sync.paginator import SnapshotPaginator logger = logging.getLogger(__name__) @@ -626,6 +632,97 @@ def beta_pause( **opts, ) + @overload + def create_snapshot( + self, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + """ + Create a snapshot of the sandbox's current state. + + The snapshot can be used to create new sandboxes with the same filesystem and state. + Snapshots are persistent and survive sandbox deletion. + + 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 + """ + ... + + @overload + @staticmethod + def create_snapshot( + sandbox_id: str, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + """ + Create a snapshot from the sandbox specified by sandbox ID. + + :param sandbox_id: Sandbox ID + + :return: Snapshot information including the snapshot ID + """ + ... + + @class_method_variant("_cls_create_snapshot") + def create_snapshot( + self, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + """ + Create a snapshot of the sandbox's current state. + + The snapshot can be used to create new sandboxes with the same filesystem and state. + Snapshots are persistent and survive sandbox deletion. + + 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 + """ + return SandboxApi._cls_create_snapshot( + sandbox_id=self.sandbox_id, + **self.connection_config.get_api_params(**opts), + ) + + @staticmethod + def list_snapshots( + sandbox_id: Optional[str] = None, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> SnapshotPaginator: + """ + List all snapshots. + + :param sandbox_id: Filter snapshots by source sandbox ID + :param limit: Maximum number of snapshots to return per page + :param next_token: Token for pagination + + :return: Paginator for listing snapshots + """ + return SnapshotPaginator( + sandbox_id=sandbox_id, + limit=limit, + next_token=next_token, + **opts, + ) + + @staticmethod + def delete_snapshot( + snapshot_id: str, + **opts: Unpack[ApiParams], + ) -> bool: + """ + Delete a snapshot. + + :param snapshot_id: Snapshot ID + :return: `True` if the snapshot was deleted, `False` if it was not found + """ + return SandboxApi._cls_delete_snapshot( + snapshot_id=snapshot_id, + **opts, + ) + def get_mcp_token(self) -> Optional[str]: """ Get the MCP token for the sandbox. diff --git a/packages/python-sdk/e2b/sandbox_sync/paginator.py b/packages/python-sdk/e2b/sandbox_sync/paginator.py index 89e3eb5c54..2984f5ce6e 100644 --- a/packages/python-sdk/e2b/sandbox_sync/paginator.py +++ b/packages/python-sdk/e2b/sandbox_sync/paginator.py @@ -3,10 +3,16 @@ from e2b.api import handle_api_exception from e2b.api.client.api.sandboxes import get_v2_sandboxes +from e2b.api.client.api.snapshots import get_snapshots from e2b.api.client.models.error import Error from e2b.api.client.types import UNSET from e2b.exceptions import SandboxException -from e2b.sandbox.sandbox_api import SandboxPaginatorBase, SandboxInfo +from e2b.sandbox.sandbox_api import ( + SandboxPaginatorBase, + SandboxInfo, + SnapshotPaginatorBase, + SnapshotInfo, +) from e2b.api.client_sync import get_api_client @@ -67,3 +73,53 @@ def next_items(self) -> List[SandboxInfo]: raise SandboxException(f"{res.parsed.message}: Request failed") return [SandboxInfo._from_listed_sandbox(sandbox) for sandbox in res.parsed] + + +class SnapshotPaginator(SnapshotPaginatorBase): + """ + Paginator for listing snapshots. + + Example: + ```python + paginator = Sandbox.list_snapshots() + + while paginator.has_next: + snapshots = paginator.next_items() + print(snapshots) + ``` + """ + + def next_items(self) -> List[SnapshotInfo]: + """ + Returns the next page of snapshots. + + Call this method only if `has_next` is `True`, otherwise it will raise an exception. + + :returns: List of snapshots + """ + if not self.has_next: + raise Exception("No more items to fetch") + + api_client = get_api_client(self._config) + res = get_snapshots.sync_detailed( + client=api_client, + sandbox_id=self.sandbox_id if self.sandbox_id else UNSET, + limit=self.limit if self.limit else UNSET, + next_token=self._next_token if self._next_token else UNSET, + ) + + if res.status_code >= 300: + raise handle_api_exception(res) + + self._next_token = res.headers.get("x-next-token") + self._has_next = bool(self._next_token) + + if res.parsed is None: + return [] + + if isinstance(res.parsed, Error): + raise SandboxException(f"{res.parsed.message}: Request failed") + + return [ + SnapshotInfo(snapshot_id=snapshot.snapshot_id) 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 87b4bd6fad..26e4668615 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -12,8 +12,10 @@ post_sandboxes, post_sandboxes_sandbox_id_connect, post_sandboxes_sandbox_id_pause, + post_sandboxes_sandbox_id_snapshots, post_sandboxes_sandbox_id_timeout, ) +from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( ConnectSandbox, Error, @@ -32,6 +34,7 @@ SandboxMetrics, SandboxNetworkOpts, SandboxQuery, + SnapshotInfo, ) from e2b.sandbox_sync.paginator import SandboxPaginator, get_api_client @@ -297,6 +300,56 @@ def _cls_connect( return res.parsed + @classmethod + def _cls_create_snapshot( + cls, + sandbox_id: str, + **opts: Unpack[ApiParams], + ) -> SnapshotInfo: + config = ConnectionConfig(**opts) + + api_client = get_api_client(config) + res = post_sandboxes_sandbox_id_snapshots.sync_detailed( + sandbox_id, + client=api_client, + ) + + if res.status_code == 404: + raise NotFoundException(f"Sandbox {sandbox_id} not found") + + if res.status_code >= 300: + raise handle_api_exception(res) + + if res.parsed is None: + raise SandboxException("Body of the request is None") + + if isinstance(res.parsed, Error): + raise SandboxException(f"{res.parsed.message}: Request failed") + + return SnapshotInfo(snapshot_id=res.parsed.snapshot_id) + + @classmethod + def _cls_delete_snapshot( + cls, + snapshot_id: str, + **opts: Unpack[ApiParams], + ) -> bool: + config = ConnectionConfig(**opts) + + api_client = get_api_client(config) + res = delete_templates_template_id.sync_detailed( + snapshot_id, + client=api_client, + ) + + if res.status_code == 404: + return False + + if res.status_code >= 300: + raise handle_api_exception(res) + + return True + @classmethod def _cls_pause( cls, diff --git a/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py b/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py new file mode 100644 index 0000000000..6a2ee6783a --- /dev/null +++ b/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py @@ -0,0 +1,143 @@ +import pytest +from e2b import AsyncSandbox + + +@pytest.mark.skip_debug() +async def test_create_snapshot(async_sandbox: AsyncSandbox): + snapshot = await async_sandbox.create_snapshot() + + assert snapshot.snapshot_id + assert len(snapshot.snapshot_id) > 0 + + await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +async def test_create_sandbox_from_snapshot(async_sandbox: AsyncSandbox): + test_content = "content from original sandbox" + await async_sandbox.files.write("/home/user/test.txt", test_content) + + snapshot = await async_sandbox.create_snapshot() + + try: + new_sandbox = await AsyncSandbox.create(snapshot.snapshot_id) + + try: + content = await new_sandbox.files.read("/home/user/test.txt") + assert content == test_content + finally: + await new_sandbox.kill() + finally: + await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +async def test_create_multiple_sandboxes_from_snapshot(async_sandbox: AsyncSandbox): + test_content = "shared snapshot content" + await async_sandbox.files.write("/home/user/shared.txt", test_content) + + snapshot = await async_sandbox.create_snapshot() + + try: + sandbox1 = await AsyncSandbox.create(snapshot.snapshot_id) + sandbox2 = await AsyncSandbox.create(snapshot.snapshot_id) + + try: + content1 = await sandbox1.files.read("/home/user/shared.txt") + content2 = await sandbox2.files.read("/home/user/shared.txt") + + assert content1 == test_content + assert content2 == test_content + + await sandbox1.files.write("/home/user/shared.txt", "modified in sandbox1") + + modified_content = await sandbox1.files.read("/home/user/shared.txt") + unchanged_content = await sandbox2.files.read("/home/user/shared.txt") + + assert modified_content == "modified in sandbox1" + assert unchanged_content == test_content + finally: + await sandbox1.kill() + await sandbox2.kill() + finally: + await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +async def test_list_snapshots(async_sandbox: AsyncSandbox): + snapshot = await async_sandbox.create_snapshot() + + try: + paginator = AsyncSandbox.list_snapshots() + assert paginator.has_next + + snapshots = await paginator.next_items() + assert isinstance(snapshots, list) + + found = any(s.snapshot_id == snapshot.snapshot_id for s in snapshots) + assert found + finally: + await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +async def test_list_snapshots_for_sandbox(async_sandbox: AsyncSandbox): + snapshot = await async_sandbox.create_snapshot() + + try: + paginator = AsyncSandbox.list_snapshots( + sandbox_id=async_sandbox.sandbox_id, + ) + snapshots = await paginator.next_items() + + found = any(s.snapshot_id == snapshot.snapshot_id for s in snapshots) + assert found + 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() + + deleted = await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + assert deleted is True + + deleted_again = await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + assert deleted_again is False + + +@pytest.mark.skip_debug() +async def test_snapshot_preserves_filesystem(async_sandbox: AsyncSandbox): + await async_sandbox.files.make_dir("/home/user/app") + await async_sandbox.files.write("/home/user/app/config.json", '{"env": "test"}') + await async_sandbox.files.write("/home/user/app/data.txt", "important data") + + snapshot = await async_sandbox.create_snapshot() + + try: + new_sandbox = await AsyncSandbox.create(snapshot.snapshot_id) + + try: + dir_exists = await new_sandbox.files.exists("/home/user/app") + assert dir_exists + + config = await new_sandbox.files.read("/home/user/app/config.json") + data = await new_sandbox.files.read("/home/user/app/data.txt") + + assert config == '{"env": "test"}' + assert data == "important data" + finally: + await new_sandbox.kill() + finally: + await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +async def test_create_snapshot_class_method(async_sandbox: AsyncSandbox): + snapshot = await AsyncSandbox.create_snapshot(async_sandbox.sandbox_id) + + assert snapshot.snapshot_id + assert len(snapshot.snapshot_id) > 0 + + await AsyncSandbox.delete_snapshot(snapshot.snapshot_id) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py b/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py new file mode 100644 index 0000000000..2f370b61df --- /dev/null +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py @@ -0,0 +1,141 @@ +import pytest +from e2b import Sandbox + + +@pytest.mark.skip_debug() +def test_create_snapshot(sandbox: Sandbox): + snapshot = sandbox.create_snapshot() + + assert snapshot.snapshot_id + assert len(snapshot.snapshot_id) > 0 + + Sandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +def test_create_sandbox_from_snapshot(sandbox: Sandbox): + test_content = "content from original sandbox" + sandbox.files.write("/home/user/test.txt", test_content) + + snapshot = sandbox.create_snapshot() + + try: + new_sandbox = Sandbox.create(snapshot.snapshot_id) + + try: + content = new_sandbox.files.read("/home/user/test.txt") + assert content == test_content + finally: + new_sandbox.kill() + finally: + Sandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +def test_create_multiple_sandboxes_from_snapshot(sandbox: Sandbox): + test_content = "shared snapshot content" + sandbox.files.write("/home/user/shared.txt", test_content) + + snapshot = sandbox.create_snapshot() + + try: + sandbox1 = Sandbox.create(snapshot.snapshot_id) + sandbox2 = Sandbox.create(snapshot.snapshot_id) + + try: + content1 = sandbox1.files.read("/home/user/shared.txt") + content2 = sandbox2.files.read("/home/user/shared.txt") + + assert content1 == test_content + assert content2 == test_content + + sandbox1.files.write("/home/user/shared.txt", "modified in sandbox1") + + modified_content = sandbox1.files.read("/home/user/shared.txt") + unchanged_content = sandbox2.files.read("/home/user/shared.txt") + + assert modified_content == "modified in sandbox1" + assert unchanged_content == test_content + finally: + sandbox1.kill() + sandbox2.kill() + finally: + Sandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +def test_list_snapshots(sandbox: Sandbox): + snapshot = sandbox.create_snapshot() + + try: + paginator = Sandbox.list_snapshots() + assert paginator.has_next + + snapshots = paginator.next_items() + assert isinstance(snapshots, list) + + found = any(s.snapshot_id == snapshot.snapshot_id for s in snapshots) + assert found + finally: + Sandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +def test_list_snapshots_for_sandbox(sandbox: Sandbox): + snapshot = sandbox.create_snapshot() + + try: + paginator = Sandbox.list_snapshots(sandbox_id=sandbox.sandbox_id) + snapshots = paginator.next_items() + + found = any(s.snapshot_id == snapshot.snapshot_id for s in snapshots) + assert found + finally: + Sandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +def test_delete_snapshot(sandbox: Sandbox): + snapshot = sandbox.create_snapshot() + + deleted = Sandbox.delete_snapshot(snapshot.snapshot_id) + assert deleted is True + + deleted_again = Sandbox.delete_snapshot(snapshot.snapshot_id) + assert deleted_again is False + + +@pytest.mark.skip_debug() +def test_snapshot_preserves_filesystem(sandbox: Sandbox): + sandbox.files.make_dir("/home/user/app") + sandbox.files.write("/home/user/app/config.json", '{"env": "test"}') + sandbox.files.write("/home/user/app/data.txt", "important data") + + snapshot = sandbox.create_snapshot() + + try: + new_sandbox = Sandbox.create(snapshot.snapshot_id) + + try: + dir_exists = new_sandbox.files.exists("/home/user/app") + assert dir_exists + + config = new_sandbox.files.read("/home/user/app/config.json") + data = new_sandbox.files.read("/home/user/app/data.txt") + + assert config == '{"env": "test"}' + assert data == "important data" + finally: + new_sandbox.kill() + finally: + Sandbox.delete_snapshot(snapshot.snapshot_id) + + +@pytest.mark.skip_debug() +def test_create_snapshot_class_method(sandbox: Sandbox): + snapshot = Sandbox.create_snapshot(sandbox.sandbox_id) + + assert snapshot.snapshot_id + assert len(snapshot.snapshot_id) > 0 + + Sandbox.delete_snapshot(snapshot.snapshot_id) diff --git a/spec/openapi.yml b/spec/openapi.yml index 2db488e8d6..b5e6fac7aa 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -107,6 +107,12 @@ components: required: false schema: type: string + volumeID: + name: volumeID + in: path + required: true + schema: + type: string responses: "400": @@ -234,10 +240,16 @@ components: type: object required: - snapshotID + - names properties: snapshotID: type: string - description: Unique identifier for the snapshot, can be used as template name + description: Identifier of the snapshot template + names: + type: array + items: + type: string + description: Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2) EnvVars: additionalProperties: @@ -271,6 +283,23 @@ components: type: string description: Specify host mask which will be used for all sandbox requests + SandboxAutoResumePolicy: + type: string + description: Auto-resume policy for paused sandboxes. Default is off. + default: "off" + enum: + - any + - "off" + + SandboxAutoResumeConfig: + type: object + description: Auto-resume configuration for paused sandboxes. Default is off. + required: + - policy + properties: + policy: + $ref: "#/components/schemas/SandboxAutoResumePolicy" + SandboxLog: description: Log entry with timestamp and line required: @@ -322,6 +351,17 @@ components: items: $ref: "#/components/schemas/SandboxLogEntry" + SandboxLogsV2Response: + required: + - logs + properties: + logs: + default: [] + description: Sandbox logs structured + type: array + items: + $ref: "#/components/schemas/SandboxLogEntry" + SandboxMetric: description: Metric entry with timestamp and line required: @@ -368,6 +408,19 @@ components: format: int64 description: Total disk space in bytes + SandboxVolumeMount: + type: object + properties: + name: + type: string + description: Name of the volume + path: + type: string + description: Path of the volume + required: + - name + - path + Sandbox: required: - templateID @@ -414,6 +467,7 @@ components: - endAt - state - envdVersion + - volumeMounts properties: templateID: type: string @@ -455,6 +509,10 @@ components: $ref: "#/components/schemas/SandboxMetadata" state: $ref: "#/components/schemas/SandboxState" + volumeMounts: + type: array + items: + $ref: "#/components/schemas/SandboxVolumeMount" ListedSandbox: required: @@ -468,6 +526,7 @@ components: - endAt - state - envdVersion + - volumeMounts properties: templateID: type: string @@ -502,6 +561,10 @@ components: $ref: "#/components/schemas/SandboxState" envdVersion: $ref: "#/components/schemas/EnvdVersion" + volumeMounts: + type: array + items: + $ref: "#/components/schemas/SandboxVolumeMount" SandboxesWithMetrics: required: @@ -528,6 +591,8 @@ components: type: boolean default: false description: Automatically pauses the sandbox after the timeout + autoResume: + $ref: "#/components/schemas/SandboxAutoResumeConfig" secure: type: boolean description: Secure all system communication with sandbox @@ -544,6 +609,10 @@ components: $ref: "#/components/schemas/EnvVars" mcp: $ref: "#/components/schemas/Mcp" + volumeMounts: + type: array + items: + $ref: "#/components/schemas/SandboxVolumeMount" ResumedSandbox: properties: @@ -1565,6 +1634,24 @@ components: format: uuid description: Identifier of the build associated with these tags + TemplateTag: + required: + - tag + - buildID + - createdAt + properties: + tag: + type: string + description: The tag name + buildID: + type: string + format: uuid + description: Identifier of the build associated with this tag + createdAt: + type: string + format: date-time + description: Time when the tag was assigned + AssignTemplateTagsRequest: required: - target @@ -1626,6 +1713,29 @@ components: type: string description: Suffix used in masked version of the token or key + Volume: + type: object + properties: + volumeID: + type: string + description: ID of the volume + name: + type: string + description: Name of the volume + required: + - volumeID + - name + + NewVolume: + type: object + properties: + name: + type: string + description: Name of the volume + pattern: "^[a-zA-Z0-9_-]+$" + required: + - name + tags: - name: templates - name: sandboxes @@ -1633,6 +1743,7 @@ tags: - name: access-tokens - name: api-keys - name: tags + - name: volumes paths: /health: @@ -1893,7 +2004,8 @@ paths: /sandboxes/{sandboxID}/logs: get: - description: Get sandbox logs + description: Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. + deprecated: true tags: [sandboxes] security: - ApiKeyAuth: [] @@ -1930,6 +2042,51 @@ paths: "500": $ref: "#/components/responses/500" + /v2/sandboxes/{sandboxID}/logs: + get: + description: Get sandbox logs + tags: [sandboxes] + security: + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + - in: query + name: cursor + schema: + type: integer + format: int64 + minimum: 0 + description: Starting timestamp of the logs that should be returned in milliseconds + - in: query + name: limit + schema: + default: 1000 + type: integer + format: int32 + minimum: 0 + maximum: 1000 + description: Maximum number of logs that should be returned + - in: query + name: direction + schema: + $ref: "#/components/schemas/LogsDirection" + description: Direction of the logs that should be returned + responses: + "200": + description: Successfully returned the sandbox logs + content: + application/json: + schema: + $ref: "#/components/schemas/SandboxLogsV2Response" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + /sandboxes/{sandboxID}: get: description: Get a sandbox by id @@ -2182,6 +2339,16 @@ paths: Supabase2TeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: 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. responses: "201": description: Snapshot created successfully @@ -2708,6 +2875,34 @@ paths: "500": $ref: "#/components/responses/500" + /templates/{templateID}/tags: + get: + description: List all tags for a template + tags: [tags] + security: + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/templateID" + responses: + "200": + description: Successfully returned the template tags + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TemplateTag" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + /templates/aliases/{alias}: get: description: Check if template with given alias exists @@ -2966,3 +3161,99 @@ paths: $ref: "#/components/responses/404" "500": $ref: "#/components/responses/500" + + /volumes: + get: + description: List all team volumes + tags: [volumes] + security: + - AccessTokenAuth: [] + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + responses: + "200": + description: Successfully listed all team volumes + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Volume" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + post: + description: Create a new team volume + tags: [volumes] + security: + - AccessTokenAuth: [] + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewVolume" + responses: + "201": + description: Successfully created a new team volume + content: + application/json: + schema: + $ref: "#/components/schemas/Volume" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /volumes/{volumeID}: + get: + description: Get team volume info + tags: [volumes] + security: + - AccessTokenAuth: [] + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/volumeID" + responses: + "200": + description: Successfully retrieved a team volume + content: + application/json: + schema: + $ref: "#/components/schemas/Volume" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + + delete: + description: Delete a team volume + tags: [volumes] + security: + - AccessTokenAuth: [] + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/volumeID" + responses: + "204": + description: Successfully deleted a team volume + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" From 740a9c90f89d690667b23280356db164186f2ab1 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Mon, 16 Feb 2026 14:04:52 -0800 Subject: [PATCH 3/9] add changeset --- .changeset/snapshot-api-support.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/snapshot-api-support.md diff --git a/.changeset/snapshot-api-support.md b/.changeset/snapshot-api-support.md new file mode 100644 index 0000000000..f4077a75dc --- /dev/null +++ b/.changeset/snapshot-api-support.md @@ -0,0 +1,6 @@ +--- +'e2b': minor +'@e2b/python-sdk': minor +--- + +Add snapshot API for creating persistent sandbox snapshots From 7a0cc6830bc9bbd1bab93a06dc287ac61f5b5055 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Mon, 16 Feb 2026 14:12:47 -0800 Subject: [PATCH 4/9] fix: format JS SDK files with project prettier version Co-authored-by: Cursor fix: regenerate API clients with snapshots tag and add missing body param - Add 'snapshots' to codegen tag filter in JS SDK and Python SDK - Add required body parameter to createSnapshot API call - Regenerate all API clients from current OpenAPI spec fix: add missing body param to Python SDK create_snapshot calls fix: use unformatted schema.gen.ts output from openapi-typescript These files are in .prettierignore and should not be reformatted. --- packages/js-sdk/package.json | 2 +- packages/js-sdk/src/api/schema.gen.ts | 672 +++++++++++++----- packages/js-sdk/src/sandbox/commands/index.ts | 5 +- packages/js-sdk/src/sandbox/commands/pty.ts | 6 +- .../js-sdk/src/sandbox/filesystem/index.ts | 5 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 14 +- packages/python-sdk/Makefile | 2 +- .../get_sandboxes_sandbox_id_logs.py | 8 +- .../get_v_2_sandboxes_sandbox_id_logs.py | 219 ++++++ .../post_sandboxes_sandbox_id_snapshots.py | 24 + .../api/client/api/snapshots/get_snapshots.py | 26 +- .../tags/get_templates_template_id_tags.py | 172 +++++ .../e2b/api/client/models/__init__.py | 18 + .../e2b/api/client/models/listed_sandbox.py | 24 +- .../e2b/api/client/models/new_sandbox.py | 40 ++ .../e2b/api/client/models/new_volume.py | 59 ++ ...ost_sandboxes_sandbox_id_snapshots_body.py | 60 ++ .../models/sandbox_auto_resume_config.py | 62 ++ .../models/sandbox_auto_resume_policy.py | 9 + .../e2b/api/client/models/sandbox_detail.py | 24 +- .../client/models/sandbox_logs_v2_response.py | 73 ++ .../api/client/models/sandbox_volume_mount.py | 67 ++ .../e2b/api/client/models/snapshot_info.py | 13 +- .../e2b/api/client/models/template_tag.py | 78 ++ .../e2b/api/client/models/volume.py | 67 ++ .../e2b/sandbox_async/sandbox_api.py | 2 + .../e2b/sandbox_sync/sandbox_api.py | 2 + 27 files changed, 1521 insertions(+), 232 deletions(-) create mode 100644 packages/python-sdk/e2b/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py create mode 100644 packages/python-sdk/e2b/api/client/api/tags/get_templates_template_id_tags.py create mode 100644 packages/python-sdk/e2b/api/client/models/new_volume.py create mode 100644 packages/python-sdk/e2b/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py create mode 100644 packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_config.py create mode 100644 packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_policy.py create mode 100644 packages/python-sdk/e2b/api/client/models/sandbox_logs_v2_response.py create mode 100644 packages/python-sdk/e2b/api/client/models/sandbox_volume_mount.py create mode 100644 packages/python-sdk/e2b/api/client/models/template_tag.py create mode 100644 packages/python-sdk/e2b/api/client/models/volume.py diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 0c8c6f02c3..75107f713b 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -29,7 +29,7 @@ "example": "tsx example.mts", "test": "vitest run", "generate": "npm-run-all generate:* && pnpm run format", - "generate:api": "python ./../../spec/remove_extra_tags.py sandboxes templates tags auth && openapi-typescript ../../spec/openapi_generated.yml -x api_key --array-length --alphabetize --default-non-nullable false --output src/api/schema.gen.ts", + "generate:api": "python ./../../spec/remove_extra_tags.py sandboxes snapshots templates tags auth && openapi-typescript ../../spec/openapi_generated.yml -x api_key --array-length --alphabetize --default-non-nullable false --output src/api/schema.gen.ts", "generate:envd": "cd ../../spec/envd && buf generate --template buf-js.gen.yaml\n", "generate:envd-api": "openapi-typescript ../../spec/envd/envd.yaml -x api_key --array-length --alphabetize --output src/envd/schema.gen.ts", "generate:mcp": "json2ts -i ./../../spec/mcp-server.json -o src/sandbox/mcp.d.ts --unreachableDefinitions --style.singleQuote --no-style.semi", diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index a32085d7d5..230c068058 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -73,48 +73,6 @@ export interface paths { patch?: never; trace?: never; }; - "/sandboxes/metrics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List metrics for given sandboxes */ - get: { - parameters: { - query: { - /** @description Comma-separated list of sandbox IDs to get metrics for */ - sandbox_ids: string[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all running sandboxes with metrics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxesWithMetrics"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/sandboxes/{sandboxID}": { parameters: { query?: never; @@ -241,14 +199,17 @@ export interface paths { path?: never; cookie?: never; }; - /** @description Get sandbox logs */ + /** + * @deprecated + * @description Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. + */ get: { parameters: { query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - start?: number; /** @description Maximum number of logs that should be returned */ limit?: number; + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + start?: number; }; header?: never; path: { @@ -291,9 +252,9 @@ export interface paths { get: { parameters: { query?: { + end?: number; /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ start?: number; - end?: number; }; header?: never; path: { @@ -479,7 +440,14 @@ export interface paths { }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": { + /** @description 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; + }; + }; + }; responses: { /** @description Snapshot created successfully */ 201: { @@ -551,6 +519,48 @@ export interface paths { patch?: never; trace?: never; }; + "/sandboxes/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List metrics for given sandboxes */ + get: { + parameters: { + query: { + /** @description Comma-separated list of sandbox IDs to get metrics for */ + sandbox_ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all running sandboxes with metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxesWithMetrics"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/snapshots": { parameters: { query?: never; @@ -562,11 +572,11 @@ export interface paths { get: { parameters: { query?: { - sandboxID?: string; /** @description Maximum number of items to return per page */ limit?: components["parameters"]["paginationLimit"]; /** @description Cursor to start the list from */ nextToken?: components["parameters"]["paginationNextToken"]; + sandboxID?: string; }; header?: never; path?: never; @@ -595,32 +605,30 @@ export interface paths { patch?: never; trace?: never; }; - "/templates": { + "/teams": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description List all templates */ + /** @description List all teams */ get: { parameters: { - query?: { - teamID?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Successfully returned all templates */ + /** @description Successfully returned all teams */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Template"][]; + "application/json": components["schemas"]["Team"][]; }; }; 401: components["responses"]["401"]; @@ -628,74 +636,96 @@ export interface paths { }; }; put?: never; - /** - * @deprecated - * @description Create a new template - */ - post: { + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamID}/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get metrics for the team */ + get: { parameters: { - query?: never; + query?: { + end?: number; + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number; + }; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateBuildRequest"]; + path: { + teamID: components["parameters"]["teamID"]; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description The build was accepted */ - 202: { + /** @description Successfully returned the team metrics */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TemplateLegacy"]; + "application/json": components["schemas"]["TeamMetric"][]; }; }; 400: components["responses"]["400"]; 401: components["responses"]["401"]; + 403: components["responses"]["403"]; 500: components["responses"]["500"]; }; }; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/templates/aliases/{alias}": { + "/teams/{teamID}/metrics/max": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description Check if template with given alias exists */ + /** @description Get the maximum metrics for the team in the given interval */ get: { parameters: { - query?: never; + query: { + end?: number; + /** @description Metric to retrieve the maximum value for */ + metric: "concurrent_sandboxes" | "sandbox_start_rate"; + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number; + }; header?: never; path: { - alias: string; + teamID: components["parameters"]["teamID"]; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Successfully queried template by alias */ + /** @description Successfully returned the team metrics */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TemplateAliasResponse"]; + "application/json": components["schemas"]["MaxTeamMetric"]; }; }; 400: components["responses"]["400"]; + 401: components["responses"]["401"]; 403: components["responses"]["403"]; - 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; @@ -707,46 +737,44 @@ export interface paths { patch?: never; trace?: never; }; - "/templates/tags": { + "/templates": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - /** @description Assign tag(s) to a template build */ - post: { + /** @description List all templates */ + get: { parameters: { - query?: never; + query?: { + teamID?: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AssignTemplateTagsRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Tag assigned successfully */ - 201: { + /** @description Successfully returned all templates */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AssignedTemplateTags"]; + "application/json": components["schemas"]["Template"][]; }; }; - 400: components["responses"]["400"]; 401: components["responses"]["401"]; - 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; - /** @description Delete multiple tags from templates */ - delete: { + put?: never; + /** + * @deprecated + * @description Create a new template + */ + post: { parameters: { query?: never; header?: never; @@ -755,23 +783,25 @@ export interface paths { }; requestBody: { content: { - "application/json": components["schemas"]["DeleteTemplateTagsRequest"]; + "application/json": components["schemas"]["TemplateBuildRequest"]; }; }; responses: { - /** @description Tags deleted successfully */ - 204: { + /** @description The build was accepted */ + 202: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["TemplateLegacy"]; + }; }; 400: components["responses"]["400"]; 401: components["responses"]["401"]; - 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; + delete?: never; options?: never; head?: never; patch?: never; @@ -788,10 +818,10 @@ export interface paths { get: { parameters: { query?: { - /** @description Cursor to start the list from */ - nextToken?: components["parameters"]["paginationNextToken"]; /** @description Maximum number of items to return per page */ limit?: components["parameters"]["paginationLimit"]; + /** @description Cursor to start the list from */ + nextToken?: components["parameters"]["paginationNextToken"]; }; header?: never; path: { @@ -923,69 +953,202 @@ export interface paths { query?: never; header?: never; path: { + buildID: components["parameters"]["buildID"]; + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The build has started */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}/builds/{buildID}/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get template build logs */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + cursor?: number; + direction?: components["schemas"]["LogsDirection"]; + level?: components["schemas"]["LogLevel"]; + /** @description Maximum number of logs that should be returned */ + limit?: number; + /** @description Source of the logs that should be returned from */ + source?: components["schemas"]["LogsSource"]; + }; + header?: never; + path: { + buildID: components["parameters"]["buildID"]; + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the template build logs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateBuildLogsResponse"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}/builds/{buildID}/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get template build info */ + get: { + parameters: { + query?: { + level?: components["schemas"]["LogLevel"]; + /** @description Maximum number of logs that should be returned */ + limit?: number; + /** @description Index of the starting build log that should be returned with the template */ + logsOffset?: number; + }; + header?: never; + path: { + buildID: components["parameters"]["buildID"]; + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the template */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateBuildInfo"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}/files/{hash}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get an upload link for a tar file containing build layer files */ + get: { + parameters: { + query?: never; + header?: never; + path: { + hash: string; templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; }; cookie?: never; }; requestBody?: never; responses: { - /** @description The build has started */ - 202: { + /** @description The upload link where to upload the tar file */ + 201: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["TemplateBuildFileUpload"]; + }; }; + 400: components["responses"]["400"]; 401: components["responses"]["401"]; + 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; }; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/templates/{templateID}/builds/{buildID}/logs": { + "/templates/{templateID}/tags": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description Get template build logs */ + /** @description List all tags for a template */ get: { parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - cursor?: number; - /** @description Maximum number of logs that should be returned */ - limit?: number; - direction?: components["schemas"]["LogsDirection"]; - level?: components["schemas"]["LogLevel"]; - /** @description Source of the logs that should be returned from */ - source?: components["schemas"]["LogsSource"]; - }; + query?: never; header?: never; path: { templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Successfully returned the template build logs */ + /** @description Successfully returned the template tags */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TemplateBuildLogsResponse"]; + "application/json": components["schemas"]["TemplateTag"][]; }; }; 401: components["responses"]["401"]; + 403: components["responses"]["403"]; 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; @@ -998,42 +1161,36 @@ export interface paths { patch?: never; trace?: never; }; - "/templates/{templateID}/builds/{buildID}/status": { + "/templates/aliases/{alias}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description Get template build info */ + /** @description Check if template with given alias exists */ get: { parameters: { - query?: { - /** @description Index of the starting build log that should be returned with the template */ - logsOffset?: number; - /** @description Maximum number of logs that should be returned */ - limit?: number; - level?: components["schemas"]["LogLevel"]; - }; + query?: never; header?: never; path: { - templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; + alias: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Successfully returned the template */ + /** @description Successfully queried template by alias */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TemplateBuildInfo"]; + "application/json": components["schemas"]["TemplateAliasResponse"]; }; }; - 401: components["responses"]["401"]; + 400: components["responses"]["400"]; + 403: components["responses"]["403"]; 404: components["responses"]["404"]; 500: components["responses"]["500"]; }; @@ -1046,33 +1203,36 @@ export interface paths { patch?: never; trace?: never; }; - "/templates/{templateID}/files/{hash}": { + "/templates/tags": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description Get an upload link for a tar file containing build layer files */ - get: { + get?: never; + put?: never; + /** @description Assign tag(s) to a template build */ + post: { parameters: { query?: never; header?: never; - path: { - templateID: components["parameters"]["templateID"]; - hash: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AssignTemplateTagsRequest"]; + }; + }; responses: { - /** @description The upload link where to upload the tar file */ + /** @description Tag assigned successfully */ 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TemplateBuildFileUpload"]; + "application/json": components["schemas"]["AssignedTemplateTags"]; }; }; 400: components["responses"]["400"]; @@ -1081,9 +1241,33 @@ export interface paths { 500: components["responses"]["500"]; }; }; - put?: never; - post?: never; - delete?: never; + /** @description Delete multiple tags from templates */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteTemplateTagsRequest"]; + }; + }; + responses: { + /** @description Tags deleted successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; options?: never; head?: never; patch?: never; @@ -1100,14 +1284,14 @@ export interface paths { get: { parameters: { query?: { + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ metadata?: string; - /** @description Filter sandboxes by one or more states */ - state?: components["schemas"]["SandboxState"][]; /** @description Cursor to start the list from */ nextToken?: components["parameters"]["paginationNextToken"]; - /** @description Maximum number of items to return per page */ - limit?: components["parameters"]["paginationLimit"]; + /** @description Filter sandboxes by one or more states */ + state?: components["schemas"]["SandboxState"][]; }; header?: never; path?: never; @@ -1137,6 +1321,54 @@ export interface paths { patch?: never; trace?: never; }; + "/v2/sandboxes/{sandboxID}/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get sandbox logs */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + cursor?: number; + /** @description Direction of the logs that should be returned */ + direction?: components["schemas"]["LogsDirection"]; + /** @description Maximum number of logs that should be returned */ + limit?: number; + }; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox logs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxLogsV2Response"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v2/templates": { parameters: { query?: never; @@ -1243,8 +1475,8 @@ export interface paths { query?: never; header?: never; path: { - templateID: components["parameters"]["templateID"]; buildID: components["parameters"]["buildID"]; + templateID: components["parameters"]["templateID"]; }; cookie?: never; }; @@ -1318,31 +1550,12 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { - AWSRegistry: { - /** @description AWS Access Key ID for ECR authentication */ - awsAccessKeyId: string; - /** @description AWS Region where the ECR registry is located */ - awsRegion: string; - /** @description AWS Secret Access Key for ECR authentication */ - awsSecretAccessKey: string; - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: "aws"; - }; AdminSandboxKillResult: { /** @description Number of sandboxes that failed to kill */ failedCount: number; /** @description Number of sandboxes successfully killed */ killedCount: number; }; - AssignTemplateTagsRequest: { - /** @description Tags to assign to the template */ - tags: string[]; - /** @description Target template in "name:tag" format */ - target: string; - }; AssignedTemplateTags: { /** * Format: uuid @@ -1352,6 +1565,25 @@ export interface components { /** @description Assigned tags of the template */ tags: string[]; }; + AssignTemplateTagsRequest: { + /** @description Tags to assign to the template */ + tags: string[]; + /** @description Target template in "name:tag" format */ + target: string; + }; + AWSRegistry: { + /** @description AWS Access Key ID for ECR authentication */ + awsAccessKeyId: string; + /** @description AWS Region where the ECR registry is located */ + awsRegion: string; + /** @description AWS Secret Access Key for ECR authentication */ + awsSecretAccessKey: string; + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: "aws"; + }; BuildLogEntry: { level: components["schemas"]["LogLevel"]; /** @description Log message content */ @@ -1369,17 +1601,12 @@ export interface components { * @description Log entries related to the status reason * @default [] */ - logEntries: components["schemas"]["BuildLogEntry"][]; + logEntries?: components["schemas"]["BuildLogEntry"][]; /** @description Message with the status reason, currently reporting only for error status */ message: string; /** @description Step that failed */ step?: string; }; - /** - * Format: int32 - * @description CPU cores for the sandbox - */ - CPUCount: number; ConnectSandbox: { /** * Format: int32 @@ -1387,6 +1614,11 @@ export interface components { */ timeout: number; }; + /** + * Format: int32 + * @description CPU cores for the sandbox + */ + CPUCount: number; CreatedAccessToken: { /** * Format: date-time @@ -1456,11 +1688,11 @@ export interface components { * @description Disk size for the sandbox in MiB */ DiskSizeMB: number; + /** @description Version of the envd running in the sandbox */ + EnvdVersion: string; EnvVars: { [key: string]: string; }; - /** @description Version of the envd running in the sandbox */ - EnvdVersion: string; Error: { /** * Format: int32 @@ -1529,6 +1761,7 @@ export interface components { state: components["schemas"]["SandboxState"]; /** @description Identifier of the template from which is the sandbox created */ templateID: string; + volumeMounts: components["schemas"]["SandboxVolumeMount"][]; }; /** * @description State of the sandbox @@ -1591,7 +1824,8 @@ export interface components { * @description Automatically pauses the sandbox after the timeout * @default false */ - autoPause: boolean; + autoPause?: boolean; + autoResume?: components["schemas"]["SandboxAutoResumeConfig"]; envVars?: components["schemas"]["EnvVars"]; mcp?: components["schemas"]["Mcp"]; metadata?: components["schemas"]["SandboxMetadata"]; @@ -1605,12 +1839,17 @@ export interface components { * @description Time to live for the sandbox in seconds. * @default 15 */ - timeout: number; + timeout?: number; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; }; NewTeamAPIKey: { /** @description Name of the API key */ name: string; }; + NewVolume: { + /** @description Name of the volume */ + name: string; + }; Node: { /** @description Identifier of the cluster */ clusterID: string; @@ -1744,7 +1983,7 @@ export interface components { * @description Time to live for the sandbox in seconds. * @default 15 */ - timeout: number; + timeout?: number; }; Sandbox: { /** @description Alias of the template */ @@ -1766,6 +2005,16 @@ export interface components { /** @description Token required for accessing sandbox via proxy. */ trafficAccessToken?: string | null; }; + /** @description Auto-resume configuration for paused sandboxes. Default is off. */ + SandboxAutoResumeConfig: { + policy: components["schemas"]["SandboxAutoResumePolicy"]; + }; + /** + * @description Auto-resume policy for paused sandboxes. Default is off. + * @default off + * @enum {string} + */ + SandboxAutoResumePolicy: "any" | "off"; SandboxDetail: { /** @description Alias of the template */ alias?: string; @@ -1798,6 +2047,12 @@ export interface components { state: components["schemas"]["SandboxState"]; /** @description Identifier of the template from which is the sandbox created */ templateID: string; + volumeMounts: components["schemas"]["SandboxVolumeMount"][]; + }; + SandboxesWithMetrics: { + sandboxes: { + [key: string]: components["schemas"]["SandboxMetric"]; + }; }; /** @description Log entry with timestamp and line */ SandboxLog: { @@ -1828,6 +2083,13 @@ export interface components { /** @description Logs of the sandbox */ logs: components["schemas"]["SandboxLog"][]; }; + SandboxLogsV2Response: { + /** + * @description Sandbox logs structured + * @default [] + */ + logs: components["schemas"]["SandboxLogEntry"][]; + }; SandboxMetadata: { [key: string]: string; }; @@ -1882,7 +2144,7 @@ export interface components { * @description Specify if the sandbox URLs should be accessible only with authentication. * @default true */ - allowPublicTraffic: boolean; + allowPublicTraffic?: boolean; /** @description List of denied CIDR blocks or IP addresses for egress traffic */ denyOut?: string[]; /** @description Specify host mask which will be used for all sandbox requests */ @@ -1893,13 +2155,16 @@ export interface components { * @enum {string} */ SandboxState: "running" | "paused"; - SandboxesWithMetrics: { - sandboxes: { - [key: string]: components["schemas"]["SandboxMetric"]; - }; + SandboxVolumeMount: { + /** @description Name of the volume */ + name: string; + /** @description Path of the volume */ + path: string; }; SnapshotInfo: { - /** @description Unique identifier for the snapshot, can be used as template name */ + /** @description Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2) */ + names: string[]; + /** @description Identifier of the snapshot template */ snapshotID: string; }; Team: { @@ -2124,7 +2389,7 @@ export interface components { * @description Whether the whole build should be forced to run regardless of the cache * @default false */ - force: boolean; + force?: boolean; /** @description Image to use as a base for the template build */ fromImage?: string; fromImageRegistry?: components["schemas"]["FromImageRegistry"]; @@ -2138,7 +2403,7 @@ export interface components { * @description List of steps to execute in the template build * @default [] */ - steps: components["schemas"]["TemplateStep"][]; + steps?: components["schemas"]["TemplateStep"][]; }; /** * @description Status of the template build @@ -2208,17 +2473,31 @@ export interface components { * @description Arguments for the step * @default [] */ - args: string[]; + args?: string[]; /** @description Hash of the files used in the step */ filesHash?: string; /** * @description Whether the step should be forced to run regardless of the cache * @default false */ - force: boolean; + force?: boolean; /** @description Type of the step */ type: string; }; + TemplateTag: { + /** + * Format: uuid + * @description Identifier of the build associated with this tag + */ + buildID: string; + /** + * Format: date-time + * @description Time when the tag was assigned + */ + createdAt: string; + /** @description The tag name */ + tag: string; + }; TemplateUpdateRequest: { /** @description Whether the template is public or only accessible by the team */ public?: boolean; @@ -2266,6 +2545,12 @@ export interface components { /** @description New name for the API key */ name: string; }; + Volume: { + /** @description Name of the volume */ + name: string; + /** @description ID of the volume */ + volumeID: string; + }; }; responses: { /** @description Bad request */ @@ -2337,6 +2622,7 @@ export interface components { tag: string; teamID: string; templateID: string; + volumeID: string; }; requestBodies: never; headers: never; diff --git a/packages/js-sdk/src/sandbox/commands/index.ts b/packages/js-sdk/src/sandbox/commands/index.ts index de417fbf10..61cb9e283c 100644 --- a/packages/js-sdk/src/sandbox/commands/index.ts +++ b/packages/js-sdk/src/sandbox/commands/index.ts @@ -28,9 +28,8 @@ export { Pty } from './pty' /** * Options for sending a command request. */ -export interface CommandRequestOpts extends Partial< - Pick -> {} +export interface CommandRequestOpts + extends Partial> {} /** * Options for starting a new command. diff --git a/packages/js-sdk/src/sandbox/commands/pty.ts b/packages/js-sdk/src/sandbox/commands/pty.ts index 5cb00a9b89..714651c4e5 100644 --- a/packages/js-sdk/src/sandbox/commands/pty.ts +++ b/packages/js-sdk/src/sandbox/commands/pty.ts @@ -21,10 +21,8 @@ import { CommandHandle } from './commandHandle' import { authenticationHeader, handleRpcError } from '../../envd/rpc' import { handleProcessStartEvent } from '../../envd/api' -export interface PtyCreateOpts extends Pick< - ConnectionOpts, - 'requestTimeoutMs' -> { +export interface PtyCreateOpts + extends Pick { /** * Number of columns for the PTY. */ diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts index 096d2d5318..e57bb8d122 100644 --- a/packages/js-sdk/src/sandbox/filesystem/index.ts +++ b/packages/js-sdk/src/sandbox/filesystem/index.ts @@ -129,9 +129,8 @@ function mapModifiedTime(modifiedTime: Timestamp | undefined) { /** * Options for the sandbox filesystem operations. */ -export interface FilesystemRequestOpts extends Partial< - Pick -> { +export interface FilesystemRequestOpts + extends Partial> { /** * User to use for the operation in the sandbox. * This affects the resolution of relative paths and ownership of the created filesystem objects. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index dbac0d840d..66abf94ad5 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -67,12 +67,13 @@ export type SandboxNetworkOpts = { /** * Options for request to the Sandbox API. */ -export interface SandboxApiOpts extends Partial< - Pick< - ConnectionOpts, - 'apiKey' | 'headers' | 'debug' | 'domain' | 'requestTimeoutMs' - > -> {} +export interface SandboxApiOpts + extends Partial< + Pick< + ConnectionOpts, + 'apiKey' | 'headers' | 'debug' | 'domain' | 'requestTimeoutMs' + > + > {} /** * Options for creating a new Sandbox. @@ -590,6 +591,7 @@ export class SandboxApi { sandboxID: sandboxId, }, }, + body: {}, signal: config.getSignal(opts?.requestTimeoutMs), }) diff --git a/packages/python-sdk/Makefile b/packages/python-sdk/Makefile index 38f4eb1e89..0ca7bfc5d8 100644 --- a/packages/python-sdk/Makefile +++ b/packages/python-sdk/Makefile @@ -1,7 +1,7 @@ ROOT_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..) generate-api: - python $(ROOT_DIR)/spec/remove_extra_tags.py sandboxes templates tags + python $(ROOT_DIR)/spec/remove_extra_tags.py sandboxes snapshots templates tags openapi-python-client generate --output-path $(ROOT_DIR)/packages/python-sdk/e2b/api/api --overwrite --path $(ROOT_DIR)/spec/openapi_generated.yml rm -rf e2b/api/client mv e2b/api/api/e2b_api_client e2b/api/client diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py index a72765c53b..39d7460615 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py @@ -76,7 +76,7 @@ def sync_detailed( start: Union[Unset, int] = UNSET, limit: Union[Unset, int] = 1000, ) -> Response[Union[Error, SandboxLogs]]: - """Get sandbox logs + """Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. Args: sandbox_id (str): @@ -111,7 +111,7 @@ def sync( start: Union[Unset, int] = UNSET, limit: Union[Unset, int] = 1000, ) -> Optional[Union[Error, SandboxLogs]]: - """Get sandbox logs + """Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. Args: sandbox_id (str): @@ -141,7 +141,7 @@ async def asyncio_detailed( start: Union[Unset, int] = UNSET, limit: Union[Unset, int] = 1000, ) -> Response[Union[Error, SandboxLogs]]: - """Get sandbox logs + """Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. Args: sandbox_id (str): @@ -174,7 +174,7 @@ async def asyncio( start: Union[Unset, int] = UNSET, limit: Union[Unset, int] = 1000, ) -> Optional[Union[Error, SandboxLogs]]: - """Get sandbox logs + """Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. Args: sandbox_id (str): diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py b/packages/python-sdk/e2b/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py new file mode 100644 index 0000000000..37d2a3b516 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py @@ -0,0 +1,219 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...models.logs_direction import LogsDirection +from ...models.sandbox_logs_v2_response import SandboxLogsV2Response +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + sandbox_id: str, + *, + cursor: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 1000, + direction: Union[Unset, LogsDirection] = UNSET, +) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["cursor"] = cursor + + params["limit"] = limit + + json_direction: Union[Unset, str] = UNSET + if not isinstance(direction, Unset): + json_direction = direction.value + + params["direction"] = json_direction + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": f"/v2/sandboxes/{sandbox_id}/logs", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, SandboxLogsV2Response]]: + if response.status_code == 200: + response_200 = SandboxLogsV2Response.from_dict(response.json()) + + return response_200 + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if response.status_code == 500: + response_500 = Error.from_dict(response.json()) + + return response_500 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, SandboxLogsV2Response]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, + cursor: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 1000, + direction: Union[Unset, LogsDirection] = UNSET, +) -> Response[Union[Error, SandboxLogsV2Response]]: + """Get sandbox logs + + Args: + sandbox_id (str): + cursor (Union[Unset, int]): + limit (Union[Unset, int]): Default: 1000. + direction (Union[Unset, LogsDirection]): Direction of the logs that should be returned + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, SandboxLogsV2Response]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + cursor=cursor, + limit=limit, + direction=direction, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + sandbox_id: str, + *, + client: AuthenticatedClient, + cursor: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 1000, + direction: Union[Unset, LogsDirection] = UNSET, +) -> Optional[Union[Error, SandboxLogsV2Response]]: + """Get sandbox logs + + Args: + sandbox_id (str): + cursor (Union[Unset, int]): + limit (Union[Unset, int]): Default: 1000. + direction (Union[Unset, LogsDirection]): Direction of the logs that should be returned + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, SandboxLogsV2Response] + """ + + return sync_detailed( + sandbox_id=sandbox_id, + client=client, + cursor=cursor, + limit=limit, + direction=direction, + ).parsed + + +async def asyncio_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, + cursor: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 1000, + direction: Union[Unset, LogsDirection] = UNSET, +) -> Response[Union[Error, SandboxLogsV2Response]]: + """Get sandbox logs + + Args: + sandbox_id (str): + cursor (Union[Unset, int]): + limit (Union[Unset, int]): Default: 1000. + direction (Union[Unset, LogsDirection]): Direction of the logs that should be returned + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, SandboxLogsV2Response]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + cursor=cursor, + limit=limit, + direction=direction, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + sandbox_id: str, + *, + client: AuthenticatedClient, + cursor: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 1000, + direction: Union[Unset, LogsDirection] = UNSET, +) -> Optional[Union[Error, SandboxLogsV2Response]]: + """Get sandbox logs + + Args: + sandbox_id (str): + cursor (Union[Unset, int]): + limit (Union[Unset, int]): Default: 1000. + direction (Union[Unset, LogsDirection]): Direction of the logs that should be returned + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, SandboxLogsV2Response] + """ + + return ( + await asyncio_detailed( + sandbox_id=sandbox_id, + client=client, + cursor=cursor, + limit=limit, + direction=direction, + ) + ).parsed diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py index 3fd0a4aacf..09b2e2a697 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py @@ -6,18 +6,30 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.error import Error +from ...models.post_sandboxes_sandbox_id_snapshots_body import ( + PostSandboxesSandboxIDSnapshotsBody, +) from ...models.snapshot_info import SnapshotInfo from ...types import Response def _get_kwargs( sandbox_id: str, + *, + body: PostSandboxesSandboxIDSnapshotsBody, ) -> dict[str, Any]: + headers: dict[str, Any] = {} + _kwargs: dict[str, Any] = { "method": "post", "url": f"/sandboxes/{sandbox_id}/snapshots", } + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers return _kwargs @@ -65,12 +77,14 @@ def sync_detailed( sandbox_id: str, *, client: AuthenticatedClient, + body: PostSandboxesSandboxIDSnapshotsBody, ) -> Response[Union[Error, SnapshotInfo]]: """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. Args: sandbox_id (str): + body (PostSandboxesSandboxIDSnapshotsBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -82,6 +96,7 @@ def sync_detailed( kwargs = _get_kwargs( sandbox_id=sandbox_id, + body=body, ) response = client.get_httpx_client().request( @@ -95,12 +110,14 @@ def sync( sandbox_id: str, *, client: AuthenticatedClient, + body: PostSandboxesSandboxIDSnapshotsBody, ) -> Optional[Union[Error, SnapshotInfo]]: """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. Args: sandbox_id (str): + body (PostSandboxesSandboxIDSnapshotsBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -113,6 +130,7 @@ def sync( return sync_detailed( sandbox_id=sandbox_id, client=client, + body=body, ).parsed @@ -120,12 +138,14 @@ async def asyncio_detailed( sandbox_id: str, *, client: AuthenticatedClient, + body: PostSandboxesSandboxIDSnapshotsBody, ) -> Response[Union[Error, SnapshotInfo]]: """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. Args: sandbox_id (str): + body (PostSandboxesSandboxIDSnapshotsBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -137,6 +157,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( sandbox_id=sandbox_id, + body=body, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -148,12 +169,14 @@ async def asyncio( sandbox_id: str, *, client: AuthenticatedClient, + body: PostSandboxesSandboxIDSnapshotsBody, ) -> Optional[Union[Error, SnapshotInfo]]: """Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. Args: sandbox_id (str): + body (PostSandboxesSandboxIDSnapshotsBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -167,5 +190,6 @@ async def asyncio( await asyncio_detailed( sandbox_id=sandbox_id, client=client, + body=body, ) ).parsed diff --git a/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py b/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py index 88b118507d..1b826e3d64 100644 --- a/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py +++ b/packages/python-sdk/e2b/api/client/api/snapshots/get_snapshots.py @@ -13,7 +13,7 @@ def _get_kwargs( *, sandbox_id: Union[Unset, str] = UNSET, - limit: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 100, next_token: Union[Unset, str] = UNSET, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -76,14 +76,14 @@ def sync_detailed( *, client: AuthenticatedClient, sandbox_id: Union[Unset, str] = UNSET, - limit: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 100, next_token: Union[Unset, str] = UNSET, ) -> Response[Union[Error, list["SnapshotInfo"]]]: """List all snapshots for the team Args: - sandbox_id (Union[Unset, str]): - limit (Union[Unset, int]): + sandbox_id (Union[Unset, str]): Filter snapshots by source sandbox ID + limit (Union[Unset, int]): Default: 100. next_token (Union[Unset, str]): Raises: @@ -111,14 +111,14 @@ def sync( *, client: AuthenticatedClient, sandbox_id: Union[Unset, str] = UNSET, - limit: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 100, next_token: Union[Unset, str] = UNSET, ) -> Optional[Union[Error, list["SnapshotInfo"]]]: """List all snapshots for the team Args: - sandbox_id (Union[Unset, str]): - limit (Union[Unset, int]): + sandbox_id (Union[Unset, str]): Filter snapshots by source sandbox ID + limit (Union[Unset, int]): Default: 100. next_token (Union[Unset, str]): Raises: @@ -141,14 +141,14 @@ async def asyncio_detailed( *, client: AuthenticatedClient, sandbox_id: Union[Unset, str] = UNSET, - limit: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 100, next_token: Union[Unset, str] = UNSET, ) -> Response[Union[Error, list["SnapshotInfo"]]]: """List all snapshots for the team Args: - sandbox_id (Union[Unset, str]): - limit (Union[Unset, int]): + sandbox_id (Union[Unset, str]): Filter snapshots by source sandbox ID + limit (Union[Unset, int]): Default: 100. next_token (Union[Unset, str]): Raises: @@ -174,14 +174,14 @@ async def asyncio( *, client: AuthenticatedClient, sandbox_id: Union[Unset, str] = UNSET, - limit: Union[Unset, int] = UNSET, + limit: Union[Unset, int] = 100, next_token: Union[Unset, str] = UNSET, ) -> Optional[Union[Error, list["SnapshotInfo"]]]: """List all snapshots for the team Args: - sandbox_id (Union[Unset, str]): - limit (Union[Unset, int]): + sandbox_id (Union[Unset, str]): Filter snapshots by source sandbox ID + limit (Union[Unset, int]): Default: 100. next_token (Union[Unset, str]): Raises: diff --git a/packages/python-sdk/e2b/api/client/api/tags/get_templates_template_id_tags.py b/packages/python-sdk/e2b/api/client/api/tags/get_templates_template_id_tags.py new file mode 100644 index 0000000000..60bc625f6e --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/tags/get_templates_template_id_tags.py @@ -0,0 +1,172 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...models.template_tag import TemplateTag +from ...types import Response + + +def _get_kwargs( + template_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "get", + "url": f"/templates/{template_id}/tags", + } + + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, list["TemplateTag"]]]: + if response.status_code == 200: + response_200 = [] + _response_200 = response.json() + for response_200_item_data in _response_200: + response_200_item = TemplateTag.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if response.status_code == 500: + response_500 = Error.from_dict(response.json()) + + return response_500 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, list["TemplateTag"]]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + template_id: str, + *, + client: AuthenticatedClient, +) -> Response[Union[Error, list["TemplateTag"]]]: + """List all tags for a template + + Args: + template_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, list['TemplateTag']]] + """ + + kwargs = _get_kwargs( + template_id=template_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + template_id: str, + *, + client: AuthenticatedClient, +) -> Optional[Union[Error, list["TemplateTag"]]]: + """List all tags for a template + + Args: + template_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, list['TemplateTag']] + """ + + return sync_detailed( + template_id=template_id, + client=client, + ).parsed + + +async def asyncio_detailed( + template_id: str, + *, + client: AuthenticatedClient, +) -> Response[Union[Error, list["TemplateTag"]]]: + """List all tags for a template + + Args: + template_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, list['TemplateTag']]] + """ + + kwargs = _get_kwargs( + template_id=template_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + template_id: str, + *, + client: AuthenticatedClient, +) -> Optional[Union[Error, list["TemplateTag"]]]: + """List all tags for a template + + Args: + template_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, list['TemplateTag']] + """ + + return ( + await asyncio_detailed( + template_id=template_id, + client=client, + ) + ).parsed diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index 21ee10cdeb..d06a594bd8 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -28,6 +28,7 @@ from .new_access_token import NewAccessToken from .new_sandbox import NewSandbox from .new_team_api_key import NewTeamAPIKey +from .new_volume import NewVolume from .node import Node from .node_detail import NodeDetail from .node_metrics import NodeMetrics @@ -36,17 +37,24 @@ from .post_sandboxes_sandbox_id_refreshes_body import ( PostSandboxesSandboxIDRefreshesBody, ) +from .post_sandboxes_sandbox_id_snapshots_body import ( + PostSandboxesSandboxIDSnapshotsBody, +) from .post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody from .resumed_sandbox import ResumedSandbox from .sandbox import Sandbox +from .sandbox_auto_resume_config import SandboxAutoResumeConfig +from .sandbox_auto_resume_policy import SandboxAutoResumePolicy from .sandbox_detail import SandboxDetail from .sandbox_log import SandboxLog from .sandbox_log_entry import SandboxLogEntry from .sandbox_log_entry_fields import SandboxLogEntryFields from .sandbox_logs import SandboxLogs +from .sandbox_logs_v2_response import SandboxLogsV2Response from .sandbox_metric import SandboxMetric from .sandbox_network_config import SandboxNetworkConfig from .sandbox_state import SandboxState +from .sandbox_volume_mount import SandboxVolumeMount from .sandboxes_with_metrics import SandboxesWithMetrics from .snapshot_info import SnapshotInfo from .team import Team @@ -67,10 +75,12 @@ from .template_legacy import TemplateLegacy from .template_request_response_v3 import TemplateRequestResponseV3 from .template_step import TemplateStep +from .template_tag import TemplateTag from .template_update_request import TemplateUpdateRequest from .template_update_response import TemplateUpdateResponse from .template_with_builds import TemplateWithBuilds from .update_team_api_key import UpdateTeamAPIKey +from .volume import Volume __all__ = ( "AdminSandboxKillResult", @@ -101,24 +111,30 @@ "NewAccessToken", "NewSandbox", "NewTeamAPIKey", + "NewVolume", "Node", "NodeDetail", "NodeMetrics", "NodeStatus", "NodeStatusChange", "PostSandboxesSandboxIDRefreshesBody", + "PostSandboxesSandboxIDSnapshotsBody", "PostSandboxesSandboxIDTimeoutBody", "ResumedSandbox", "Sandbox", + "SandboxAutoResumeConfig", + "SandboxAutoResumePolicy", "SandboxDetail", "SandboxesWithMetrics", "SandboxLog", "SandboxLogEntry", "SandboxLogEntryFields", "SandboxLogs", + "SandboxLogsV2Response", "SandboxMetric", "SandboxNetworkConfig", "SandboxState", + "SandboxVolumeMount", "SnapshotInfo", "Team", "TeamAPIKey", @@ -138,8 +154,10 @@ "TemplateLegacy", "TemplateRequestResponseV3", "TemplateStep", + "TemplateTag", "TemplateUpdateRequest", "TemplateUpdateResponse", "TemplateWithBuilds", "UpdateTeamAPIKey", + "Volume", ) diff --git a/packages/python-sdk/e2b/api/client/models/listed_sandbox.py b/packages/python-sdk/e2b/api/client/models/listed_sandbox.py index 2d385a48e3..0324ace54d 100644 --- a/packages/python-sdk/e2b/api/client/models/listed_sandbox.py +++ b/packages/python-sdk/e2b/api/client/models/listed_sandbox.py @@ -1,6 +1,6 @@ import datetime from collections.abc import Mapping -from typing import Any, TypeVar, Union +from typing import TYPE_CHECKING, Any, TypeVar, Union from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -9,6 +9,10 @@ from ..models.sandbox_state import SandboxState from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.sandbox_volume_mount import SandboxVolumeMount + + T = TypeVar("T", bound="ListedSandbox") @@ -26,6 +30,7 @@ class ListedSandbox: started_at (datetime.datetime): Time when the sandbox was started state (SandboxState): State of the sandbox template_id (str): Identifier of the template from which is the sandbox created + volume_mounts (list['SandboxVolumeMount']): alias (Union[Unset, str]): Alias of the template metadata (Union[Unset, Any]): """ @@ -40,6 +45,7 @@ class ListedSandbox: started_at: datetime.datetime state: SandboxState template_id: str + volume_mounts: list["SandboxVolumeMount"] alias: Union[Unset, str] = UNSET metadata: Union[Unset, Any] = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -65,6 +71,11 @@ def to_dict(self) -> dict[str, Any]: template_id = self.template_id + volume_mounts = [] + for volume_mounts_item_data in self.volume_mounts: + volume_mounts_item = volume_mounts_item_data.to_dict() + volume_mounts.append(volume_mounts_item) + alias = self.alias metadata = self.metadata @@ -83,6 +94,7 @@ def to_dict(self) -> dict[str, Any]: "startedAt": started_at, "state": state, "templateID": template_id, + "volumeMounts": volume_mounts, } ) if alias is not UNSET: @@ -94,6 +106,8 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.sandbox_volume_mount import SandboxVolumeMount + d = dict(src_dict) client_id = d.pop("clientID") @@ -115,6 +129,13 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: template_id = d.pop("templateID") + volume_mounts = [] + _volume_mounts = d.pop("volumeMounts") + for volume_mounts_item_data in _volume_mounts: + volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) + + volume_mounts.append(volume_mounts_item) + alias = d.pop("alias", UNSET) metadata = d.pop("metadata", UNSET) @@ -130,6 +151,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: started_at=started_at, state=state, template_id=template_id, + volume_mounts=volume_mounts, alias=alias, metadata=metadata, ) diff --git a/packages/python-sdk/e2b/api/client/models/new_sandbox.py b/packages/python-sdk/e2b/api/client/models/new_sandbox.py index 1f80c900c4..dcac3592c3 100644 --- a/packages/python-sdk/e2b/api/client/models/new_sandbox.py +++ b/packages/python-sdk/e2b/api/client/models/new_sandbox.py @@ -8,7 +8,9 @@ if TYPE_CHECKING: from ..models.mcp_type_0 import McpType0 + from ..models.sandbox_auto_resume_config import SandboxAutoResumeConfig from ..models.sandbox_network_config import SandboxNetworkConfig + from ..models.sandbox_volume_mount import SandboxVolumeMount T = TypeVar("T", bound="NewSandbox") @@ -22,23 +24,28 @@ class NewSandbox: allow_internet_access (Union[Unset, bool]): Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. auto_pause (Union[Unset, bool]): Automatically pauses the sandbox after the timeout Default: False. + auto_resume (Union[Unset, SandboxAutoResumeConfig]): Auto-resume configuration for paused sandboxes. Default is + off. env_vars (Union[Unset, Any]): mcp (Union['McpType0', None, Unset]): MCP configuration for the sandbox metadata (Union[Unset, Any]): network (Union[Unset, SandboxNetworkConfig]): secure (Union[Unset, bool]): Secure all system communication with sandbox timeout (Union[Unset, int]): Time to live for the sandbox in seconds. Default: 15. + volume_mounts (Union[Unset, list['SandboxVolumeMount']]): """ template_id: str allow_internet_access: Union[Unset, bool] = UNSET auto_pause: Union[Unset, bool] = False + auto_resume: Union[Unset, "SandboxAutoResumeConfig"] = UNSET env_vars: Union[Unset, Any] = UNSET mcp: Union["McpType0", None, Unset] = UNSET metadata: Union[Unset, Any] = UNSET network: Union[Unset, "SandboxNetworkConfig"] = UNSET secure: Union[Unset, bool] = UNSET timeout: Union[Unset, int] = 15 + volume_mounts: Union[Unset, list["SandboxVolumeMount"]] = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -50,6 +57,10 @@ def to_dict(self) -> dict[str, Any]: auto_pause = self.auto_pause + auto_resume: Union[Unset, dict[str, Any]] = UNSET + if not isinstance(self.auto_resume, Unset): + auto_resume = self.auto_resume.to_dict() + env_vars = self.env_vars mcp: Union[None, Unset, dict[str, Any]] @@ -70,6 +81,13 @@ def to_dict(self) -> dict[str, Any]: timeout = self.timeout + volume_mounts: Union[Unset, list[dict[str, Any]]] = UNSET + if not isinstance(self.volume_mounts, Unset): + volume_mounts = [] + for volume_mounts_item_data in self.volume_mounts: + volume_mounts_item = volume_mounts_item_data.to_dict() + volume_mounts.append(volume_mounts_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -81,6 +99,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_internet_access"] = allow_internet_access if auto_pause is not UNSET: field_dict["autoPause"] = auto_pause + if auto_resume is not UNSET: + field_dict["autoResume"] = auto_resume if env_vars is not UNSET: field_dict["envVars"] = env_vars if mcp is not UNSET: @@ -93,13 +113,17 @@ def to_dict(self) -> dict[str, Any]: field_dict["secure"] = secure if timeout is not UNSET: field_dict["timeout"] = timeout + if volume_mounts is not UNSET: + field_dict["volumeMounts"] = volume_mounts return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.mcp_type_0 import McpType0 + from ..models.sandbox_auto_resume_config import SandboxAutoResumeConfig from ..models.sandbox_network_config import SandboxNetworkConfig + from ..models.sandbox_volume_mount import SandboxVolumeMount d = dict(src_dict) template_id = d.pop("templateID") @@ -108,6 +132,13 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: auto_pause = d.pop("autoPause", UNSET) + _auto_resume = d.pop("autoResume", UNSET) + auto_resume: Union[Unset, SandboxAutoResumeConfig] + if isinstance(_auto_resume, Unset): + auto_resume = UNSET + else: + auto_resume = SandboxAutoResumeConfig.from_dict(_auto_resume) + env_vars = d.pop("envVars", UNSET) def _parse_mcp(data: object) -> Union["McpType0", None, Unset]: @@ -140,16 +171,25 @@ def _parse_mcp(data: object) -> Union["McpType0", None, Unset]: timeout = d.pop("timeout", UNSET) + volume_mounts = [] + _volume_mounts = d.pop("volumeMounts", UNSET) + for volume_mounts_item_data in _volume_mounts or []: + volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) + + volume_mounts.append(volume_mounts_item) + new_sandbox = cls( template_id=template_id, allow_internet_access=allow_internet_access, auto_pause=auto_pause, + auto_resume=auto_resume, env_vars=env_vars, mcp=mcp, metadata=metadata, network=network, secure=secure, timeout=timeout, + volume_mounts=volume_mounts, ) new_sandbox.additional_properties = d diff --git a/packages/python-sdk/e2b/api/client/models/new_volume.py b/packages/python-sdk/e2b/api/client/models/new_volume.py new file mode 100644 index 0000000000..02dc4505f0 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/new_volume.py @@ -0,0 +1,59 @@ +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="NewVolume") + + +@_attrs_define +class NewVolume: + """ + Attributes: + name (str): Name of the volume + """ + + name: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + new_volume = cls( + name=name, + ) + + new_volume.additional_properties = d + return new_volume + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py b/packages/python-sdk/e2b/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py new file mode 100644 index 0000000000..680f5b5ecc --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py @@ -0,0 +1,60 @@ +from collections.abc import Mapping +from typing import Any, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PostSandboxesSandboxIDSnapshotsBody") + + +@_attrs_define +class PostSandboxesSandboxIDSnapshotsBody: + """ + Attributes: + name (Union[Unset, str]): 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: Union[Unset, str] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if name is not UNSET: + field_dict["name"] = name + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name", UNSET) + + post_sandboxes_sandbox_id_snapshots_body = cls( + name=name, + ) + + post_sandboxes_sandbox_id_snapshots_body.additional_properties = d + return post_sandboxes_sandbox_id_snapshots_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_config.py b/packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_config.py new file mode 100644 index 0000000000..f4c66e0c87 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_config.py @@ -0,0 +1,62 @@ +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.sandbox_auto_resume_policy import SandboxAutoResumePolicy + +T = TypeVar("T", bound="SandboxAutoResumeConfig") + + +@_attrs_define +class SandboxAutoResumeConfig: + """Auto-resume configuration for paused sandboxes. Default is off. + + Attributes: + policy (SandboxAutoResumePolicy): Auto-resume policy for paused sandboxes. Default is off. + """ + + policy: SandboxAutoResumePolicy + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + policy = self.policy.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "policy": policy, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + policy = SandboxAutoResumePolicy(d.pop("policy")) + + sandbox_auto_resume_config = cls( + policy=policy, + ) + + sandbox_auto_resume_config.additional_properties = d + return sandbox_auto_resume_config + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_policy.py b/packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_policy.py new file mode 100644 index 0000000000..cec37aa608 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/sandbox_auto_resume_policy.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class SandboxAutoResumePolicy(str, Enum): + ANY = "any" + OFF = "off" + + def __str__(self) -> str: + return str(self.value) diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_detail.py b/packages/python-sdk/e2b/api/client/models/sandbox_detail.py index da864cc449..9ab03dce64 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_detail.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_detail.py @@ -1,6 +1,6 @@ import datetime from collections.abc import Mapping -from typing import Any, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, TypeVar, Union, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -9,6 +9,10 @@ from ..models.sandbox_state import SandboxState from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.sandbox_volume_mount import SandboxVolumeMount + + T = TypeVar("T", bound="SandboxDetail") @@ -26,6 +30,7 @@ class SandboxDetail: started_at (datetime.datetime): Time when the sandbox was started state (SandboxState): State of the sandbox template_id (str): Identifier of the template from which is the sandbox created + volume_mounts (list['SandboxVolumeMount']): alias (Union[Unset, str]): Alias of the template domain (Union[None, Unset, str]): Base domain where the sandbox traffic is accessible envd_access_token (Union[Unset, str]): Access token used for envd communication @@ -42,6 +47,7 @@ class SandboxDetail: started_at: datetime.datetime state: SandboxState template_id: str + volume_mounts: list["SandboxVolumeMount"] alias: Union[Unset, str] = UNSET domain: Union[None, Unset, str] = UNSET envd_access_token: Union[Unset, str] = UNSET @@ -69,6 +75,11 @@ def to_dict(self) -> dict[str, Any]: template_id = self.template_id + volume_mounts = [] + for volume_mounts_item_data in self.volume_mounts: + volume_mounts_item = volume_mounts_item_data.to_dict() + volume_mounts.append(volume_mounts_item) + alias = self.alias domain: Union[None, Unset, str] @@ -95,6 +106,7 @@ def to_dict(self) -> dict[str, Any]: "startedAt": started_at, "state": state, "templateID": template_id, + "volumeMounts": volume_mounts, } ) if alias is not UNSET: @@ -110,6 +122,8 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.sandbox_volume_mount import SandboxVolumeMount + d = dict(src_dict) client_id = d.pop("clientID") @@ -131,6 +145,13 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: template_id = d.pop("templateID") + volume_mounts = [] + _volume_mounts = d.pop("volumeMounts") + for volume_mounts_item_data in _volume_mounts: + volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) + + volume_mounts.append(volume_mounts_item) + alias = d.pop("alias", UNSET) def _parse_domain(data: object) -> Union[None, Unset, str]: @@ -157,6 +178,7 @@ def _parse_domain(data: object) -> Union[None, Unset, str]: started_at=started_at, state=state, template_id=template_id, + volume_mounts=volume_mounts, alias=alias, domain=domain, envd_access_token=envd_access_token, diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_logs_v2_response.py b/packages/python-sdk/e2b/api/client/models/sandbox_logs_v2_response.py new file mode 100644 index 0000000000..f7e25448f3 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/sandbox_logs_v2_response.py @@ -0,0 +1,73 @@ +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.sandbox_log_entry import SandboxLogEntry + + +T = TypeVar("T", bound="SandboxLogsV2Response") + + +@_attrs_define +class SandboxLogsV2Response: + """ + Attributes: + logs (list['SandboxLogEntry']): Sandbox logs structured + """ + + logs: list["SandboxLogEntry"] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + logs = [] + for logs_item_data in self.logs: + logs_item = logs_item_data.to_dict() + logs.append(logs_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "logs": logs, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.sandbox_log_entry import SandboxLogEntry + + d = dict(src_dict) + logs = [] + _logs = d.pop("logs") + for logs_item_data in _logs: + logs_item = SandboxLogEntry.from_dict(logs_item_data) + + logs.append(logs_item) + + sandbox_logs_v2_response = cls( + logs=logs, + ) + + sandbox_logs_v2_response.additional_properties = d + return sandbox_logs_v2_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_volume_mount.py b/packages/python-sdk/e2b/api/client/models/sandbox_volume_mount.py new file mode 100644 index 0000000000..946cbbf1b0 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/sandbox_volume_mount.py @@ -0,0 +1,67 @@ +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SandboxVolumeMount") + + +@_attrs_define +class SandboxVolumeMount: + """ + Attributes: + name (str): Name of the volume + path (str): Path of the volume + """ + + name: str + path: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + path = self.path + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "path": path, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + path = d.pop("path") + + sandbox_volume_mount = cls( + name=name, + path=path, + ) + + sandbox_volume_mount.additional_properties = d + return sandbox_volume_mount + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/snapshot_info.py b/packages/python-sdk/e2b/api/client/models/snapshot_info.py index ff40bc2689..9cd5250dc9 100644 --- a/packages/python-sdk/e2b/api/client/models/snapshot_info.py +++ b/packages/python-sdk/e2b/api/client/models/snapshot_info.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, TypeVar +from typing import Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -11,19 +11,25 @@ class SnapshotInfo: """ Attributes: - snapshot_id (str): Unique identifier for the snapshot, can be used as template name + names (list[str]): Full names of the snapshot template including team namespace and tag (e.g. team-slug/my- + snapshot:v2) + snapshot_id (str): Identifier of the snapshot template """ + names: list[str] snapshot_id: str additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + names = self.names + snapshot_id = self.snapshot_id field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { + "names": names, "snapshotID": snapshot_id, } ) @@ -33,9 +39,12 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) + names = cast(list[str], d.pop("names")) + snapshot_id = d.pop("snapshotID") snapshot_info = cls( + names=names, snapshot_id=snapshot_id, ) diff --git a/packages/python-sdk/e2b/api/client/models/template_tag.py b/packages/python-sdk/e2b/api/client/models/template_tag.py new file mode 100644 index 0000000000..cd2680906f --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/template_tag.py @@ -0,0 +1,78 @@ +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar +from uuid import UUID + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +T = TypeVar("T", bound="TemplateTag") + + +@_attrs_define +class TemplateTag: + """ + Attributes: + build_id (UUID): Identifier of the build associated with this tag + created_at (datetime.datetime): Time when the tag was assigned + tag (str): The tag name + """ + + build_id: UUID + created_at: datetime.datetime + tag: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + build_id = str(self.build_id) + + created_at = self.created_at.isoformat() + + tag = self.tag + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "buildID": build_id, + "createdAt": created_at, + "tag": tag, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + build_id = UUID(d.pop("buildID")) + + created_at = isoparse(d.pop("createdAt")) + + tag = d.pop("tag") + + template_tag = cls( + build_id=build_id, + created_at=created_at, + tag=tag, + ) + + template_tag.additional_properties = d + return template_tag + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/volume.py b/packages/python-sdk/e2b/api/client/models/volume.py new file mode 100644 index 0000000000..77474989c2 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/volume.py @@ -0,0 +1,67 @@ +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="Volume") + + +@_attrs_define +class Volume: + """ + Attributes: + name (str): Name of the volume + volume_id (str): ID of the volume + """ + + name: str + volume_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + volume_id = self.volume_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "volumeID": volume_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + volume_id = d.pop("volumeID") + + volume = cls( + name=name, + volume_id=volume_id, + ) + + volume.additional_properties = d + return volume + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 47b03f6d09..9a08b00fca 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -20,6 +20,7 @@ ConnectSandbox, Error, NewSandbox, + PostSandboxesSandboxIDSnapshotsBody, PostSandboxesSandboxIDTimeoutBody, Sandbox, SandboxNetworkConfig, @@ -285,6 +286,7 @@ async def _cls_create_snapshot( res = await post_sandboxes_sandbox_id_snapshots.asyncio_detailed( sandbox_id, client=api_client, + body=PostSandboxesSandboxIDSnapshotsBody(), ) if res.status_code == 404: diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 26e4668615..3fbd7f956e 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -20,6 +20,7 @@ ConnectSandbox, Error, NewSandbox, + PostSandboxesSandboxIDSnapshotsBody, PostSandboxesSandboxIDTimeoutBody, Sandbox, SandboxNetworkConfig, @@ -312,6 +313,7 @@ def _cls_create_snapshot( res = post_sandboxes_sandbox_id_snapshots.sync_detailed( sandbox_id, client=api_client, + body=PostSandboxesSandboxIDSnapshotsBody(), ) if res.status_code == 404: From b622691d93077bff7b42f08226cf585eafcce7d3 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Mon, 16 Feb 2026 17:18:56 -0800 Subject: [PATCH 5/9] remove not yet implemented volume mounts --- packages/js-sdk/src/api/schema.gen.ts | 4 +-- .../e2b/api/client/models/listed_sandbox.py | 33 +++++++++-------- .../e2b/api/client/models/sandbox_detail.py | 35 ++++++++++--------- spec/openapi.yml | 2 -- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 230c068058..74decb671d 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -1761,7 +1761,7 @@ export interface components { state: components["schemas"]["SandboxState"]; /** @description Identifier of the template from which is the sandbox created */ templateID: string; - volumeMounts: components["schemas"]["SandboxVolumeMount"][]; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; }; /** * @description State of the sandbox @@ -2047,7 +2047,7 @@ export interface components { state: components["schemas"]["SandboxState"]; /** @description Identifier of the template from which is the sandbox created */ templateID: string; - volumeMounts: components["schemas"]["SandboxVolumeMount"][]; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; }; SandboxesWithMetrics: { sandboxes: { diff --git a/packages/python-sdk/e2b/api/client/models/listed_sandbox.py b/packages/python-sdk/e2b/api/client/models/listed_sandbox.py index 0324ace54d..3cc572bf1e 100644 --- a/packages/python-sdk/e2b/api/client/models/listed_sandbox.py +++ b/packages/python-sdk/e2b/api/client/models/listed_sandbox.py @@ -30,9 +30,9 @@ class ListedSandbox: started_at (datetime.datetime): Time when the sandbox was started state (SandboxState): State of the sandbox template_id (str): Identifier of the template from which is the sandbox created - volume_mounts (list['SandboxVolumeMount']): alias (Union[Unset, str]): Alias of the template metadata (Union[Unset, Any]): + volume_mounts (Union[Unset, list['SandboxVolumeMount']]): """ client_id: str @@ -45,9 +45,9 @@ class ListedSandbox: started_at: datetime.datetime state: SandboxState template_id: str - volume_mounts: list["SandboxVolumeMount"] alias: Union[Unset, str] = UNSET metadata: Union[Unset, Any] = UNSET + volume_mounts: Union[Unset, list["SandboxVolumeMount"]] = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -71,15 +71,17 @@ def to_dict(self) -> dict[str, Any]: template_id = self.template_id - volume_mounts = [] - for volume_mounts_item_data in self.volume_mounts: - volume_mounts_item = volume_mounts_item_data.to_dict() - volume_mounts.append(volume_mounts_item) - alias = self.alias metadata = self.metadata + volume_mounts: Union[Unset, list[dict[str, Any]]] = UNSET + if not isinstance(self.volume_mounts, Unset): + volume_mounts = [] + for volume_mounts_item_data in self.volume_mounts: + volume_mounts_item = volume_mounts_item_data.to_dict() + volume_mounts.append(volume_mounts_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -94,13 +96,14 @@ def to_dict(self) -> dict[str, Any]: "startedAt": started_at, "state": state, "templateID": template_id, - "volumeMounts": volume_mounts, } ) if alias is not UNSET: field_dict["alias"] = alias if metadata is not UNSET: field_dict["metadata"] = metadata + if volume_mounts is not UNSET: + field_dict["volumeMounts"] = volume_mounts return field_dict @@ -129,17 +132,17 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: template_id = d.pop("templateID") + alias = d.pop("alias", UNSET) + + metadata = d.pop("metadata", UNSET) + volume_mounts = [] - _volume_mounts = d.pop("volumeMounts") - for volume_mounts_item_data in _volume_mounts: + _volume_mounts = d.pop("volumeMounts", UNSET) + for volume_mounts_item_data in _volume_mounts or []: volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) volume_mounts.append(volume_mounts_item) - alias = d.pop("alias", UNSET) - - metadata = d.pop("metadata", UNSET) - listed_sandbox = cls( client_id=client_id, cpu_count=cpu_count, @@ -151,9 +154,9 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: started_at=started_at, state=state, template_id=template_id, - volume_mounts=volume_mounts, alias=alias, metadata=metadata, + volume_mounts=volume_mounts, ) listed_sandbox.additional_properties = d diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_detail.py b/packages/python-sdk/e2b/api/client/models/sandbox_detail.py index 9ab03dce64..46455fc881 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_detail.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_detail.py @@ -30,11 +30,11 @@ class SandboxDetail: started_at (datetime.datetime): Time when the sandbox was started state (SandboxState): State of the sandbox template_id (str): Identifier of the template from which is the sandbox created - volume_mounts (list['SandboxVolumeMount']): alias (Union[Unset, str]): Alias of the template domain (Union[None, Unset, str]): Base domain where the sandbox traffic is accessible envd_access_token (Union[Unset, str]): Access token used for envd communication metadata (Union[Unset, Any]): + volume_mounts (Union[Unset, list['SandboxVolumeMount']]): """ client_id: str @@ -47,11 +47,11 @@ class SandboxDetail: started_at: datetime.datetime state: SandboxState template_id: str - volume_mounts: list["SandboxVolumeMount"] alias: Union[Unset, str] = UNSET domain: Union[None, Unset, str] = UNSET envd_access_token: Union[Unset, str] = UNSET metadata: Union[Unset, Any] = UNSET + volume_mounts: Union[Unset, list["SandboxVolumeMount"]] = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -75,11 +75,6 @@ def to_dict(self) -> dict[str, Any]: template_id = self.template_id - volume_mounts = [] - for volume_mounts_item_data in self.volume_mounts: - volume_mounts_item = volume_mounts_item_data.to_dict() - volume_mounts.append(volume_mounts_item) - alias = self.alias domain: Union[None, Unset, str] @@ -92,6 +87,13 @@ def to_dict(self) -> dict[str, Any]: metadata = self.metadata + volume_mounts: Union[Unset, list[dict[str, Any]]] = UNSET + if not isinstance(self.volume_mounts, Unset): + volume_mounts = [] + for volume_mounts_item_data in self.volume_mounts: + volume_mounts_item = volume_mounts_item_data.to_dict() + volume_mounts.append(volume_mounts_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -106,7 +108,6 @@ def to_dict(self) -> dict[str, Any]: "startedAt": started_at, "state": state, "templateID": template_id, - "volumeMounts": volume_mounts, } ) if alias is not UNSET: @@ -117,6 +118,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["envdAccessToken"] = envd_access_token if metadata is not UNSET: field_dict["metadata"] = metadata + if volume_mounts is not UNSET: + field_dict["volumeMounts"] = volume_mounts return field_dict @@ -145,13 +148,6 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: template_id = d.pop("templateID") - volume_mounts = [] - _volume_mounts = d.pop("volumeMounts") - for volume_mounts_item_data in _volume_mounts: - volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) - - volume_mounts.append(volume_mounts_item) - alias = d.pop("alias", UNSET) def _parse_domain(data: object) -> Union[None, Unset, str]: @@ -167,6 +163,13 @@ def _parse_domain(data: object) -> Union[None, Unset, str]: metadata = d.pop("metadata", UNSET) + volume_mounts = [] + _volume_mounts = d.pop("volumeMounts", UNSET) + for volume_mounts_item_data in _volume_mounts or []: + volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) + + volume_mounts.append(volume_mounts_item) + sandbox_detail = cls( client_id=client_id, cpu_count=cpu_count, @@ -178,11 +181,11 @@ def _parse_domain(data: object) -> Union[None, Unset, str]: started_at=started_at, state=state, template_id=template_id, - volume_mounts=volume_mounts, alias=alias, domain=domain, envd_access_token=envd_access_token, metadata=metadata, + volume_mounts=volume_mounts, ) sandbox_detail.additional_properties = d diff --git a/spec/openapi.yml b/spec/openapi.yml index b5e6fac7aa..94b9cf7d84 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -467,7 +467,6 @@ components: - endAt - state - envdVersion - - volumeMounts properties: templateID: type: string @@ -526,7 +525,6 @@ components: - endAt - state - envdVersion - - volumeMounts properties: templateID: type: string From 0a512529b52e0dec7cb530031becaca23e5f8c3c Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Mon, 16 Feb 2026 18:07:59 -0800 Subject: [PATCH 6/9] docs: clarify snapshot ID description in JS and Python SDKs --- packages/js-sdk/src/sandbox/sandboxApi.ts | 6 +++--- packages/python-sdk/e2b/sandbox/sandbox_api.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 66abf94ad5..706d879678 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -232,8 +232,8 @@ export interface SnapshotListOpts extends SandboxApiOpts { */ export interface SnapshotInfo { /** - * Unique identifier for the snapshot. - * Can be used as templateId in Sandbox.create() to create a new sandbox from this snapshot. + * 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 } @@ -576,7 +576,7 @@ export class SandboxApi { * @param sandboxId sandbox ID to create snapshot from. * @param opts snapshot options. * - * @returns snapshot information including the snapshot ID that can be used with Sandbox.create(). + * @returns snapshot information including the snapshot name that can be used with Sandbox.create(). */ static async createSnapshot( sandboxId: string, diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index 4210732cb2..d112475493 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -187,7 +187,7 @@ class SnapshotInfo: """Information about a snapshot.""" snapshot_id: str - """Unique identifier for the snapshot. Can be used as template ID in Sandbox.create() to create a new sandbox from this snapshot.""" + """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.""" class SnapshotPaginatorBase: From ee06cb944a7188391c1bfcef7983bab784f2193d Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Mon, 16 Feb 2026 19:20:55 -0800 Subject: [PATCH 7/9] fix nits --- packages/js-sdk/src/sandbox/sandboxApi.ts | 4 ++-- packages/python-sdk/e2b/sandbox_async/paginator.py | 6 +++--- packages/python-sdk/e2b/sandbox_sync/paginator.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 706d879678..b0036dafaf 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -793,7 +793,7 @@ export class SandboxPaginator { } /** - * Returns True if there are more items to fetch. + * Returns true if there are more items to fetch. */ get hasNext(): boolean { return this._hasNext @@ -920,7 +920,7 @@ export class SnapshotPaginator { } /** - * Returns True if there are more items to fetch. + * Returns true if there are more items to fetch. */ get hasNext(): boolean { return this._hasNext diff --git a/packages/python-sdk/e2b/sandbox_async/paginator.py b/packages/python-sdk/e2b/sandbox_async/paginator.py index b67a177a04..9497997042 100644 --- a/packages/python-sdk/e2b/sandbox_async/paginator.py +++ b/packages/python-sdk/e2b/sandbox_async/paginator.py @@ -39,7 +39,7 @@ async def next_items(self) -> List[SandboxInfo]: :returns: List of sandboxes """ if not self.has_next: - raise Exception("No more items to fetch") + raise SandboxException("No more items to fetch") # Convert filters to the format expected by the API metadata: Optional[str] = None @@ -68,7 +68,7 @@ async def next_items(self) -> List[SandboxInfo]: if res.parsed is None: return [] - # Check if res.parse is Error + # Check if res.parsed is Error if isinstance(res.parsed, Error): raise SandboxException(f"{res.parsed.message}: Request failed") @@ -98,7 +98,7 @@ async def next_items(self) -> List[SnapshotInfo]: :returns: List of snapshots """ if not self.has_next: - raise Exception("No more items to fetch") + raise SandboxException("No more items to fetch") api_client = get_api_client(self._config) res = await get_snapshots.asyncio_detailed( diff --git a/packages/python-sdk/e2b/sandbox_sync/paginator.py b/packages/python-sdk/e2b/sandbox_sync/paginator.py index 2984f5ce6e..d588805db0 100644 --- a/packages/python-sdk/e2b/sandbox_sync/paginator.py +++ b/packages/python-sdk/e2b/sandbox_sync/paginator.py @@ -39,7 +39,7 @@ def next_items(self) -> List[SandboxInfo]: :returns: List of sandboxes """ if not self.has_next: - raise Exception("No more items to fetch") + raise SandboxException("No more items to fetch") # Convert filters to the format expected by the API metadata: Optional[str] = None @@ -68,7 +68,7 @@ def next_items(self) -> List[SandboxInfo]: if res.parsed is None: return [] - # Check if res.parse is Error + # Check if res.parsed is Error if isinstance(res.parsed, Error): raise SandboxException(f"{res.parsed.message}: Request failed") @@ -98,7 +98,7 @@ def next_items(self) -> List[SnapshotInfo]: :returns: List of snapshots """ if not self.has_next: - raise Exception("No more items to fetch") + raise SandboxException("No more items to fetch") api_client = get_api_client(self._config) res = get_snapshots.sync_detailed( From 881b3108e13dfa8b71a2a57eb439e65cb301ff7b Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Wed, 18 Feb 2026 04:59:14 -0800 Subject: [PATCH 8/9] fix: use UUIDs instead of timestamps for unique IDs in list sandbox tests Timestamp-based unique IDs caused flaky failures when parallel test workers (async/sync variants) generated the same value within the same second/millisecond. --- packages/js-sdk/tests/api/list.test.ts | 5 +++-- packages/python-sdk/tests/async/api_async/test_sbx_list.py | 4 ++-- packages/python-sdk/tests/sync/api_sync/test_sbx_list.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/js-sdk/tests/api/list.test.ts b/packages/js-sdk/tests/api/list.test.ts index 2ed7662540..3960b9fffd 100644 --- a/packages/js-sdk/tests/api/list.test.ts +++ b/packages/js-sdk/tests/api/list.test.ts @@ -1,4 +1,5 @@ import { assert } from 'vitest' +import { randomUUID } from 'crypto' import { Sandbox, SandboxInfo } from '../../src' import { sandboxTest, isDebug } from '../setup.js' @@ -19,7 +20,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => { - const uniqueId = Date.now().toString() + const uniqueId = randomUUID() const extraSbx = await Sandbox.create({ metadata: { uniqueId } }) try { @@ -236,7 +237,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => { - const uniqueId = Date.now().toString() + const uniqueId = randomUUID() const extraSbx = await Sandbox.create({ metadata: { uniqueId } }) try { diff --git a/packages/python-sdk/tests/async/api_async/test_sbx_list.py b/packages/python-sdk/tests/async/api_async/test_sbx_list.py index 71036869d0..e902fd9a07 100644 --- a/packages/python-sdk/tests/async/api_async/test_sbx_list.py +++ b/packages/python-sdk/tests/async/api_async/test_sbx_list.py @@ -1,4 +1,4 @@ -import time +import uuid import pytest @@ -17,7 +17,7 @@ async def test_list_sandboxes(async_sandbox: AsyncSandbox, sandbox_test_id: str) @pytest.mark.skip_debug() async def test_list_sandboxes_with_filter(sandbox_test_id: str, async_sandbox_factory): - unique_id = str(int(time.time())) + unique_id = str(uuid.uuid4()) extra_sbx = await async_sandbox_factory(metadata={"unique_id": unique_id}) paginator = AsyncSandbox.list(query=SandboxQuery(metadata={"unique_id": unique_id})) diff --git a/packages/python-sdk/tests/sync/api_sync/test_sbx_list.py b/packages/python-sdk/tests/sync/api_sync/test_sbx_list.py index 20f2d0f39f..c139ccbd85 100644 --- a/packages/python-sdk/tests/sync/api_sync/test_sbx_list.py +++ b/packages/python-sdk/tests/sync/api_sync/test_sbx_list.py @@ -1,4 +1,4 @@ -import time +import uuid import pytest @@ -17,7 +17,7 @@ def test_list_sandboxes(sandbox: Sandbox, sandbox_test_id: str): @pytest.mark.skip_debug() def test_list_sandboxes_with_filter(sandbox_factory, sandbox_test_id: str): - unique_id = str(int(time.time())) + unique_id = str(uuid.uuid4()) extra_sbx = sandbox_factory(metadata={"unique_id": unique_id}) paginator = Sandbox.list(query=SandboxQuery(metadata={"unique_id": unique_id})) From 3fd6132960b1b1d4b16ee8cd2845c10a76eb39c7 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 24 Feb 2026 11:14:37 -0800 Subject: [PATCH 9/9] refactor: extract base paginator and clean up snapshot API - Extract BasePaginator class in JS and PaginatorBase in Python to deduplicate pagination logic between sandbox and snapshot - Remove SnapshotOpts in favor of SandboxApiOpts - Add instance-level list_snapshots method to Python SDKs - Remove async iterator from JS paginators - Simplify error handling in createSnapshot/deleteSnapshot - Add "sandbox will be paused" note to snapshot docs - Clean up snapshot tests to use variables for paths --- packages/js-sdk/src/index.ts | 1 - packages/js-sdk/src/sandbox/index.ts | 7 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 174 ++++++------------ .../js-sdk/tests/sandbox/snapshot-api.test.ts | 23 ++- .../python-sdk/e2b/sandbox/sandbox_api.py | 41 ++--- packages/python-sdk/e2b/sandbox_async/main.py | 53 ++++++ .../python-sdk/e2b/sandbox_async/paginator.py | 4 +- packages/python-sdk/e2b/sandbox_sync/main.py | 53 ++++++ .../python-sdk/e2b/sandbox_sync/paginator.py | 4 +- .../async/sandbox_async/test_snapshot_api.py | 22 ++- .../sync/sandbox_sync/test_snapshot_api.py | 22 ++- 11 files changed, 223 insertions(+), 181 deletions(-) diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 1bce78804c..de04c3c4ab 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -50,7 +50,6 @@ export type { SandboxPaginator, SandboxNetworkOpts, SnapshotInfo, - SnapshotOpts, SnapshotListOpts, SnapshotPaginator, } from './sandbox/sandboxApi' diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 0e9f5dda48..4d0d54c7ff 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -20,7 +20,7 @@ import { SandboxListOpts, SandboxPaginator, SandboxBetaCreateOpts, - SnapshotOpts, + SandboxApiOpts, SnapshotListOpts, SnapshotInfo, SnapshotPaginator, @@ -586,12 +586,13 @@ export class Sandbox extends SandboxApi { /** * Create a snapshot of the sandbox's current state. * + * The sandbox will be paused while the snapshot is being created. * The snapshot can be used to create new sandboxes with the same filesystem and state. * Snapshots are persistent and survive sandbox deletion. * * Use the returned `snapshotId` with `Sandbox.create(snapshotId)` to create a new sandbox from the snapshot. * - * @param opts snapshot options. + * @param opts connection options. * * @returns snapshot information including the snapshot ID. * @@ -607,7 +608,7 @@ export class Sandbox extends SandboxApi { * const newSandbox = await Sandbox.create(snapshot.snapshotId) * ``` */ - async createSnapshot(opts?: SnapshotOpts): Promise { + async createSnapshot(opts?: SandboxApiOpts): Promise { return await SandboxApi.createSnapshot(this.sandboxId, { ...this.connectionConfig, ...opts, diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index b0036dafaf..9a3bb7b9f0 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -5,7 +5,7 @@ import { DEFAULT_SANDBOX_TIMEOUT_MS, } from '../connectionConfig' import { compareVersions } from 'compare-versions' -import { NotFoundError, SandboxError, TemplateError } from '../errors' +import { NotFoundError, TemplateError } from '../errors' import { timeoutToSeconds } from '../utils' import type { McpServer as BaseMcpServer } from './mcp' @@ -200,11 +200,6 @@ export interface SandboxMetricsOpts extends SandboxApiOpts { end?: Date } -/** - * Options for creating a snapshot. - */ -export interface SnapshotOpts extends SandboxApiOpts {} - /** * Options for listing snapshots. */ @@ -570,17 +565,18 @@ export class SandboxApi { /** * Create a snapshot from a sandbox. * + * The sandbox will be paused while the snapshot is being created. * The snapshot can be used to create new sandboxes with the same state. * The snapshot is a persistent image that survives sandbox deletion. * * @param sandboxId sandbox ID to create snapshot from. - * @param opts snapshot options. + * @param opts connection options. * * @returns snapshot information including the snapshot name that can be used with Sandbox.create(). */ static async createSnapshot( sandboxId: string, - opts?: SnapshotOpts + opts?: SandboxApiOpts ): Promise { const config = new ConnectionConfig(opts) const client = new ApiClient(config) @@ -595,7 +591,7 @@ export class SandboxApi { signal: config.getSignal(opts?.requestTimeoutMs), }) - if (res.error?.code === 404 || res.response.status === 404) { + if (res.error?.code === 404) { throw new NotFoundError(`Sandbox ${sandboxId} not found`) } @@ -604,14 +600,8 @@ export class SandboxApi { throw err } - // Additional check for non-success status codes that might not have error body - if (!res.data || res.response.status >= 400) { - const errorMessage = res.error?.message || `HTTP ${res.response.status}` - throw new SandboxError(`Failed to create snapshot: ${errorMessage}`) - } - return { - snapshotId: res.data.snapshotID, + snapshotId: res.data!.snapshotID, } } @@ -650,7 +640,7 @@ export class SandboxApi { signal: config.getSignal(opts?.requestTimeoutMs), }) - if (res.error?.code === 404 || res.response.status === 404) { + if (res.error?.code === 404) { return false } @@ -680,10 +670,7 @@ export class SandboxApi { timeout: timeoutToSeconds(timeoutMs), secure: opts?.secure ?? true, allow_internet_access: opts?.allowInternetAccess ?? true, - network: opts?.network && { - ...opts.network, - allowPublicTraffic: opts.network.allowPublicTraffic ?? true, - }, + network: opts?.network, }, signal: config.getSignal(opts?.requestTimeoutMs), }) @@ -750,46 +737,22 @@ export class SandboxApi { } } -/** - * Paginator for listing sandboxes. - * - * @example - * ```ts - * // Using async iterator - * for await (const sandbox of Sandbox.list()) { - * console.log(sandbox.sandboxId) - * } - * - * // Or collect all to array - * const sandboxes = await Array.fromAsync(Sandbox.list()) - * - * // Manual pagination - * const paginator = Sandbox.list() - * while (paginator.hasNext) { - * const sandboxes = await paginator.nextItems() - * console.log(sandboxes) - * } - * ``` - */ -export class SandboxPaginator { +abstract class BasePaginator { + protected readonly config: ConnectionConfig + protected client: ApiClient + protected readonly limit?: number + private _hasNext: boolean private _nextToken?: string - private readonly config: ConnectionConfig - private client: ApiClient - - private query: SandboxListOpts['query'] - private readonly limit?: number - - constructor(opts?: SandboxListOpts) { - this.config = new ConnectionConfig(opts) + constructor(config: ConnectionConfig, limit?: number, nextToken?: string) { + this.config = config this.client = new ApiClient(this.config) this._hasNext = true - this._nextToken = opts?.nextToken + this._nextToken = nextToken - this.query = opts?.query - this.limit = opts?.limit + this.limit = limit } /** @@ -806,13 +769,42 @@ export class SandboxPaginator { return this._nextToken } + protected updatePagination(response: Response) { + this._nextToken = response.headers.get('x-next-token') || undefined + this._hasNext = !!this._nextToken + } + /** - * Get the next page of sandboxes. + * Get the next page of items. * * @throws Error if there are no more items to fetch. Call this method only if `hasNext` is `true`. * - * @returns List of sandboxes + * @returns List of items */ + abstract nextItems(): Promise +} + +/** + * Paginator for listing sandboxes. + * + * @example + * ```ts + * const paginator = Sandbox.list() + * while (paginator.hasNext) { + * const sandboxes = await paginator.nextItems() + * console.log(sandboxes) + * } + * ``` + */ +export class SandboxPaginator extends BasePaginator { + private query: SandboxListOpts['query'] + + constructor(opts?: SandboxListOpts) { + super(new ConnectionConfig(opts), opts?.limit, opts?.nextToken) + + this.query = opts?.query + } + async nextItems(): Promise { if (!this.hasNext) { throw new Error('No more items to fetch') @@ -848,8 +840,7 @@ export class SandboxPaginator { throw err } - this._nextToken = res.response.headers.get('x-next-token') || undefined - this._hasNext = !!this._nextToken + this.updatePagination(res.response) return (res.data ?? []).map( (sandbox: components['schemas']['ListedSandbox']) => ({ @@ -866,15 +857,6 @@ export class SandboxPaginator { }) ) } - - async *[Symbol.asyncIterator](): AsyncIterableIterator { - while (this.hasNext) { - const items = await this.nextItems() - for (const item of items) { - yield item - } - } - } } /** @@ -882,15 +864,6 @@ export class SandboxPaginator { * * @example * ```ts - * // Using async iterator - * for await (const snapshot of Sandbox.listSnapshots()) { - * console.log(snapshot.snapshotId) - * } - * - * // Or collect all to array - * const snapshots = await Array.fromAsync(Sandbox.listSnapshots()) - * - * // Manual pagination * const paginator = Sandbox.listSnapshots() * while (paginator.hasNext) { * const snapshots = await paginator.nextItems() @@ -898,48 +871,15 @@ export class SandboxPaginator { * } * ``` */ -export class SnapshotPaginator { - private _hasNext: boolean - private _nextToken?: string - - private readonly config: ConnectionConfig - private client: ApiClient - +export class SnapshotPaginator extends BasePaginator { private readonly sandboxId?: string - private readonly limit?: number constructor(opts?: SnapshotListOpts) { - this.config = new ConnectionConfig(opts) - this.client = new ApiClient(this.config) - - this._hasNext = true - this._nextToken = opts?.nextToken + super(new ConnectionConfig(opts), opts?.limit, opts?.nextToken) this.sandboxId = opts?.sandboxId - this.limit = opts?.limit } - /** - * Returns true if there are more items to fetch. - */ - get hasNext(): boolean { - return this._hasNext - } - - /** - * Returns the next token to use for pagination. - */ - get nextToken(): string | undefined { - return this._nextToken - } - - /** - * Get the next page of snapshots. - * - * @throws Error if there are no more items to fetch. Call this method only if `hasNext` is `true`. - * - * @returns List of snapshots - */ async nextItems(): Promise { if (!this.hasNext) { throw new Error('No more items to fetch') @@ -961,8 +901,7 @@ export class SnapshotPaginator { throw err } - this._nextToken = res.response.headers.get('x-next-token') || undefined - this._hasNext = !!this._nextToken + this.updatePagination(res.response) return (res.data ?? []).map( (snapshot: components['schemas']['SnapshotInfo']) => ({ @@ -970,13 +909,4 @@ export class SnapshotPaginator { }) ) } - - async *[Symbol.asyncIterator](): AsyncIterableIterator { - while (this.hasNext) { - const items = await this.nextItems() - for (const item of items) { - yield item - } - } - } } diff --git a/packages/js-sdk/tests/sandbox/snapshot-api.test.ts b/packages/js-sdk/tests/sandbox/snapshot-api.test.ts index ffffe3bc51..69eabf119e 100644 --- a/packages/js-sdk/tests/sandbox/snapshot-api.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot-api.test.ts @@ -156,10 +156,15 @@ sandboxTest.skipIf(isDebug)('delete snapshot', async ({ sandbox }) => { sandboxTest.skipIf(isDebug)( 'snapshot preserves file system state', async ({ sandbox, sandboxTestId }) => { - // Create directory structure - await sandbox.files.makeDir('/home/user/app') - await sandbox.files.write('/home/user/app/config.json', '{"env": "test"}') - await sandbox.files.write('/home/user/app/data.txt', 'important data') + const appDir = '/home/user/app' + const configPath = `${appDir}/config.json` + const configContent = '{"env": "test"}' + const dataPath = `${appDir}/data.txt` + const dataContent = 'important data' + + await sandbox.files.makeDir(appDir) + await sandbox.files.write(configPath, configContent) + await sandbox.files.write(dataPath, dataContent) const snapshot = await sandbox.createSnapshot() @@ -170,15 +175,15 @@ sandboxTest.skipIf(isDebug)( try { // Verify directory exists - const dirExists = await newSandbox.files.exists('/home/user/app') + const dirExists = await newSandbox.files.exists(appDir) assert.isTrue(dirExists) // Verify files exist with correct content - const config = await newSandbox.files.read('/home/user/app/config.json') - const data = await newSandbox.files.read('/home/user/app/data.txt') + const config = await newSandbox.files.read(configPath) + const data = await newSandbox.files.read(dataPath) - assert.equal(config, '{"env": "test"}') - assert.equal(data, 'important data') + assert.equal(config, configContent) + assert.equal(data, dataContent) } finally { await newSandbox.kill() } diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index d112475493..b5a715ee15 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -190,19 +190,15 @@ class 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.""" -class SnapshotPaginatorBase: +class PaginatorBase: def __init__( self, - sandbox_id: Optional[str] = None, limit: Optional[int] = None, next_token: Optional[str] = None, **opts: Unpack[ApiParams], ): self._config = ConnectionConfig(**opts) - - self.sandbox_id = sandbox_id self.limit = limit - self._has_next = True self._next_token = next_token @@ -221,32 +217,25 @@ def next_token(self) -> Optional[str]: return self._next_token -class SandboxPaginatorBase: +class SnapshotPaginatorBase(PaginatorBase): def __init__( self, - query: Optional[SandboxQuery] = None, + sandbox_id: Optional[str] = None, limit: Optional[int] = None, next_token: Optional[str] = None, **opts: Unpack[ApiParams], ): - self._config = ConnectionConfig(**opts) - - self.query = query - self.limit = limit - - self._has_next = True - self._next_token = next_token + super().__init__(limit=limit, next_token=next_token, **opts) + self.sandbox_id = sandbox_id - @property - def has_next(self) -> bool: - """ - Returns True if there are more items to fetch. - """ - return self._has_next - @property - def next_token(self) -> Optional[str]: - """ - Returns the next token to use for pagination. - """ - return self._next_token +class SandboxPaginatorBase(PaginatorBase): + def __init__( + self, + query: Optional[SandboxQuery] = None, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ): + super().__init__(limit=limit, next_token=next_token, **opts) + self.query = query diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index d3eb54646b..f7d0e0a590 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -643,6 +643,7 @@ async def create_snapshot( """ Create a snapshot of the sandbox's current state. + The sandbox will be paused while the snapshot is being created. The snapshot can be used to create new sandboxes with the same filesystem and state. Snapshots are persistent and survive sandbox deletion. @@ -661,6 +662,8 @@ async def create_snapshot( """ Create a snapshot from the sandbox specified by sandbox ID. + The sandbox will be paused while the snapshot is being created. + :param sandbox_id: Sandbox ID :return: Snapshot information including the snapshot ID @@ -675,6 +678,7 @@ async def create_snapshot( """ Create a snapshot of the sandbox's current state. + The sandbox will be paused while the snapshot is being created. The snapshot can be used to create new sandboxes with the same filesystem and state. Snapshots are persistent and survive sandbox deletion. @@ -687,6 +691,24 @@ async def create_snapshot( **self.connection_config.get_api_params(**opts), ) + @overload + def list_snapshots( + self, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> AsyncSnapshotPaginator: + """ + List snapshots for this sandbox. + + :param limit: Maximum number of snapshots to return per page + :param next_token: Token for pagination + + :return: Paginator for listing snapshots + """ + ... + + @overload @staticmethod def list_snapshots( sandbox_id: Optional[str] = None, @@ -703,6 +725,37 @@ def list_snapshots( :return: Paginator for listing snapshots """ + ... + + @class_method_variant("_cls_list_snapshots") + def list_snapshots( + self, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> AsyncSnapshotPaginator: + """ + List snapshots for this sandbox. + + :param limit: Maximum number of snapshots to return per page + :param next_token: Token for pagination + + :return: Paginator for listing snapshots + """ + return AsyncSnapshotPaginator( + sandbox_id=self.sandbox_id, + limit=limit, + next_token=next_token, + **self.connection_config.get_api_params(**opts), + ) + + @staticmethod + def _cls_list_snapshots( + sandbox_id: Optional[str] = None, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> AsyncSnapshotPaginator: return AsyncSnapshotPaginator( sandbox_id=sandbox_id, limit=limit, diff --git a/packages/python-sdk/e2b/sandbox_async/paginator.py b/packages/python-sdk/e2b/sandbox_async/paginator.py index 9497997042..de6d31fb63 100644 --- a/packages/python-sdk/e2b/sandbox_async/paginator.py +++ b/packages/python-sdk/e2b/sandbox_async/paginator.py @@ -39,7 +39,7 @@ async def next_items(self) -> List[SandboxInfo]: :returns: List of sandboxes """ if not self.has_next: - raise SandboxException("No more items to fetch") + raise Exception("No more items to fetch") # Convert filters to the format expected by the API metadata: Optional[str] = None @@ -98,7 +98,7 @@ async def next_items(self) -> List[SnapshotInfo]: :returns: List of snapshots """ if not self.has_next: - raise SandboxException("No more items to fetch") + raise Exception("No more items to fetch") api_client = get_api_client(self._config) res = await get_snapshots.asyncio_detailed( diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index f3fd1a10e2..673f919b9f 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -640,6 +640,7 @@ def create_snapshot( """ Create a snapshot of the sandbox's current state. + The sandbox will be paused while the snapshot is being created. The snapshot can be used to create new sandboxes with the same filesystem and state. Snapshots are persistent and survive sandbox deletion. @@ -658,6 +659,8 @@ def create_snapshot( """ Create a snapshot from the sandbox specified by sandbox ID. + The sandbox will be paused while the snapshot is being created. + :param sandbox_id: Sandbox ID :return: Snapshot information including the snapshot ID @@ -672,6 +675,7 @@ def create_snapshot( """ Create a snapshot of the sandbox's current state. + The sandbox will be paused while the snapshot is being created. The snapshot can be used to create new sandboxes with the same filesystem and state. Snapshots are persistent and survive sandbox deletion. @@ -684,6 +688,24 @@ def create_snapshot( **self.connection_config.get_api_params(**opts), ) + @overload + def list_snapshots( + self, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> SnapshotPaginator: + """ + List snapshots for this sandbox. + + :param limit: Maximum number of snapshots to return per page + :param next_token: Token for pagination + + :return: Paginator for listing snapshots + """ + ... + + @overload @staticmethod def list_snapshots( sandbox_id: Optional[str] = None, @@ -700,6 +722,37 @@ def list_snapshots( :return: Paginator for listing snapshots """ + ... + + @class_method_variant("_cls_list_snapshots") + def list_snapshots( + self, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> SnapshotPaginator: + """ + List snapshots for this sandbox. + + :param limit: Maximum number of snapshots to return per page + :param next_token: Token for pagination + + :return: Paginator for listing snapshots + """ + return SnapshotPaginator( + sandbox_id=self.sandbox_id, + limit=limit, + next_token=next_token, + **self.connection_config.get_api_params(**opts), + ) + + @staticmethod + def _cls_list_snapshots( + sandbox_id: Optional[str] = None, + limit: Optional[int] = None, + next_token: Optional[str] = None, + **opts: Unpack[ApiParams], + ) -> SnapshotPaginator: return SnapshotPaginator( sandbox_id=sandbox_id, limit=limit, diff --git a/packages/python-sdk/e2b/sandbox_sync/paginator.py b/packages/python-sdk/e2b/sandbox_sync/paginator.py index d588805db0..0a5649052b 100644 --- a/packages/python-sdk/e2b/sandbox_sync/paginator.py +++ b/packages/python-sdk/e2b/sandbox_sync/paginator.py @@ -39,7 +39,7 @@ def next_items(self) -> List[SandboxInfo]: :returns: List of sandboxes """ if not self.has_next: - raise SandboxException("No more items to fetch") + raise Exception("No more items to fetch") # Convert filters to the format expected by the API metadata: Optional[str] = None @@ -98,7 +98,7 @@ def next_items(self) -> List[SnapshotInfo]: :returns: List of snapshots """ if not self.has_next: - raise SandboxException("No more items to fetch") + raise Exception("No more items to fetch") api_client = get_api_client(self._config) res = get_snapshots.sync_detailed( diff --git a/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py b/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py index 6a2ee6783a..ff4c9b33dd 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_snapshot_api.py @@ -109,9 +109,15 @@ async def test_delete_snapshot(async_sandbox: AsyncSandbox): @pytest.mark.skip_debug() async def test_snapshot_preserves_filesystem(async_sandbox: AsyncSandbox): - await async_sandbox.files.make_dir("/home/user/app") - await async_sandbox.files.write("/home/user/app/config.json", '{"env": "test"}') - await async_sandbox.files.write("/home/user/app/data.txt", "important data") + app_dir = "/home/user/app" + config_path = f"{app_dir}/config.json" + config_content = '{"env": "test"}' + data_path = f"{app_dir}/data.txt" + data_content = "important data" + + await async_sandbox.files.make_dir(app_dir) + await async_sandbox.files.write(config_path, config_content) + await async_sandbox.files.write(data_path, data_content) snapshot = await async_sandbox.create_snapshot() @@ -119,14 +125,14 @@ async def test_snapshot_preserves_filesystem(async_sandbox: AsyncSandbox): new_sandbox = await AsyncSandbox.create(snapshot.snapshot_id) try: - dir_exists = await new_sandbox.files.exists("/home/user/app") + dir_exists = await new_sandbox.files.exists(app_dir) assert dir_exists - config = await new_sandbox.files.read("/home/user/app/config.json") - data = await new_sandbox.files.read("/home/user/app/data.txt") + config = await new_sandbox.files.read(config_path) + data = await new_sandbox.files.read(data_path) - assert config == '{"env": "test"}' - assert data == "important data" + assert config == config_content + assert data == data_content finally: await new_sandbox.kill() finally: diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py b/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py index 2f370b61df..97b7b23672 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_snapshot_api.py @@ -107,9 +107,15 @@ def test_delete_snapshot(sandbox: Sandbox): @pytest.mark.skip_debug() def test_snapshot_preserves_filesystem(sandbox: Sandbox): - sandbox.files.make_dir("/home/user/app") - sandbox.files.write("/home/user/app/config.json", '{"env": "test"}') - sandbox.files.write("/home/user/app/data.txt", "important data") + app_dir = "/home/user/app" + config_path = f"{app_dir}/config.json" + config_content = '{"env": "test"}' + data_path = f"{app_dir}/data.txt" + data_content = "important data" + + sandbox.files.make_dir(app_dir) + sandbox.files.write(config_path, config_content) + sandbox.files.write(data_path, data_content) snapshot = sandbox.create_snapshot() @@ -117,14 +123,14 @@ def test_snapshot_preserves_filesystem(sandbox: Sandbox): new_sandbox = Sandbox.create(snapshot.snapshot_id) try: - dir_exists = new_sandbox.files.exists("/home/user/app") + dir_exists = new_sandbox.files.exists(app_dir) assert dir_exists - config = new_sandbox.files.read("/home/user/app/config.json") - data = new_sandbox.files.read("/home/user/app/data.txt") + config = new_sandbox.files.read(config_path) + data = new_sandbox.files.read(data_path) - assert config == '{"env": "test"}' - assert data == "important data" + assert config == config_content + assert data == data_content finally: new_sandbox.kill() finally: