diff --git a/.changeset/khaki-shirts-switch.md b/.changeset/khaki-shirts-switch.md new file mode 100644 index 0000000000..49c5f9b8e6 --- /dev/null +++ b/.changeset/khaki-shirts-switch.md @@ -0,0 +1,6 @@ +--- +'@e2b/python-sdk': patch +'e2b': patch +--- + +added getInfo methods diff --git a/apps/web/src/app/(docs)/docs/sandbox/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/page.mdx index d6599f1c3b..f0aea3e8a1 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/page.mdx @@ -56,6 +56,37 @@ sandbox.set_timeout(30) ``` +## Retrieve sandbox information + +You can retrieve sandbox information like sandbox id, template, metadata, started at/end at date by calling the `getInfo` method in JavaScript or `get_info` method in Python. + + +```js +import { Sandbox } from '@e2b/code-interpreter' + +// Create sandbox with and keep it running for 60 seconds. +const sandbox = await Sandbox.create({ timeoutMs: 60_000 }) + +// Retrieve sandbox information. +const info = await sandbox.getInfo() + +// Retrieve sandbox expiration date. +const expirationDate = info.endAt +console.log(expirationDate) +``` + +```python +from e2b_code_interpreter import Sandbox + +# Create sandbox with and keep it running for 60 seconds. +sandbox = Sandbox(timeout=60) + +# Retrieve sandbox expiration date. +expiration_date = sandbox.get_info().end_at +print(expiration_date) +``` + + ## Shutdown sandbox You can shutdown the sandbox any time even before the timeout is up by calling the `kill` method. diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index ac8249d4f8..c23a447405 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -122,7 +122,7 @@ export class Sandbox extends SandboxApi { logger: opts?.logger, }, { - version: opts?.envdVersion + version: opts?.envdVersion, } ) this.files = new Filesystem( @@ -184,12 +184,15 @@ export class Sandbox extends SandboxApi { const config = new ConnectionConfig(sandboxOpts) if (config.debug) { - return new this({ sandboxId: 'debug_sandbox_id', ...config }) as InstanceType - } else { - const sandbox = await this.createSandbox( - template, - sandboxOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs, - sandboxOpts + return new this({ + sandboxId: 'debug_sandbox_id', + ...config, + }) as InstanceType + } else { + const sandbox = await this.createSandbox( + template, + sandboxOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs, + sandboxOpts ) return new this({ ...sandbox, ...config }) as InstanceType } @@ -356,4 +359,18 @@ export class Sandbox extends SandboxApi { return url.toString() } + + /** + * Get sandbox information like sandbox id, template, metadata, started at/end at date. + * + * @param opts connection options. + * + * @returns information about the sandbox + */ + async getInfo(opts?: Pick) { + return await Sandbox.getInfo(this.sandboxId, { + ...this.connectionConfig, + ...opts, + }) + } } diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index a18429a59a..031aaf59c9 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -15,7 +15,7 @@ export interface SandboxListOpts extends SandboxApiOpts { /** * Filter the list of sandboxes, e.g. by metadata `metadata:{"key": "value"}`, if there are multiple filters they are combined with AND. */ - query?: {metadata?: Record} + query?: { metadata?: Record } } /** @@ -46,6 +46,11 @@ export interface SandboxInfo { * Sandbox start time. */ startedAt: Date + + /** + * Sandbox expiration date. + */ + endAt: Date } export class SandboxApi { @@ -94,23 +99,27 @@ export class SandboxApi { * * @returns list of running sandboxes. */ - static async list( - opts?: SandboxListOpts): Promise { + static async list(opts?: SandboxListOpts): Promise { const config = new ConnectionConfig(opts) const client = new ApiClient(config) let metadata = undefined if (opts?.query) { if (opts.query.metadata) { - const encodedPairs: Record = Object.fromEntries(Object.entries(opts.query.metadata).map(([key, value]) => [encodeURIComponent(key), encodeURIComponent(value)])) + const encodedPairs: Record = Object.fromEntries( + Object.entries(opts.query.metadata).map(([key, value]) => [ + encodeURIComponent(key), + encodeURIComponent(value), + ]) + ) metadata = new URLSearchParams(encodedPairs).toString() } } const res = await client.api.GET('/sandboxes', { - params: { - query: {metadata}, - }, + params: { + query: { metadata }, + }, signal: config.getSignal(opts?.requestTimeoutMs), }) @@ -129,10 +138,57 @@ export class SandboxApi { ...(sandbox.alias && { name: sandbox.alias }), metadata: sandbox.metadata ?? {}, startedAt: new Date(sandbox.startedAt), + endAt: new Date(sandbox.endAt), })) ?? [] ) } + /** + * Get sandbox information like sandbox id, template, metadata, started at/end at date. + * + * @param sandboxId sandbox ID. + * @param opts connection options. + * + * @returns sandbox information. + */ + static async getInfo( + sandboxId: string, + opts?: SandboxApiOpts + ): Promise { + const config = new ConnectionConfig(opts) + const client = new ApiClient(config) + + const res = await client.api.GET('/sandboxes/{sandboxID}', { + params: { + path: { + sandboxID: sandboxId, + }, + }, + signal: config.getSignal(opts?.requestTimeoutMs), + }) + + const err = handleApiError(res) + if (err) { + throw err + } + + if (!res.data) { + throw new Error('Sandbox not found') + } + + return { + sandboxId: this.getSandboxId({ + sandboxId: res.data.sandboxID, + clientId: res.data.clientID, + }), + templateId: res.data.templateID, + ...(res.data.alias && { name: res.data.alias }), + metadata: res.data.metadata ?? {}, + startedAt: new Date(res.data.startedAt), + endAt: new Date(res.data.endAt), + } + } + /** * Set the timeout of the specified sandbox. * After the timeout expires the sandbox will be automatically killed. @@ -219,7 +275,7 @@ export class SandboxApi { sandboxId: res.data!.sandboxID, clientId: res.data!.clientID, }), - envdVersion: res.data!.envdVersion + envdVersion: res.data!.envdVersion, } } diff --git a/packages/js-sdk/tests/sandbox/timeout.test.ts b/packages/js-sdk/tests/sandbox/timeout.test.ts index 6fab68f93d..d38b0b4504 100644 --- a/packages/js-sdk/tests/sandbox/timeout.test.ts +++ b/packages/js-sdk/tests/sandbox/timeout.test.ts @@ -19,5 +19,10 @@ sandboxTest.skipIf(isDebug)('shorten then lenghten timeout', async ({ sandbox }) await wait(6000) - await sandbox.isRunning() + expect(await sandbox.isRunning()).toBeTruthy() }) + +sandboxTest.skipIf(isDebug)('get sandbox timeout', async ({ sandbox }) => { + const { endAt } = await sandbox.getInfo() + expect(endAt).toBeInstanceOf(Date) +}) \ No newline at end of file diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index c746de575f..51175d745f 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -19,6 +19,8 @@ class SandboxInfo: """Saved sandbox metadata.""" started_at: datetime """Sandbox start time.""" + end_at: datetime + """Sandbox expiration date.""" @dataclass diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index cb2a393282..091206bf5b 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -12,7 +12,7 @@ from e2b.sandbox_async.filesystem.filesystem import Filesystem from e2b.sandbox_async.commands.command import Commands from e2b.sandbox_async.commands.pty import Pty -from e2b.sandbox_async.sandbox_api import SandboxApi +from e2b.sandbox_async.sandbox_api import SandboxApi, SandboxInfo logger = logging.getLogger(__name__) @@ -371,3 +371,25 @@ async def set_timeout( # type: ignore timeout=timeout, **self.connection_config.__dict__, ) + + async def get_info( # type: ignore + self, + request_timeout: Optional[float] = None, + ) -> SandboxInfo: + """ + Get sandbox information like sandbox id, template, metadata, started at/end at date. + :param request_timeout: Timeout for the request in **seconds** + :return: Sandbox info + """ + + config_dict = self.connection_config.__dict__ + config_dict.pop("access_token", None) + config_dict.pop("api_url", None) + + if request_timeout: + config_dict["request_timeout"] = request_timeout + + return await SandboxApi.get_info( + sandbox_id=self.sandbox_id, + **self.connection_config.__dict__, + ) \ No newline at end of file diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 9268024767..7e7bc29b5e 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -7,6 +7,7 @@ from e2b.api import AsyncApiClient, SandboxCreateResponse from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody from e2b.api.client.api.sandboxes import ( + get_sandboxes_sandbox_id, post_sandboxes_sandbox_id_timeout, get_sandboxes, delete_sandboxes_sandbox_id, @@ -78,9 +79,61 @@ async def list( sandbox.metadata if isinstance(sandbox.metadata, dict) else {} ), started_at=sandbox.started_at, + end_at=sandbox.end_at, ) for sandbox in res.parsed ] + + @classmethod + async def get_info( + cls, + sandbox_id: str, + api_key: Optional[str] = None, + domain: Optional[str] = None, + debug: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> SandboxInfo: + """ + Get the sandbox info. + :param sandbox_id: Sandbox ID + :param api_key: API key to use for authentication, defaults to `E2B_API_KEY` environment variable + :param domain: Domain to use for the request, defaults to `E2B_DOMAIN` environment variable + :param debug: Debug mode, defaults to `E2B_DEBUG` environment variable + :param request_timeout: Timeout for the request in **seconds** + :return: Sandbox info + """ + config = ConnectionConfig( + api_key=api_key, + domain=domain, + debug=debug, + request_timeout=request_timeout, + ) + + async with AsyncApiClient(config) as api_client: + res = await get_sandboxes_sandbox_id.asyncio_detailed( + sandbox_id, + client=api_client, + ) + + if res.status_code >= 300: + raise handle_api_exception(res) + + if res.parsed is None: + raise Exception("Body of the request is None") + + return SandboxInfo( + sandbox_id=SandboxApi._get_sandbox_id( + res.parsed.sandbox_id, + res.parsed.client_id, + ), + template_id=res.parsed.template_id, + name=res.parsed.alias if isinstance(res.parsed.alias, str) else None, + metadata=( + res.parsed.metadata if isinstance(res.parsed.metadata, dict) else {} + ), + started_at=res.parsed.started_at, + end_at=res.parsed.end_at, + ) @classmethod async def _cls_kill( diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 8922b2dc4d..ebfd49d8f4 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -10,7 +10,7 @@ from e2b.sandbox_sync.filesystem.filesystem import Filesystem from e2b.sandbox_sync.commands.command import Commands from e2b.sandbox_sync.commands.pty import Pty -from e2b.sandbox_sync.sandbox_api import SandboxApi +from e2b.sandbox_sync.sandbox_api import SandboxApi, SandboxInfo logger = logging.getLogger(__name__) @@ -361,3 +361,24 @@ def set_timeout( # type: ignore timeout=timeout, **self.connection_config.__dict__, ) + + def get_info( # type: ignore + self, + request_timeout: Optional[float] = None, + ) -> SandboxInfo: + """ + Get sandbox information like sandbox id, template, metadata, started at/end at date. + :param request_timeout: Timeout for the request in **seconds** + :return: Sandbox info + """ + config_dict = self.connection_config.__dict__ + config_dict.pop("access_token", None) + config_dict.pop("api_url", None) + + if request_timeout: + config_dict["request_timeout"] = request_timeout + + return SandboxApi.get_info( + sandbox_id=self.sandbox_id, + **self.connection_config.__dict__, + ) \ No newline at end of file diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 92460e7526..da4c5920c7 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -9,6 +9,7 @@ from e2b.api import ApiClient, SandboxCreateResponse from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody from e2b.api.client.api.sandboxes import ( + get_sandboxes_sandbox_id, post_sandboxes_sandbox_id_timeout, get_sandboxes, delete_sandboxes_sandbox_id, @@ -79,9 +80,63 @@ def list( sandbox.metadata if isinstance(sandbox.metadata, dict) else {} ), started_at=sandbox.started_at, + end_at=sandbox.end_at, ) for sandbox in res.parsed ] + + @classmethod + def get_info( + cls, + sandbox_id: str, + api_key: Optional[str] = None, + domain: Optional[str] = None, + debug: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> SandboxInfo: + """ + Get the sandbox info. + :param sandbox_id: Sandbox ID + :param api_key: API key to use for authentication, defaults to `E2B_API_KEY` environment variable + :param domain: Domain to use for the request, defaults to `E2B_DOMAIN` environment variable + :param debug: Debug mode, defaults to `E2B_DEBUG` environment variable + :param request_timeout: Timeout for the request in **seconds** + :return: Sandbox info + """ + config = ConnectionConfig( + api_key=api_key, + domain=domain, + debug=debug, + request_timeout=request_timeout, + ) + + with ApiClient( + config, transport=HTTPTransport(limits=SandboxApiBase._limits) + ) as api_client: + res = get_sandboxes_sandbox_id.sync_detailed( + sandbox_id, + client=api_client, + ) + + if res.status_code >= 300: + raise handle_api_exception(res) + + if res.parsed is None: + raise Exception("Body of the request is None") + + return SandboxInfo( + sandbox_id=SandboxApi._get_sandbox_id( + res.parsed.sandbox_id, + res.parsed.client_id, + ), + template_id=res.parsed.template_id, + name=res.parsed.alias if isinstance(res.parsed.alias, str) else None, + metadata=( + res.parsed.metadata if isinstance(res.parsed.metadata, dict) else {} + ), + started_at=res.parsed.started_at, + end_at=res.parsed.end_at, + ) @classmethod def _cls_kill( diff --git a/packages/python-sdk/tests/async/sandbox_async/test_timeout.py b/packages/python-sdk/tests/async/sandbox_async/test_timeout.py index be6715bc4d..4ed1e05a70 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_timeout.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_timeout.py @@ -1,4 +1,5 @@ import pytest +from datetime import datetime from time import sleep @@ -21,3 +22,8 @@ async def test_shorten_then_lengthen_timeout(async_sandbox: AsyncSandbox): await async_sandbox.set_timeout(10) sleep(6) await async_sandbox.is_running() + +@pytest.mark.skip_debug() +async def test_get_timeout(async_sandbox: AsyncSandbox): + info = await async_sandbox.get_info() + assert isinstance(info.end_at, datetime) \ No newline at end of file diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_timeout.py b/packages/python-sdk/tests/sync/sandbox_sync/test_timeout.py index e8015fdfb3..1d7a5d9d12 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_timeout.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_timeout.py @@ -1,4 +1,5 @@ from time import sleep +from datetime import datetime import pytest @@ -19,3 +20,8 @@ def test_shorten_then_lengthen_timeout(sandbox): sandbox.set_timeout(10) sleep(6) sandbox.is_running() + +@pytest.mark.skip_debug() +def test_get_timeout(sandbox): + info = sandbox.get_info() + assert isinstance(info.end_at, datetime) \ No newline at end of file