diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md new file mode 100644 index 0000000000..9050d567f2 --- /dev/null +++ b/.changeset/olive-poets-hammer.md @@ -0,0 +1,6 @@ +--- +'e2b': minor +'@e2b/python-sdk': minor +--- + +Remove SDK-side defaults from API request payloads so the API defaults apply when options are omitted. Sandbox create/fork/connect no longer preset a 5-minute timeout, fork no longer presets `count: 1`, create no longer presets `secure: true` or `allow_internet_access`, pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. Note: until the API-side defaults for `secure` and connect `timeout` are deployed, omitting them changes behavior (sandboxes are created unsecured and connect requests without a timeout are rejected). diff --git a/.changeset/witty-parrots-decide.md b/.changeset/witty-parrots-decide.md new file mode 100644 index 0000000000..8b7793a719 --- /dev/null +++ b/.changeset/witty-parrots-decide.md @@ -0,0 +1,6 @@ +--- +'e2b': patch +'@e2b/python-sdk': patch +--- + +Remove client-side validation of the fork `count` argument. The API validates the requested fork count and rejects invalid values. diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index f22de94e07..26f30a4e74 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -3,7 +3,6 @@ import { createConnectTransport } from '@connectrpc/connect-web' import { ConnectionConfig, ConnectionOpts, - DEFAULT_SANDBOX_TIMEOUT_MS, defaultUsername, Username, } from '../connectionConfig' @@ -76,7 +75,6 @@ export interface SandboxUrlOpts { export class Sandbox extends SandboxApi { protected static readonly defaultTemplate: string = 'base' protected static readonly defaultMcpTemplate: string = 'mcp-gateway' - protected static readonly defaultSandboxTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS /** * Module for interacting with the sandbox filesystem @@ -317,7 +315,7 @@ export class Sandbox extends SandboxApi { const sandboxInfo = await this.createSandbox( template, - apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs, + apiOpts?.timeoutMs, apiOpts ) @@ -433,8 +431,8 @@ export class Sandbox extends SandboxApi { const results = await this.forkSandbox( sandboxId, - apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs, - apiOpts?.count ?? 1, + apiOpts?.timeoutMs, + apiOpts?.count, apiOpts ) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 3af6b1bace..761b7a734d 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -3,7 +3,6 @@ import { ClientFactory, ConnectionConfig, ConnectionOpts, - DEFAULT_SANDBOX_TIMEOUT_MS, } from '../connectionConfig' import { compareVersions } from 'compare-versions' import { ALL_TRAFFIC } from './network' @@ -513,8 +512,6 @@ export interface SandboxPauseOpts extends SandboxApiOpts { * When `false`, the in-memory state is dropped and only the filesystem is * persisted (a filesystem-only snapshot); resuming such a sandbox cold-boots * (reboots) it from disk, losing running processes and open connections. - * - * @default true */ keepMemory?: boolean } @@ -529,16 +526,12 @@ export interface SandboxForkOpts extends ConnectionOpts { * All forks boot from the same snapshot — the snapshot is captured once * regardless of count. Each fork succeeds or fails independently; the * outcome of each is reported in its entry of the returned array. - * - * @default 1 */ count?: number /** * Timeout for the forked sandboxes in **milliseconds**. * Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users. - * - * @default 300_000 // 5 minutes */ timeoutMs?: number } @@ -590,22 +583,16 @@ export interface SandboxOpts extends ConnectionOpts { /** * Timeout for the sandbox in **milliseconds**. * Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users. - * - * @default 300_000 // 5 minutes */ timeoutMs?: number /** * Secure all traffic coming to the sandbox controller with auth token - * - * @default true */ secure?: boolean /** * Allow sandbox to access the internet. If set to `False`, it works the same as setting network `denyOut` to `[0.0.0.0/0]`. - * - * @default true */ allowInternetAccess?: boolean @@ -673,8 +660,6 @@ export type SandboxConnectOpts = ConnectionOpts & { * Timeout for the sandbox in **milliseconds**. * For running sandboxes, the timeout will update only if the new timeout is longer than the existing one. * Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users. - * - * @default 300_000 // 5 minutes */ timeoutMs?: number } @@ -714,8 +699,6 @@ export interface SandboxListOpts extends Omit { /** * Sort order of the list of sandboxes by start time, applied across the * whole result set before pagination (not within a page). - * - * @default 'desc' */ order?: SandboxListOrder @@ -1475,7 +1458,7 @@ export class SandboxApi extends ClientFactory { }, }, body: { - memory: apiOpts?.keepMemory ?? true, + memory: apiOpts?.keepMemory, }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) @@ -1602,7 +1585,7 @@ export class SandboxApi extends ClientFactory { protected static async createSandbox( template: string, - timeoutMs: number, + timeoutMs?: number, opts?: SandboxOpts ) { const apiOpts = this.resolveOpts(opts) @@ -1656,9 +1639,10 @@ export class SandboxApi extends ClientFactory { metadata: opts?.metadata, mcp: opts?.mcp as Record | undefined, envVars: opts?.envs, - timeout: timeoutToSeconds(timeoutMs), - secure: opts?.secure ?? true, - allow_internet_access: opts?.allowInternetAccess ?? true, + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), + secure: opts?.secure, + allow_internet_access: opts?.allowInternetAccess, network: buildNetworkBody(opts?.network, iam), iam, autoPause: onTimeoutConfigured ? action === 'pause' : undefined, @@ -1705,14 +1689,10 @@ export class SandboxApi extends ClientFactory { protected static async forkSandbox( sandboxId: string, - timeoutMs: number, - count: number, + timeoutMs?: number, + count?: number, opts?: SandboxApiOpts ): Promise { - if (count < 1) { - throw new InvalidArgumentError('count must be at least 1') - } - const apiOpts = this.resolveOpts(opts) const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) @@ -1724,7 +1704,8 @@ export class SandboxApi extends ClientFactory { }, }, body: { - timeout: timeoutToSeconds(timeoutMs), + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), count, }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), @@ -1777,7 +1758,7 @@ export class SandboxApi extends ClientFactory { opts?: SandboxConnectOpts ) { const apiOpts = this.resolveOpts(opts) - const timeoutMs = apiOpts?.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS + const timeoutMs = apiOpts?.timeoutMs const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) @@ -1788,9 +1769,11 @@ export class SandboxApi extends ClientFactory { sandboxID: sandboxId, }, }, + // TODO: drop the cast once the API spec makes `timeout` optional body: { - timeout: timeoutToSeconds(timeoutMs), - }, + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), + } as components['schemas']['ConnectSandbox'], signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) diff --git a/packages/js-sdk/src/template/buildApi.ts b/packages/js-sdk/src/template/buildApi.ts index f79321166e..11153db5aa 100644 --- a/packages/js-sdk/src/template/buildApi.ts +++ b/packages/js-sdk/src/template/buildApi.ts @@ -19,8 +19,8 @@ import { type RequestBuildInput = { name: string tags?: string[] - cpuCount: number - memoryMB: number + cpuCount?: number + memoryMB?: number } type GetFileUploadLinkInput = { diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 1b712c18ae..b8611a3f03 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -1076,8 +1076,8 @@ export class TemplateBase { name, tags: options.tags, - cpuCount: options.cpuCount ?? 2, - memoryMB: options.memoryMB ?? 1024, + cpuCount: options.cpuCount, + memoryMB: options.memoryMB, }, config.getSignal(undefined, options.signal) ) diff --git a/packages/js-sdk/src/template/types.ts b/packages/js-sdk/src/template/types.ts index 87ff44441d..e7b7c70efd 100644 --- a/packages/js-sdk/src/template/types.ts +++ b/packages/js-sdk/src/template/types.ts @@ -34,12 +34,10 @@ export type BasicBuildOptions = { tags?: string[] /** * Number of CPUs allocated to the sandbox. - * @default 2 */ cpuCount?: number /** * Amount of memory in MB allocated to the sandbox. - * @default 1024 */ memoryMB?: number /** diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts new file mode 100644 index 0000000000..0783744318 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -0,0 +1,131 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +import { Sandbox } from '../../src' +import { TEST_API_KEY, apiUrl } from '../setup' + +let lastCreateBody: Record | undefined +let lastForkBody: Record | undefined +let lastPauseBody: Record | undefined +let lastConnectBody: Record | undefined + +const server = setupServer( + http.post(apiUrl('/sandboxes'), async ({ request }) => { + lastCreateBody = (await request.json()) as Record + return HttpResponse.json({ + sandboxID: 'test-sandbox-id', + templateID: 'base', + envdVersion: '0.2.4', + }) + }), + http.post(apiUrl('/sandboxes/:sandboxID/connect'), async ({ request }) => { + lastConnectBody = (await request.json()) as Record + return HttpResponse.json({ + sandboxID: 'test-sandbox-id', + templateID: 'base', + envdVersion: '0.2.4', + }) + }), + http.post(apiUrl('/sandboxes/:sandboxID/fork'), async ({ request }) => { + lastForkBody = (await request.json()) as Record + return HttpResponse.json([ + { + sandbox: { + sandboxID: 'forked-sandbox-id', + templateID: 'base', + envdVersion: '0.2.4', + }, + }, + ]) + }), + http.post(apiUrl('/sandboxes/:sandboxID/pause'), async ({ request }) => { + lastPauseBody = (await request.json()) as Record + return new HttpResponse(null, { status: 204 }) + }) +) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +afterAll(() => server.close()) + +afterEach(() => { + lastCreateBody = undefined + lastForkBody = undefined + lastPauseBody = undefined + lastConnectBody = undefined + server.resetHandlers() +}) + +test('Sandbox.create omits timeout, secure and allow_internet_access when unset', async () => { + await Sandbox.create('base', { apiKey: TEST_API_KEY }) + + expect(lastCreateBody).toBeDefined() + expect(lastCreateBody).not.toHaveProperty('timeout') + expect(lastCreateBody).not.toHaveProperty('secure') + expect(lastCreateBody).not.toHaveProperty('allow_internet_access') +}) + +test('Sandbox.create sends explicit timeout, secure and allow_internet_access', async () => { + await Sandbox.create('base', { + apiKey: TEST_API_KEY, + timeoutMs: 60_000, + secure: false, + allowInternetAccess: false, + }) + + expect(lastCreateBody?.timeout).toBe(60) + expect(lastCreateBody?.secure).toBe(false) + expect(lastCreateBody?.allow_internet_access).toBe(false) +}) + +test('Sandbox.fork omits timeout and count when unset', async () => { + await Sandbox.fork('test-sandbox-id', { apiKey: TEST_API_KEY }) + + expect(lastForkBody).toBeDefined() + expect(lastForkBody).not.toHaveProperty('timeout') + expect(lastForkBody).not.toHaveProperty('count') +}) + +test('Sandbox.fork sends explicit timeout and count', async () => { + await Sandbox.fork('test-sandbox-id', { + apiKey: TEST_API_KEY, + timeoutMs: 60_000, + count: 2, + }) + + expect(lastForkBody?.timeout).toBe(60) + expect(lastForkBody?.count).toBe(2) +}) + +test('Sandbox.pause omits memory when keepMemory is unset', async () => { + await Sandbox.pause('test-sandbox-id', { apiKey: TEST_API_KEY }) + + expect(lastPauseBody).toBeDefined() + expect(lastPauseBody).not.toHaveProperty('memory') +}) + +test('Sandbox.pause sends an explicit keepMemory', async () => { + await Sandbox.pause('test-sandbox-id', { + apiKey: TEST_API_KEY, + keepMemory: false, + }) + + expect(lastPauseBody?.memory).toBe(false) +}) + +test('Sandbox.connect omits timeout when unset', async () => { + await Sandbox.connect('test-sandbox-id', { apiKey: TEST_API_KEY }) + + expect(lastConnectBody).toBeDefined() + expect(lastConnectBody).not.toHaveProperty('timeout') +}) + +test('Sandbox.connect sends an explicit timeout', async () => { + await Sandbox.connect('test-sandbox-id', { + apiKey: TEST_API_KEY, + timeoutMs: 60_000, + }) + + expect(lastConnectBody?.timeout).toBe(60) +}) diff --git a/packages/js-sdk/tests/sandbox/fork.test.ts b/packages/js-sdk/tests/sandbox/fork.test.ts index da3d30b9a6..20e8a227a7 100644 --- a/packages/js-sdk/tests/sandbox/fork.test.ts +++ b/packages/js-sdk/tests/sandbox/fork.test.ts @@ -1,8 +1,8 @@ import { assert, expect, test } from 'vitest' -import { sandboxTest, isDebug, TEST_API_KEY } from '../setup.js' +import { sandboxTest, isDebug } from '../setup.js' import { Sandbox } from '../../src' -import { InvalidArgumentError, SandboxNotFoundError } from '../../src/errors' +import { SandboxNotFoundError } from '../../src/errors' sandboxTest.skipIf(isDebug)('fork a sandbox', async ({ sandbox }) => { await sandbox.files.write('/home/user/state.txt', 'state before fork') @@ -86,9 +86,3 @@ test.skipIf(isDebug)('fork a killed sandbox fails', async () => { await expect(sandbox.fork()).rejects.toThrowError(SandboxNotFoundError) }) - -test('fork with count lower than 1 fails', async () => { - await expect( - Sandbox.fork('sbx-test', { count: 0, apiKey: TEST_API_KEY }) - ).rejects.toThrowError(InvalidArgumentError) -}) diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 91d427c3cc..90a1774788 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -82,6 +82,7 @@ export const sandboxTest = base.extend({ async ({ sandboxTestId, sandboxOpts }, use) => { const sandbox = await Sandbox.create(template, { metadata: { sandboxTestId }, + timeoutMs: 300_000, ...sandboxOpts, }) onTestFailed(() => { diff --git a/packages/js-sdk/tests/template/apiDefaults.test.ts b/packages/js-sdk/tests/template/apiDefaults.test.ts new file mode 100644 index 0000000000..0c1fcbb7f7 --- /dev/null +++ b/packages/js-sdk/tests/template/apiDefaults.test.ts @@ -0,0 +1,52 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +import { ApiClient } from '../../src/api' +import { ConnectionConfig } from '../../src/connectionConfig' +import { requestBuild } from '../../src/template/buildApi' +import { TEST_API_KEY, apiUrl } from '../setup' + +let lastBuildBody: Record | undefined + +const server = setupServer( + http.post(apiUrl('/v3/templates'), async ({ request }) => { + lastBuildBody = (await request.json()) as Record + return HttpResponse.json({ + templateID: 'test-template-id', + buildID: 'test-build-id', + }) + }) +) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +afterAll(() => server.close()) + +afterEach(() => { + lastBuildBody = undefined + server.resetHandlers() +}) + +function client() { + return new ApiClient(new ConnectionConfig({ apiKey: TEST_API_KEY })) +} + +test('template build request omits cpuCount and memoryMB when unset', async () => { + await requestBuild(client(), { name: 'test-template' }) + + expect(lastBuildBody).toBeDefined() + expect(lastBuildBody).not.toHaveProperty('cpuCount') + expect(lastBuildBody).not.toHaveProperty('memoryMB') +}) + +test('template build request sends explicit cpuCount and memoryMB', async () => { + await requestBuild(client(), { + name: 'test-template', + cpuCount: 1, + memoryMB: 512, + }) + + expect(lastBuildBody?.cpuCount).toBe(1) + expect(lastBuildBody?.memoryMB).toBe(512) +}) diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index d53c84915a..9b80d05163 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -18,6 +18,7 @@ from typing_extensions import NotRequired, Unpack from e2b.api.client.models import ( + ConnectSandbox, ListedSandbox, SandboxDetail, SandboxState, @@ -73,6 +74,21 @@ from e2b.paginator import PaginatorBase +class ConnectSandboxBody(ConnectSandbox): + """Connect request body that omits `timeout` when not provided so the + API default applies. The generated model still requires `timeout`; + remove this once the spec makes it optional.""" + + def __init__(self, timeout: Optional[int] = None): + super().__init__(timeout=cast(int, timeout)) + + def to_dict(self) -> Dict[str, Any]: + result = super().to_dict() + if result["timeout"] is None: + del result["timeout"] + return result + + class GitHubMcpServerConfig(TypedDict): """ Configuration for a GitHub-based MCP server. diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 3196008757..ed578f746e 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -172,8 +172,8 @@ async def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: bool = True, - allow_internet_access: bool = True, + secure: Optional[bool] = None, + allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -188,11 +188,11 @@ async def create( By default, the sandbox is created from the default `base` sandbox template. :param template: Sandbox template name or ID - :param timeout: Timeout for the sandbox in **seconds**, default to 300 seconds. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. + :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it, defaults to `True`. - :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. + :param secure: Envd is secured with access token and cannot be used without it. + :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}``. Registered tokens are exposed to ``network.rules`` ``transform`` callables as ``ctx.iam.tokens[name]`` placeholders, which the egress proxy resolves per request @@ -377,8 +377,8 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -416,8 +416,8 @@ async def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -453,8 +453,8 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -749,13 +749,13 @@ async def get_metrics( @overload async def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -765,14 +765,14 @@ async def pause( @staticmethod async def pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox specified by sandbox ID. :param sandbox_id: Sandbox ID - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -781,13 +781,13 @@ async def pause( @class_method_variant("_cls_pause") async def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. Defaults to `True` (full memory snapshot). + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -801,7 +801,7 @@ async def pause( @overload async def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @@ -809,14 +809,14 @@ async def beta_pause( @staticmethod async def beta_pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @class_method_variant("_cls_pause") async def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ @@ -1110,8 +1110,8 @@ async def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: bool, - allow_internet_access: bool, + secure: Optional[bool], + allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -1133,7 +1133,7 @@ async def _create( else: response = await SandboxApi._create_sandbox( template=template or cls.default_template, - timeout=timeout or cls.default_sandbox_timeout, + timeout=timeout, metadata=metadata, env_vars=envs, secure=secure, diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index b6a208d6a8..8866dafffe 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -25,7 +25,6 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( - ConnectSandbox, Error, NewSandbox, SandboxSnapshotRequest, @@ -39,7 +38,6 @@ from e2b.api.client_async import get_api_client from e2b.connection_config import ApiParams, ConnectionConfig from e2b.exceptions import ( - InvalidArgumentException, NotFoundException, SandboxException, SandboxNotFoundException, @@ -48,6 +46,7 @@ from e2b.sandbox.main import SandboxBase from e2b.sandbox.sandbox_api import ( build_network_update_body, + ConnectSandboxBody, McpServer, SandboxIamOpts, SandboxInfo, @@ -84,7 +83,7 @@ def list( :param query: Filter the list of sandboxes by metadata, state, start time, or template, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])` :param limit: Maximum number of sandboxes to return per page :param next_token: Token for pagination - :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), defaults to `"desc"` (newest first) + :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page) :return: An `AsyncSandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `await paginator.next_items()` while `paginator.has_next` is True. """ @@ -208,11 +207,11 @@ async def _cls_update_network( async def _create_sandbox( cls, template: str, - timeout: int, - allow_internet_access: bool, + timeout: Optional[int], + allow_internet_access: Optional[bool], metadata: Optional[Dict[str, str]], env_vars: Optional[Dict[str, str]], - secure: bool, + secure: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -236,11 +235,13 @@ async def _create_sandbox( auto_pause_memory=lifecycle_body.auto_pause_memory, auto_resume=lifecycle_body.auto_resume, metadata=metadata or {}, - timeout=timeout, + timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure, - allow_internet_access=allow_internet_access, + secure=secure if secure is not None else UNSET, + allow_internet_access=( + allow_internet_access if allow_internet_access is not None else UNSET + ), network=SandboxNetworkConfig(**network_body) if network_body else UNSET, iam=iam_body or UNSET, volume_mounts=volume_mounts if volume_mounts else UNSET, @@ -405,7 +406,7 @@ async def _cls_delete_snapshot( async def _cls_pause( cls, sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: config = ConnectionConfig(**cls._resolve_api_params(**opts)) @@ -414,7 +415,9 @@ async def _cls_pause( res = await post_sandboxes_sandbox_id_pause.asyncio_detailed( sandbox_id, client=api_client, - body=SandboxPauseRequest(memory=keep_memory), + body=SandboxPauseRequest( + memory=keep_memory if keep_memory is not None else UNSET + ), ) if res.status_code == 404: @@ -442,21 +445,16 @@ async def _cls_fork( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> List[Union[SandboxCreateResponse, Exception]]: - timeout = ( - timeout if timeout is not None else SandboxBase.default_sandbox_timeout - ) - count = count if count is not None else 1 - - if count < 1: - raise InvalidArgumentException("count must be at least 1") - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) res = await post_sandboxes_sandbox_id_fork.asyncio_detailed( sandbox_id, client=api_client, - body=SandboxForkRequest(timeout=timeout, count=count), + body=SandboxForkRequest( + timeout=timeout if timeout is not None else UNSET, + count=count if count is not None else UNSET, + ), ) if res.status_code == 404: @@ -528,8 +526,6 @@ async def _cls_connect( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> SandboxCreateResponse: - timeout = timeout or SandboxBase.default_sandbox_timeout - # Sandbox is not running, resume it config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) @@ -537,7 +533,7 @@ async def _cls_connect( res = await post_sandboxes_sandbox_id_connect.asyncio_detailed( sandbox_id, client=api_client, - body=ConnectSandbox(timeout=timeout), + body=ConnectSandboxBody(timeout=timeout), ) if res.status_code == 404: diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 2ebda95aab..31567e9909 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -168,8 +168,8 @@ def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: bool = True, - allow_internet_access: bool = True, + secure: Optional[bool] = None, + allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -184,11 +184,11 @@ def create( By default, the sandbox is created from the default `base` sandbox template. :param template: Sandbox template name or ID - :param timeout: Timeout for the sandbox in **seconds**, default to 300 seconds. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. + :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it, defaults to `True`. - :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. + :param secure: Envd is secured with access token and cannot be used without it. + :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}``. Registered tokens are exposed to ``network.rules`` ``transform`` callables as ``ctx.iam.tokens[name]`` placeholders, which the egress proxy resolves per request @@ -372,8 +372,8 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -411,8 +411,8 @@ def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -448,8 +448,8 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -747,13 +747,13 @@ def get_metrics( @overload def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -763,14 +763,14 @@ def pause( @staticmethod def pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox specified by sandbox ID. :param sandbox_id: Sandbox ID - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -779,13 +779,13 @@ def pause( @class_method_variant("_cls_pause") def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. Defaults to `True` (full memory snapshot). + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -799,7 +799,7 @@ def pause( @overload def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @@ -807,14 +807,14 @@ def beta_pause( @staticmethod def beta_pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @class_method_variant("_cls_pause") def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ @@ -1106,8 +1106,8 @@ def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: bool, - allow_internet_access: bool, + secure: Optional[bool], + allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -1129,7 +1129,7 @@ def _create( else: response = SandboxApi._create_sandbox( template=template or cls.default_template, - timeout=timeout or cls.default_sandbox_timeout, + timeout=timeout, metadata=metadata, env_vars=envs, secure=secure, diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index ff8dc4a3e8..0f9806d878 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -25,7 +25,6 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( - ConnectSandbox, Error, NewSandbox, SandboxSnapshotRequest, @@ -38,7 +37,6 @@ from e2b.api.client.types import UNSET, Unset from e2b.connection_config import ApiParams, ConnectionConfig from e2b.exceptions import ( - InvalidArgumentException, NotFoundException, SandboxException, SandboxNotFoundException, @@ -47,6 +45,7 @@ from e2b.sandbox.main import SandboxBase from e2b.sandbox.sandbox_api import ( build_network_update_body, + ConnectSandboxBody, McpServer, SandboxIamOpts, SandboxInfo, @@ -83,7 +82,7 @@ def list( :param query: Filter the list of sandboxes by metadata, state, start time, or template, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])` :param limit: Maximum number of sandboxes to return per page :param next_token: Token for pagination - :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), defaults to `"desc"` (newest first) + :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page) :return: A `SandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `paginator.next_items()` while `paginator.has_next` is True. """ @@ -207,11 +206,11 @@ def _cls_update_network( def _create_sandbox( cls, template: str, - timeout: int, - allow_internet_access: bool, + timeout: Optional[int], + allow_internet_access: Optional[bool], metadata: Optional[Dict[str, str]], env_vars: Optional[Dict[str, str]], - secure: bool, + secure: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -235,11 +234,13 @@ def _create_sandbox( auto_pause_memory=lifecycle_body.auto_pause_memory, auto_resume=lifecycle_body.auto_resume, metadata=metadata or {}, - timeout=timeout, + timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure, - allow_internet_access=allow_internet_access, + secure=secure if secure is not None else UNSET, + allow_internet_access=( + allow_internet_access if allow_internet_access is not None else UNSET + ), network=SandboxNetworkConfig(**network_body) if network_body else UNSET, iam=iam_body or UNSET, volume_mounts=volume_mounts if volume_mounts else UNSET, @@ -343,15 +344,13 @@ def _cls_connect( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> SandboxCreateResponse: - timeout = timeout or SandboxBase.default_sandbox_timeout - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) res = post_sandboxes_sandbox_id_connect.sync_detailed( sandbox_id, client=api_client, - body=ConnectSandbox(timeout=timeout), + body=ConnectSandboxBody(timeout=timeout), ) if res.status_code == 404: @@ -395,21 +394,16 @@ def _cls_fork( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> List[Union[SandboxCreateResponse, Exception]]: - timeout = ( - timeout if timeout is not None else SandboxBase.default_sandbox_timeout - ) - count = count if count is not None else 1 - - if count < 1: - raise InvalidArgumentException("count must be at least 1") - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) res = post_sandboxes_sandbox_id_fork.sync_detailed( sandbox_id, client=api_client, - body=SandboxForkRequest(timeout=timeout, count=count), + body=SandboxForkRequest( + timeout=timeout if timeout is not None else UNSET, + count=count if count is not None else UNSET, + ), ) if res.status_code == 404: @@ -532,7 +526,7 @@ def _cls_delete_snapshot( def _cls_pause( cls, sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: config = ConnectionConfig(**cls._resolve_api_params(**opts)) @@ -541,7 +535,9 @@ def _cls_pause( res = post_sandboxes_sandbox_id_pause.sync_detailed( sandbox_id, client=api_client, - body=SandboxPauseRequest(memory=keep_memory), + body=SandboxPauseRequest( + memory=keep_memory if keep_memory is not None else UNSET + ), ) if res.status_code == 404: diff --git a/packages/python-sdk/e2b/template_async/build_api.py b/packages/python-sdk/e2b/template_async/build_api.py index 9a822cd516..b51494a093 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -49,16 +49,16 @@ async def request_build( client: AuthenticatedClient, name: str, tags: Optional[List[str]], - cpu_count: int, - memory_mb: int, + cpu_count: Optional[int], + memory_mb: Optional[int], ): res = await post_v3_templates.asyncio_detailed( client=client, body=TemplateBuildRequestV3( name=name, tags=tags if tags else UNSET, - cpu_count=cpu_count, - memory_mb=memory_mb, + cpu_count=cpu_count if cpu_count is not None else UNSET, + memory_mb=memory_mb if memory_mb is not None else UNSET, ), ) diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index 250d8aea32..531cb0fbcb 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -37,8 +37,8 @@ async def _build( template: TemplateClass, name: str, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, request_timeout: Optional[float] = None, @@ -50,8 +50,8 @@ async def _build( :param template: The template to build :param name: Name for the template :param tags: Optional tags for the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process """ @@ -196,8 +196,8 @@ async def build( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -209,8 +209,8 @@ async def build( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process @@ -299,8 +299,8 @@ async def build_in_background( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -312,8 +312,8 @@ async def build_in_background( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID diff --git a/packages/python-sdk/e2b/template_sync/build_api.py b/packages/python-sdk/e2b/template_sync/build_api.py index 2bc918ab2f..0fcd4ddc67 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -47,16 +47,16 @@ def request_build( client: AuthenticatedClient, name: str, tags: Optional[List[str]], - cpu_count: int, - memory_mb: int, + cpu_count: Optional[int], + memory_mb: Optional[int], ): res = post_v3_templates.sync_detailed( client=client, body=TemplateBuildRequestV3( name=name, tags=tags if tags else UNSET, - cpu_count=cpu_count, - memory_mb=memory_mb, + cpu_count=cpu_count if cpu_count is not None else UNSET, + memory_mb=memory_mb if memory_mb is not None else UNSET, ), ) diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index 9d397b41b4..af302fbd0c 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -37,8 +37,8 @@ def _build( template: TemplateClass, name: str, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, request_timeout: Optional[float] = None, @@ -50,8 +50,8 @@ def _build( :param template: The template to build :param name: Name for the template :param tags: Optional tags for the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process """ @@ -196,8 +196,8 @@ def build( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -209,8 +209,8 @@ def build( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process @@ -300,8 +300,8 @@ def build_in_background( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -313,8 +313,8 @@ def build_in_background( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID diff --git a/packages/python-sdk/tests/async/sandbox_async/test_fork.py b/packages/python-sdk/tests/async/sandbox_async/test_fork.py index 19f8c7ef23..ab15402dc7 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_fork.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_fork.py @@ -1,7 +1,7 @@ import pytest from e2b import AsyncSandbox -from e2b.exceptions import InvalidArgumentException, SandboxNotFoundException +from e2b.exceptions import SandboxNotFoundException @pytest.mark.skip_debug() @@ -76,8 +76,3 @@ async def test_fork_killed_sandbox(async_sandbox_factory): with pytest.raises(SandboxNotFoundException): await sandbox.fork() - - -async def test_fork_invalid_count(): - with pytest.raises(InvalidArgumentException): - await AsyncSandbox.fork("sbx-test", count=0) diff --git a/packages/python-sdk/tests/conftest.py b/packages/python-sdk/tests/conftest.py index fa8f4a0949..9847f460e7 100644 --- a/packages/python-sdk/tests/conftest.py +++ b/packages/python-sdk/tests/conftest.py @@ -72,6 +72,7 @@ def sandbox_factory(request, template, sandbox_test_id): def factory(*, template_name: str = template, **kwargs): metadata = kwargs.setdefault("metadata", dict()) metadata.setdefault("sandbox_test_id", sandbox_test_id) + kwargs.setdefault("timeout", 300) sandbox = Sandbox.create(template_name, **kwargs) @@ -99,6 +100,7 @@ async def async_sandbox_factory(request, template, sandbox_test_id): async def factory(*, template_name: str = template, **kwargs): metadata = kwargs.setdefault("metadata", dict()) metadata.setdefault("sandbox_test_id", sandbox_test_id) + kwargs.setdefault("timeout", 300) sandbox = await AsyncSandbox.create(template_name, **kwargs) sandboxes.append(sandbox) diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py new file mode 100644 index 0000000000..785f8d6b4b --- /dev/null +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -0,0 +1,220 @@ +from types import SimpleNamespace +from typing import Any, Dict +from unittest.mock import AsyncMock, Mock + +from e2b import AsyncSandbox, Sandbox +from e2b.api.client.api.sandboxes import ( + post_sandboxes, + post_sandboxes_sandbox_id_connect, + post_sandboxes_sandbox_id_fork, + post_sandboxes_sandbox_id_pause, +) +from e2b.api.client.models import Sandbox as SandboxModel + + +def _created_sandbox(): + return SimpleNamespace( + status_code=200, + parsed=SandboxModel( + client_id="client-id", + envd_version="0.2.4", + sandbox_id="sbx-test", + template_id="template-id", + ), + ) + + +def _sync_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes, "sync_detailed", request) + + Sandbox.create(api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes, "asyncio_detailed", request) + + await AsyncSandbox.create(api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_create_omits_api_owned_fields_when_unset(monkeypatch, test_api_key): + body = _sync_create_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "secure" not in body + assert "allow_internet_access" not in body + + +def test_create_sends_explicit_values(monkeypatch, test_api_key): + body = _sync_create_body( + monkeypatch, + test_api_key, + timeout=60, + secure=False, + allow_internet_access=False, + ) + + assert body["timeout"] == 60 + assert body["secure"] is False + assert body["allow_internet_access"] is False + + +async def test_async_create_omits_api_owned_fields_when_unset( + monkeypatch, test_api_key +): + body = await _async_create_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "secure" not in body + assert "allow_internet_access" not in body + + +async def test_async_create_sends_explicit_values(monkeypatch, test_api_key): + body = await _async_create_body( + monkeypatch, + test_api_key, + timeout=60, + secure=False, + allow_internet_access=False, + ) + + assert body["timeout"] == 60 + assert body["secure"] is False + assert body["allow_internet_access"] is False + + +def _sync_fork_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=SimpleNamespace(status_code=200, parsed=[])) + monkeypatch.setattr(post_sandboxes_sandbox_id_fork, "sync_detailed", request) + + Sandbox.fork("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_fork_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=SimpleNamespace(status_code=200, parsed=[])) + monkeypatch.setattr(post_sandboxes_sandbox_id_fork, "asyncio_detailed", request) + + await AsyncSandbox.fork("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_fork_omits_timeout_and_count_when_unset(monkeypatch, test_api_key): + body = _sync_fork_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "count" not in body + + +def test_fork_sends_explicit_timeout_and_count(monkeypatch, test_api_key): + body = _sync_fork_body(monkeypatch, test_api_key, timeout=60, count=2) + + assert body["timeout"] == 60 + assert body["count"] == 2 + + +async def test_async_fork_omits_timeout_and_count_when_unset(monkeypatch, test_api_key): + body = await _async_fork_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "count" not in body + + +async def test_async_fork_sends_explicit_timeout_and_count(monkeypatch, test_api_key): + body = await _async_fork_body(monkeypatch, test_api_key, timeout=60, count=2) + + assert body["timeout"] == 60 + assert body["count"] == 2 + + +def _sync_pause_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=SimpleNamespace(status_code=204, parsed=None)) + monkeypatch.setattr(post_sandboxes_sandbox_id_pause, "sync_detailed", request) + + Sandbox.pause("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_pause_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=SimpleNamespace(status_code=204, parsed=None)) + monkeypatch.setattr(post_sandboxes_sandbox_id_pause, "asyncio_detailed", request) + + await AsyncSandbox.pause("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_pause_omits_memory_when_keep_memory_unset(monkeypatch, test_api_key): + body = _sync_pause_body(monkeypatch, test_api_key) + + assert "memory" not in body + + +def test_pause_sends_explicit_keep_memory(monkeypatch, test_api_key): + body = _sync_pause_body(monkeypatch, test_api_key, keep_memory=False) + + assert body["memory"] is False + + +async def test_async_pause_omits_memory_when_keep_memory_unset( + monkeypatch, test_api_key +): + body = await _async_pause_body(monkeypatch, test_api_key) + + assert "memory" not in body + + +async def test_async_pause_sends_explicit_keep_memory(monkeypatch, test_api_key): + body = await _async_pause_body(monkeypatch, test_api_key, keep_memory=False) + + assert body["memory"] is False + + +def _sync_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "sync_detailed", request) + + Sandbox.connect("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "asyncio_detailed", request) + + await AsyncSandbox.connect("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_connect_omits_timeout_when_unset(monkeypatch, test_api_key): + body = _sync_connect_body(monkeypatch, test_api_key) + + assert "timeout" not in body + + +def test_connect_sends_explicit_timeout(monkeypatch, test_api_key): + body = _sync_connect_body(monkeypatch, test_api_key, timeout=60) + + assert body["timeout"] == 60 + + +async def test_async_connect_omits_timeout_when_unset(monkeypatch, test_api_key): + body = await _async_connect_body(monkeypatch, test_api_key) + + assert "timeout" not in body + + +async def test_async_connect_sends_explicit_timeout(monkeypatch, test_api_key): + body = await _async_connect_body(monkeypatch, test_api_key, timeout=60) + + assert body["timeout"] == 60 diff --git a/packages/python-sdk/tests/shared/template/test_api_defaults.py b/packages/python-sdk/tests/shared/template/test_api_defaults.py new file mode 100644 index 0000000000..da061fbf90 --- /dev/null +++ b/packages/python-sdk/tests/shared/template/test_api_defaults.py @@ -0,0 +1,68 @@ +from types import SimpleNamespace +from typing import Any, Dict +from unittest.mock import AsyncMock, Mock + +from e2b.api.client.api.templates import post_v3_templates +from e2b.api.client.models import TemplateRequestResponseV3 +from e2b.template_async.build_api import request_build as async_request_build +from e2b.template_sync.build_api import request_build as sync_request_build + + +def _build_response(): + return SimpleNamespace( + status_code=200, + parsed=TemplateRequestResponseV3( + template_id="template-id", + build_id="build-id", + public=False, + names=[], + tags=[], + aliases=[], + ), + ) + + +def _sync_build_body(monkeypatch, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_build_response()) + monkeypatch.setattr(post_v3_templates, "sync_detailed", request) + + sync_request_build(Mock(), name="test-template", tags=None, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_build_body(monkeypatch, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=_build_response()) + monkeypatch.setattr(post_v3_templates, "asyncio_detailed", request) + + await async_request_build(Mock(), name="test-template", tags=None, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_build_omits_cpu_and_memory_when_unset(monkeypatch): + body = _sync_build_body(monkeypatch, cpu_count=None, memory_mb=None) + + assert "cpuCount" not in body + assert "memoryMB" not in body + + +def test_build_sends_explicit_cpu_and_memory(monkeypatch): + body = _sync_build_body(monkeypatch, cpu_count=1, memory_mb=512) + + assert body["cpuCount"] == 1 + assert body["memoryMB"] == 512 + + +async def test_async_build_omits_cpu_and_memory_when_unset(monkeypatch): + body = await _async_build_body(monkeypatch, cpu_count=None, memory_mb=None) + + assert "cpuCount" not in body + assert "memoryMB" not in body + + +async def test_async_build_sends_explicit_cpu_and_memory(monkeypatch): + body = await _async_build_body(monkeypatch, cpu_count=1, memory_mb=512) + + assert body["cpuCount"] == 1 + assert body["memoryMB"] == 512 diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py b/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py index 7159a53102..903431591b 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py @@ -1,7 +1,7 @@ import pytest from e2b import Sandbox -from e2b.exceptions import InvalidArgumentException, SandboxNotFoundException +from e2b.exceptions import SandboxNotFoundException @pytest.mark.skip_debug() @@ -73,8 +73,3 @@ def test_fork_killed_sandbox(sandbox_factory): with pytest.raises(SandboxNotFoundException): sandbox.fork() - - -def test_fork_invalid_count(): - with pytest.raises(InvalidArgumentException): - Sandbox.fork("sbx-test", count=0)