From f4c4bbde6a84d0f5fa485d17e3ac720a9975fef8 Mon Sep 17 00:00:00 2001 From: 0div Date: Mon, 23 Dec 2024 17:38:04 +0100 Subject: [PATCH 01/39] created boilerplate for integration tests within js-sdk --- packages/js-sdk/package.json | 3 +- .../js-sdk/tests/integration/stress.test.ts | 73 +++++++++++++++++++ .../tests/integration/template/README.md | 5 ++ .../tests/integration/template/e2b.Dockerfile | 8 ++ .../tests/integration/template/e2b.toml | 18 +++++ packages/js-sdk/tests/setup.ts | 3 +- packages/js-sdk/vitest.workspace.mts | 35 +++++---- 7 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 packages/js-sdk/tests/integration/stress.test.ts create mode 100644 packages/js-sdk/tests/integration/template/README.md create mode 100644 packages/js-sdk/tests/integration/template/e2b.Dockerfile create mode 100644 packages/js-sdk/tests/integration/template/e2b.toml diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index d57b08e15..ad2fdc50e 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -35,7 +35,8 @@ "update-deps": "ncu -u && pnpm i", "postPublish": "./scripts/post-publish.sh || true", "test:bun": "bun test tests/runtimes/bun --env-file=.env", - "test:deno": "deno test tests/runtimes/deno/ --allow-net --allow-read --allow-env --unstable-sloppy-imports --trace-leaks" + "test:deno": "deno test tests/runtimes/deno/ --allow-net --allow-read --allow-env --unstable-sloppy-imports --trace-leaks", + "test:integration": "E2B_INTEGRATION_TEST=1 vitest run tests/integration/**" }, "devDependencies": { "@testing-library/react": "^16.0.1", diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts new file mode 100644 index 000000000..b82830372 --- /dev/null +++ b/packages/js-sdk/tests/integration/stress.test.ts @@ -0,0 +1,73 @@ +import { test } from 'vitest' + +import Sandbox from '../../src/index.js' +import { wait, isIntegrationTest } from '../setup.js' + +const heavyArray = new ArrayBuffer(256 * 1024 * 1024) // 256 MiB = 256 * 1024 * 1024 bytes +const view = new Uint8Array(heavyArray) +for (let i = 0; i < view.length; i++) { + view[i] = Math.floor(Math.random() * 256) +} + +const integrationTestTemplate = 'jonas_base' + +test.skipIf(!isIntegrationTest)( + 'stress test heavy file writes and reads', + async () => { + const promises: Array> = [] + for (let i = 0; i < 500; i++) { + promises.push( + Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }) + .then((sbx) => { + console.log(sbx.sandboxId) + return sbx.files + .write('heavy-file', heavyArray) + .then(() => sbx.files.read('heavy-file')) + }) + .catch(console.error) + ) + } + await wait(10_000) + await Promise.all(promises) + } +) + +test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { + const promises: Array> = [] + + for (let i = 0; i < 10; i++) { + promises.push( + Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }).then((sbx) => { + console.log('created sandbox', sbx.sandboxId) + sbx.files + .write('heavy-file', heavyArray) + .then(() => { + sbx.commands.run('python -m http.server 8000', { background: true }) + }) + .then(() => { + new Promise((resolve, reject) => { + try { + resolve(sbx.getHost(8000)) + } catch (e) { + console.error('error getting sbx host', e) + reject(e) + } + }).then((host) => { + const url = `https://${host}` + console.log('fetching url', url) + fetch(url) + }) + + try { + sbx.kill() + } catch (e) { + console.error('error killing sbx', e) + } + }) + }) + ) + } + + await wait(10_000) + await Promise.all(promises) +}) diff --git a/packages/js-sdk/tests/integration/template/README.md b/packages/js-sdk/tests/integration/template/README.md new file mode 100644 index 000000000..2d4155dc3 --- /dev/null +++ b/packages/js-sdk/tests/integration/template/README.md @@ -0,0 +1,5 @@ +# Integration test template + +# Build the template + +`$ e2b template build"` diff --git a/packages/js-sdk/tests/integration/template/e2b.Dockerfile b/packages/js-sdk/tests/integration/template/e2b.Dockerfile new file mode 100644 index 000000000..65ceb7d65 --- /dev/null +++ b/packages/js-sdk/tests/integration/template/e2b.Dockerfile @@ -0,0 +1,8 @@ +FROM e2bdev/code-interpreter:latest + +# Clone the Next.js app repository +RUN git clone https://github.com/ezesundayeze/basic-nextjs-app + +# Install dependencies +RUN cd basic-nextjs-app && npm install + diff --git a/packages/js-sdk/tests/integration/template/e2b.toml b/packages/js-sdk/tests/integration/template/e2b.toml new file mode 100644 index 000000000..4f6dff736 --- /dev/null +++ b/packages/js-sdk/tests/integration/template/e2b.toml @@ -0,0 +1,18 @@ +# This is a config for E2B sandbox template. +# You can use template ID (2e2z80zhv34yumbrybvn) or template name (integration-test-v1) to create a sandbox: + +# Python SDK +# from e2b import Sandbox, AsyncSandbox +# sandbox = Sandbox("integration-test-v1") # Sync sandbox +# sandbox = await AsyncSandbox.create("integration-test-v1") # Async sandbox + +# JS SDK +# import { Sandbox } from 'e2b' +# const sandbox = await Sandbox.create('integration-test-v1') + +team_id = "b9c07023-d095-4bdc-9634-e25d5530ba47" +memory_mb = 1_024 +start_cmd = "npm run dev" +dockerfile = "e2b.Dockerfile" +template_name = "integration-test-v1" +template_id = "2e2z80zhv34yumbrybvn" diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 7cdbd5e0c..30889c60f 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -1,7 +1,7 @@ import { Sandbox } from '../src' import { test as base } from 'vitest' -export const template = 'base' +export const template = 'jonas_base' interface SandboxFixture { sandbox: Sandbox @@ -30,6 +30,7 @@ export const sandboxTest = base.extend({ }) export const isDebug = process.env.E2B_DEBUG !== undefined +export const isIntegrationTest = process.env.E2B_INTEGRATION_TEST !== undefined export async function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/packages/js-sdk/vitest.workspace.mts b/packages/js-sdk/vitest.workspace.mts index 8ce2118f6..38006cd44 100644 --- a/packages/js-sdk/vitest.workspace.mts +++ b/packages/js-sdk/vitest.workspace.mts @@ -5,20 +5,11 @@ const env = config() export default defineWorkspace([ { test: { - include: [ - 'tests/**/*.test.ts', - ], - exclude: [ - 'tests/runtimes/**', - ], - poolOptions: { - threads: { - minThreads: 1, - maxThreads: 4, - }, - }, + include: ['tests/**/*.test.ts'], + exclude: ['tests/runtimes/**', 'tests/integration/**'], + isolate: false, // for projects that don't rely on side effects, disabling isolation will improve the speed of the tests globals: false, - testTimeout: 30000, + testTimeout: 30_000, environment: 'node', bail: 1, server: {}, @@ -26,7 +17,7 @@ export default defineWorkspace([ interopDefault: true, }, env: { - ...process.env as Record, + ...(process.env as Record), ...env.parsed, }, }, @@ -43,8 +34,8 @@ export default defineWorkspace([ providerOptions: {}, }, env: { - ...process.env as Record, - ...env.parsed, + ...(process.env as Record), + ...env.parsed, }, }, }, @@ -55,4 +46,16 @@ export default defineWorkspace([ environment: 'edge-runtime', }, }, + { + test: { + include: ['tests/integration/**/*.test.ts'], + globals: false, + testTimeout: 60_000, + environment: 'node', + env: { + ...(process.env as Record), + ...env.parsed, + }, + }, + }, ]) From 411631385d742cedbb7e3d9ef6fdb188e3baf82f Mon Sep 17 00:00:00 2001 From: 0div Date: Mon, 23 Dec 2024 17:42:39 +0100 Subject: [PATCH 02/39] rm integration test template work --- .../js-sdk/tests/integration/stress.test.ts | 73 ------------------- .../tests/integration/template/README.md | 5 -- .../tests/integration/template/e2b.Dockerfile | 8 -- .../tests/integration/template/e2b.toml | 18 ----- 4 files changed, 104 deletions(-) delete mode 100644 packages/js-sdk/tests/integration/stress.test.ts delete mode 100644 packages/js-sdk/tests/integration/template/README.md delete mode 100644 packages/js-sdk/tests/integration/template/e2b.Dockerfile delete mode 100644 packages/js-sdk/tests/integration/template/e2b.toml diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts deleted file mode 100644 index b82830372..000000000 --- a/packages/js-sdk/tests/integration/stress.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { test } from 'vitest' - -import Sandbox from '../../src/index.js' -import { wait, isIntegrationTest } from '../setup.js' - -const heavyArray = new ArrayBuffer(256 * 1024 * 1024) // 256 MiB = 256 * 1024 * 1024 bytes -const view = new Uint8Array(heavyArray) -for (let i = 0; i < view.length; i++) { - view[i] = Math.floor(Math.random() * 256) -} - -const integrationTestTemplate = 'jonas_base' - -test.skipIf(!isIntegrationTest)( - 'stress test heavy file writes and reads', - async () => { - const promises: Array> = [] - for (let i = 0; i < 500; i++) { - promises.push( - Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }) - .then((sbx) => { - console.log(sbx.sandboxId) - return sbx.files - .write('heavy-file', heavyArray) - .then(() => sbx.files.read('heavy-file')) - }) - .catch(console.error) - ) - } - await wait(10_000) - await Promise.all(promises) - } -) - -test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { - const promises: Array> = [] - - for (let i = 0; i < 10; i++) { - promises.push( - Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }).then((sbx) => { - console.log('created sandbox', sbx.sandboxId) - sbx.files - .write('heavy-file', heavyArray) - .then(() => { - sbx.commands.run('python -m http.server 8000', { background: true }) - }) - .then(() => { - new Promise((resolve, reject) => { - try { - resolve(sbx.getHost(8000)) - } catch (e) { - console.error('error getting sbx host', e) - reject(e) - } - }).then((host) => { - const url = `https://${host}` - console.log('fetching url', url) - fetch(url) - }) - - try { - sbx.kill() - } catch (e) { - console.error('error killing sbx', e) - } - }) - }) - ) - } - - await wait(10_000) - await Promise.all(promises) -}) diff --git a/packages/js-sdk/tests/integration/template/README.md b/packages/js-sdk/tests/integration/template/README.md deleted file mode 100644 index 2d4155dc3..000000000 --- a/packages/js-sdk/tests/integration/template/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Integration test template - -# Build the template - -`$ e2b template build"` diff --git a/packages/js-sdk/tests/integration/template/e2b.Dockerfile b/packages/js-sdk/tests/integration/template/e2b.Dockerfile deleted file mode 100644 index 65ceb7d65..000000000 --- a/packages/js-sdk/tests/integration/template/e2b.Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM e2bdev/code-interpreter:latest - -# Clone the Next.js app repository -RUN git clone https://github.com/ezesundayeze/basic-nextjs-app - -# Install dependencies -RUN cd basic-nextjs-app && npm install - diff --git a/packages/js-sdk/tests/integration/template/e2b.toml b/packages/js-sdk/tests/integration/template/e2b.toml deleted file mode 100644 index 4f6dff736..000000000 --- a/packages/js-sdk/tests/integration/template/e2b.toml +++ /dev/null @@ -1,18 +0,0 @@ -# This is a config for E2B sandbox template. -# You can use template ID (2e2z80zhv34yumbrybvn) or template name (integration-test-v1) to create a sandbox: - -# Python SDK -# from e2b import Sandbox, AsyncSandbox -# sandbox = Sandbox("integration-test-v1") # Sync sandbox -# sandbox = await AsyncSandbox.create("integration-test-v1") # Async sandbox - -# JS SDK -# import { Sandbox } from 'e2b' -# const sandbox = await Sandbox.create('integration-test-v1') - -team_id = "b9c07023-d095-4bdc-9634-e25d5530ba47" -memory_mb = 1_024 -start_cmd = "npm run dev" -dockerfile = "e2b.Dockerfile" -template_name = "integration-test-v1" -template_id = "2e2z80zhv34yumbrybvn" From d90fc02ed1586b5ffba4bcc7113442b37cf952e8 Mon Sep 17 00:00:00 2001 From: 0div Date: Mon, 23 Dec 2024 17:43:24 +0100 Subject: [PATCH 03/39] add stress tests --- .../js-sdk/tests/integration/stress.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/js-sdk/tests/integration/stress.test.ts diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts new file mode 100644 index 000000000..b82830372 --- /dev/null +++ b/packages/js-sdk/tests/integration/stress.test.ts @@ -0,0 +1,73 @@ +import { test } from 'vitest' + +import Sandbox from '../../src/index.js' +import { wait, isIntegrationTest } from '../setup.js' + +const heavyArray = new ArrayBuffer(256 * 1024 * 1024) // 256 MiB = 256 * 1024 * 1024 bytes +const view = new Uint8Array(heavyArray) +for (let i = 0; i < view.length; i++) { + view[i] = Math.floor(Math.random() * 256) +} + +const integrationTestTemplate = 'jonas_base' + +test.skipIf(!isIntegrationTest)( + 'stress test heavy file writes and reads', + async () => { + const promises: Array> = [] + for (let i = 0; i < 500; i++) { + promises.push( + Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }) + .then((sbx) => { + console.log(sbx.sandboxId) + return sbx.files + .write('heavy-file', heavyArray) + .then(() => sbx.files.read('heavy-file')) + }) + .catch(console.error) + ) + } + await wait(10_000) + await Promise.all(promises) + } +) + +test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { + const promises: Array> = [] + + for (let i = 0; i < 10; i++) { + promises.push( + Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }).then((sbx) => { + console.log('created sandbox', sbx.sandboxId) + sbx.files + .write('heavy-file', heavyArray) + .then(() => { + sbx.commands.run('python -m http.server 8000', { background: true }) + }) + .then(() => { + new Promise((resolve, reject) => { + try { + resolve(sbx.getHost(8000)) + } catch (e) { + console.error('error getting sbx host', e) + reject(e) + } + }).then((host) => { + const url = `https://${host}` + console.log('fetching url', url) + fetch(url) + }) + + try { + sbx.kill() + } catch (e) { + console.error('error killing sbx', e) + } + }) + }) + ) + } + + await wait(10_000) + await Promise.all(promises) +}) From 7dc89ac7368309e1db40dce1726307eca8458c9e Mon Sep 17 00:00:00 2001 From: 0div Date: Mon, 23 Dec 2024 17:56:19 +0100 Subject: [PATCH 04/39] boilerplate for integration test template build --- .../tests/integration/template/e2b.Dockerfile | 8 ++++++++ .../js-sdk/tests/integration/template/e2b.toml | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 packages/js-sdk/tests/integration/template/e2b.Dockerfile create mode 100644 packages/js-sdk/tests/integration/template/e2b.toml diff --git a/packages/js-sdk/tests/integration/template/e2b.Dockerfile b/packages/js-sdk/tests/integration/template/e2b.Dockerfile new file mode 100644 index 000000000..65ceb7d65 --- /dev/null +++ b/packages/js-sdk/tests/integration/template/e2b.Dockerfile @@ -0,0 +1,8 @@ +FROM e2bdev/code-interpreter:latest + +# Clone the Next.js app repository +RUN git clone https://github.com/ezesundayeze/basic-nextjs-app + +# Install dependencies +RUN cd basic-nextjs-app && npm install + diff --git a/packages/js-sdk/tests/integration/template/e2b.toml b/packages/js-sdk/tests/integration/template/e2b.toml new file mode 100644 index 000000000..4f6dff736 --- /dev/null +++ b/packages/js-sdk/tests/integration/template/e2b.toml @@ -0,0 +1,18 @@ +# This is a config for E2B sandbox template. +# You can use template ID (2e2z80zhv34yumbrybvn) or template name (integration-test-v1) to create a sandbox: + +# Python SDK +# from e2b import Sandbox, AsyncSandbox +# sandbox = Sandbox("integration-test-v1") # Sync sandbox +# sandbox = await AsyncSandbox.create("integration-test-v1") # Async sandbox + +# JS SDK +# import { Sandbox } from 'e2b' +# const sandbox = await Sandbox.create('integration-test-v1') + +team_id = "b9c07023-d095-4bdc-9634-e25d5530ba47" +memory_mb = 1_024 +start_cmd = "npm run dev" +dockerfile = "e2b.Dockerfile" +template_name = "integration-test-v1" +template_id = "2e2z80zhv34yumbrybvn" From 3245acd4d9829763e739c9ee7c6a34c1fcdfce2f Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 26 Dec 2024 11:27:24 +0100 Subject: [PATCH 05/39] fix template build command to include npm start command and add nextjs stress test to integration suite --- .../js-sdk/tests/integration/stress.test.ts | 41 +++++++++++++++++-- .../tests/integration/template/README.md | 5 +++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 packages/js-sdk/tests/integration/template/README.md diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts index b82830372..c880dcbb2 100644 --- a/packages/js-sdk/tests/integration/stress.test.ts +++ b/packages/js-sdk/tests/integration/stress.test.ts @@ -9,13 +9,14 @@ for (let i = 0; i < view.length; i++) { view[i] = Math.floor(Math.random() * 256) } -const integrationTestTemplate = 'jonas_base' +const integrationTestTemplate = 'integration-test-v1' +const sanboxCount = 10 test.skipIf(!isIntegrationTest)( 'stress test heavy file writes and reads', async () => { const promises: Array> = [] - for (let i = 0; i < 500; i++) { + for (let i = 0; i < sanboxCount; i++) { promises.push( Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }) .then((sbx) => { @@ -35,7 +36,7 @@ test.skipIf(!isIntegrationTest)( test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { const promises: Array> = [] - for (let i = 0; i < 10; i++) { + for (let i = 0; i < sanboxCount; i++) { promises.push( Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }).then((sbx) => { console.log('created sandbox', sbx.sandboxId) @@ -71,3 +72,37 @@ test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { await wait(10_000) await Promise.all(promises) }) + +test.skipIf(!isIntegrationTest)('stress requests to nextjs app', async ({}) => { + const promises: Array> = [] + + for (let i = 0; i < sanboxCount; i++) { + promises.push( + Sandbox.create(integrationTestTemplate, { timeoutMs: 60_000 }) + .then((sbx) => { + console.log('created sandbox', sbx.sandboxId) + return new Promise((resolve, reject) => { + try { + resolve(sbx.getHost(3000)) + } catch (e) { + console.error('error getting sbx host', e) + reject(e) + } + }).then(async (host) => { + const url = `https://${host}` + console.log('fetching url', url) + await fetch(url).then(async (res) => { + console.log('response', res.status) + await res.text().then((text) => { + console.log('response body', text) + }) + }) + }) + }) + .catch(console.error) + ) + } + + await wait(10_000) + await Promise.all(promises) +}) diff --git a/packages/js-sdk/tests/integration/template/README.md b/packages/js-sdk/tests/integration/template/README.md new file mode 100644 index 000000000..7da9296a3 --- /dev/null +++ b/packages/js-sdk/tests/integration/template/README.md @@ -0,0 +1,5 @@ +# Integration test template + +# Build the template + +`$ e2b template build -c "cd /basic-nextjs-app/ && sudo npm run dev"` From e84bf8d0b4f431a3e03dd1da95bd87dd7edb2649 Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 26 Dec 2024 12:06:13 +0100 Subject: [PATCH 06/39] improve nextjs stress test --- .../js-sdk/tests/integration/stress.test.ts | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts index c880dcbb2..7f2e3bd78 100644 --- a/packages/js-sdk/tests/integration/stress.test.ts +++ b/packages/js-sdk/tests/integration/stress.test.ts @@ -74,12 +74,12 @@ test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { }) test.skipIf(!isIntegrationTest)('stress requests to nextjs app', async ({}) => { - const promises: Array> = [] + const hostPromises: Array> = [] for (let i = 0; i < sanboxCount; i++) { - promises.push( - Sandbox.create(integrationTestTemplate, { timeoutMs: 60_000 }) - .then((sbx) => { + hostPromises.push( + Sandbox.create(integrationTestTemplate, { timeoutMs: 60_000 }).then( + (sbx) => { console.log('created sandbox', sbx.sandboxId) return new Promise((resolve, reject) => { try { @@ -88,21 +88,30 @@ test.skipIf(!isIntegrationTest)('stress requests to nextjs app', async ({}) => { console.error('error getting sbx host', e) reject(e) } - }).then(async (host) => { - const url = `https://${host}` - console.log('fetching url', url) - await fetch(url).then(async (res) => { - console.log('response', res.status) - await res.text().then((text) => { - console.log('response body', text) - }) - }) }) - }) - .catch(console.error) + } + ) ) } await wait(10_000) - await Promise.all(promises) + const hosts = await Promise.all(hostPromises) + + const fetchPromises: Array> = [] + + for (let i = 0; i < 100; i++) { + for (const host of hosts) { + fetchPromises.push( + new Promise((resolve) => { + fetch('https://' + host) + .then((res) => { + console.log(`response for ${host}: ${res.status}`) + }) + .then(resolve) + }) + ) + } + } + + await Promise.all(fetchPromises) }) From b1d02dfbeab89a145328a0b18835b8681c872c24 Mon Sep 17 00:00:00 2001 From: 0div Date: Fri, 27 Dec 2024 18:48:34 +0100 Subject: [PATCH 07/39] add get_metrics method to sandbox sync & async --- .../get_sandboxes_sandbox_id_metrics.py | 164 ++++++++++++++++++ .../e2b/api/client/models/sandbox_metric.py | 93 ++++++++++ .../python-sdk/e2b/sandbox/sandbox_api.py | 19 +- packages/python-sdk/e2b/sandbox_async/main.py | 28 ++- .../e2b/sandbox_async/sandbox_api.py | 62 ++++++- packages/python-sdk/e2b/sandbox_sync/main.py | 23 ++- .../e2b/sandbox_sync/sandbox_api.py | 63 +++++-- .../tests/async/sandbox_async/test_metrics.py | 12 ++ .../tests/sync/sandbox_sync/test_metrics.py | 11 ++ spec/openapi.yml | 55 ++++++ 10 files changed, 501 insertions(+), 29 deletions(-) create mode 100644 packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py create mode 100644 packages/python-sdk/e2b/api/client/models/sandbox_metric.py create mode 100644 packages/python-sdk/tests/async/sandbox_async/test_metrics.py create mode 100644 packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py new file mode 100644 index 000000000..ee1ae717b --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py @@ -0,0 +1,164 @@ +from http import HTTPStatus +from typing import Any, Optional, Union, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.sandbox_metric import SandboxMetric +from ...types import Response + + +def _get_kwargs( + sandbox_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "get", + "url": f"/sandboxes/{sandbox_id}/metrics", + } + + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Any, list["SandboxMetric"]]]: + if response.status_code == 200: + response_200 = [] + _response_200 = response.json() + for response_200_item_data in _response_200: + response_200_item = SandboxMetric.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 + if response.status_code == 401: + response_401 = cast(Any, None) + return response_401 + if response.status_code == 500: + response_500 = cast(Any, None) + 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[Any, list["SandboxMetric"]]]: + 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[Any, list["SandboxMetric"]]]: + """Get sandbox metrics + + 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[Any, list['SandboxMetric']]] + """ + + 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[Any, list["SandboxMetric"]]]: + """Get sandbox metrics + + 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[Any, list['SandboxMetric']] + """ + + return sync_detailed( + sandbox_id=sandbox_id, + client=client, + ).parsed + + +async def asyncio_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, +) -> Response[Union[Any, list["SandboxMetric"]]]: + """Get sandbox metrics + + 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[Any, list['SandboxMetric']]] + """ + + 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[Any, list["SandboxMetric"]]]: + """Get sandbox metrics + + 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[Any, list['SandboxMetric']] + """ + + return ( + await asyncio_detailed( + sandbox_id=sandbox_id, + client=client, + ) + ).parsed diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py new file mode 100644 index 000000000..5867253ce --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py @@ -0,0 +1,93 @@ +import datetime +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +T = TypeVar("T", bound="SandboxMetric") + + +@_attrs_define +class SandboxMetric: + """Metric entry with timestamp and line + + Attributes: + timestamp (datetime.datetime): Timestamp of the log entry + cpu_pct (float): CPU usage percentage + cpu_count (int): Number of CPU cores + mem_mi_b_used (int): Memory used in MiB + mem_mi_b_total (int): Total memory in MiB + """ + + timestamp: datetime.datetime + cpu_pct: float + cpu_count: int + mem_mi_b_used: int + mem_mi_b_total: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + timestamp = self.timestamp.isoformat() + + cpu_pct = self.cpu_pct + + cpu_count = self.cpu_count + + mem_mi_b_used = self.mem_mi_b_used + + mem_mi_b_total = self.mem_mi_b_total + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "timestamp": timestamp, + "cpuPct": cpu_pct, + "cpuCount": cpu_count, + "memMiBUsed": mem_mi_b_used, + "memMiBTotal": mem_mi_b_total, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + d = src_dict.copy() + timestamp = isoparse(d.pop("timestamp")) + + cpu_pct = d.pop("cpuPct") + + cpu_count = d.pop("cpuCount") + + mem_mi_b_used = d.pop("memMiBUsed") + + mem_mi_b_total = d.pop("memMiBTotal") + + sandbox_metric = cls( + timestamp=timestamp, + cpu_pct=cpu_pct, + cpu_count=cpu_count, + mem_mi_b_used=mem_mi_b_used, + mem_mi_b_total=mem_mi_b_total, + ) + + sandbox_metric.additional_properties = d + return sandbox_metric + + @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/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index cbf876053..2f96f4510 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -1,7 +1,8 @@ from abc import ABC from dataclasses import dataclass -from typing import Optional, Dict from datetime import datetime +from typing import Dict, Optional + from httpx import Limits @@ -21,6 +22,22 @@ class SandboxInfo: """Sandbox start time.""" +@dataclass +class SandboxMetrics: + """Sandbox resource usage metrics""" + + timestamp: datetime + """Timestamp of the metrics.""" + cpu_pct: float + """CPU usage in percentage.""" + cpu_count: int + """Number of CPU cores.""" + mem_mib_used: int + """Memory usage in bytes.""" + mem_mib_total: int + """Total memory available""" + + class SandboxApiBase(ABC): _limits = Limits( max_keepalive_connections=10, diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 066162d85..740dd661f 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -1,18 +1,17 @@ import logging -import httpx - -from typing import Dict, Optional, TypedDict, overload -from typing_extensions import Unpack +from typing import Dict, List, Optional, TypedDict, overload +import httpx from e2b.connection_config import ConnectionConfig from e2b.envd.api import ENVD_API_HEALTH_ROUTE, ahandle_envd_api_exception from e2b.exceptions import format_request_timeout_error from e2b.sandbox.main import SandboxSetup from e2b.sandbox.utils import class_method_variant -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.filesystem.filesystem import Filesystem +from e2b.sandbox_async.sandbox_api import SandboxApi, SandboxMetrics +from typing_extensions import Unpack logger = logging.getLogger(__name__) @@ -364,3 +363,20 @@ async def set_timeout( # type: ignore timeout=timeout, **self.connection_config.__dict__, ) + + @class_method_variant("_cls_get_metrics") + async def get_metrics( # type: ignore + self, + request_timeout: Optional[float] = None, + ) -> List[SandboxMetrics]: + 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._cls_get_metrics( + sandbox_id=self.sandbox_id, + **self.connection_config.__dict__, + ) diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index ea99105cc..fbdd8b02c 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -1,18 +1,19 @@ -from typing import Optional, Dict, List -from packaging.version import Version +from datetime import datetime +from typing import Dict, List, Optional -from e2b.sandbox.sandbox_api import SandboxInfo, SandboxApiBase -from e2b.exceptions import TemplateException -from e2b.api import AsyncApiClient -from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody +from e2b.api import AsyncApiClient, handle_api_exception from e2b.api.client.api.sandboxes import ( - post_sandboxes_sandbox_id_timeout, - get_sandboxes, delete_sandboxes_sandbox_id, + get_sandboxes, + get_sandboxes_sandbox_id_metrics, post_sandboxes, + post_sandboxes_sandbox_id_timeout, ) +from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody from e2b.connection_config import ConnectionConfig -from e2b.api import handle_api_exception +from e2b.exceptions import TemplateException +from e2b.sandbox.sandbox_api import SandboxApiBase, SandboxInfo, SandboxMetrics +from packaging.version import Version class SandboxApi(SandboxApiBase): @@ -131,6 +132,49 @@ async def _cls_set_timeout( if res.status_code >= 300: raise handle_api_exception(res) + @classmethod + async def _cls_get_metrics( + cls, + sandbox_id: str, + api_key: Optional[str] = None, + domain: Optional[str] = None, + debug: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> List[SandboxMetrics]: + config = ConnectionConfig( + api_key=api_key, + domain=domain, + debug=debug, + request_timeout=request_timeout, + ) + + if config.debug: + # Skip getting the metrics in debug mode + return [] + + async with AsyncApiClient(config) as api_client: + res = await get_sandboxes_sandbox_id_metrics.asyncio_detailed( + sandbox_id, + client=api_client, + ) + + if res.status_code >= 300: + raise handle_api_exception(res) + + if res.parsed is None: + return [] + + return [ + SandboxMetrics( + timestamp=metric.timestamp, + cpu_pct=metric.cpu_pct, + cpu_count=metric.cpu_count, + mem_mib_used=metric.mem_mi_b_used, + mem_mib_total=metric.mem_mi_b_total, + ) + for metric in res.parsed + ] + @classmethod async def _create_sandbox( cls, diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 2fa54ea86..4d2b9094c 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Optional, overload +from typing import Dict, List, Optional, overload import httpx from e2b.connection_config import ConnectionConfig @@ -7,10 +7,10 @@ from e2b.exceptions import SandboxException, format_request_timeout_error from e2b.sandbox.main import SandboxSetup from e2b.sandbox.utils import class_method_variant -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.filesystem.filesystem import Filesystem +from e2b.sandbox_sync.sandbox_api import SandboxApi, SandboxMetrics logger = logging.getLogger(__name__) @@ -355,3 +355,20 @@ def set_timeout( # type: ignore timeout=timeout, **self.connection_config.__dict__, ) + + @class_method_variant("_get_metrics") + async def get_metrics( # type: ignore + self, + request_timeout: Optional[float] = None, + ) -> List[SandboxMetrics]: + 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._cls_get_metrics( + sandbox_id=self.sandbox_id, + **self.connection_config.__dict__, + ) diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 8e37aab02..1ff476916 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -1,19 +1,19 @@ -from httpx import HTTPTransport -from typing import Optional, Dict, List -from packaging.version import Version +from typing import Dict, List, Optional -from e2b.sandbox.sandbox_api import SandboxInfo, SandboxApiBase -from e2b.exceptions import TemplateException -from e2b.api import ApiClient -from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody +from e2b.api import ApiClient, handle_api_exception from e2b.api.client.api.sandboxes import ( - post_sandboxes_sandbox_id_timeout, - get_sandboxes, delete_sandboxes_sandbox_id, + get_sandboxes, + get_sandboxes_sandbox_id_metrics, post_sandboxes, + post_sandboxes_sandbox_id_timeout, ) +from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody from e2b.connection_config import ConnectionConfig -from e2b.api import handle_api_exception +from e2b.exceptions import TemplateException +from e2b.sandbox.sandbox_api import SandboxApiBase, SandboxInfo, SandboxMetrics +from httpx import HTTPTransport +from packaging.version import Version class SandboxApi(SandboxApiBase): @@ -138,6 +138,49 @@ def _cls_set_timeout( if res.status_code >= 300: raise handle_api_exception(res) + @classmethod + def _cls_get_metrics( + cls, + sandbox_id: str, + api_key: Optional[str] = None, + domain: Optional[str] = None, + debug: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> List[SandboxMetrics]: + config = ConnectionConfig( + api_key=api_key, + domain=domain, + debug=debug, + request_timeout=request_timeout, + ) + + if config.debug: + # Skip getting the metrics in debug mode + return [] + + with ApiClient(config) as api_client: + res = get_sandboxes_sandbox_id_metrics.sync_detailed( + sandbox_id, + client=api_client, + ) + + if res.status_code >= 300: + raise handle_api_exception(res) + + if res.parsed is None: + return [] + + return [ + SandboxMetrics( + timestamp=metric.timestamp, + cpu_pct=metric.cpu_pct, + cpu_count=metric.cpu_count, + mem_mib_used=metric.mem_mi_b_used, + mem_mib_total=metric.mem_mi_b_total, + ) + for metric in res.parsed + ] + @classmethod def _create_sandbox( cls, diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py new file mode 100644 index 000000000..57d1fb9ce --- /dev/null +++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py @@ -0,0 +1,12 @@ +import pytest +from e2b import AsyncSandbox + + +@pytest.mark.skip_debug() +async def test_get_metrics(async_sandbox: AsyncSandbox): + metrics = await async_sandbox.get_metrics() + assert len(metrics) > 0 + assert metrics[0].cpu_pct is not None + assert metrics[0].cpu_count is not None + assert metrics[0].mem_mib_used is not None + assert metrics[0].mem_mib_total is not None diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py new file mode 100644 index 000000000..a2b977cdd --- /dev/null +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.mark.skip_debug() +async def test_get_metrics(sandbox): + metrics = sandbox.get_metrics() + assert len(metrics) > 0 + assert metrics[0].cpu_pct is not None + assert metrics[0].cpu_count is not None + assert metrics[0].mem_mib_used is not None + assert metrics[0].mem_mib_total is not None diff --git a/spec/openapi.yml b/spec/openapi.yml index 17a1db854..a216b14f5 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -335,6 +335,35 @@ components: - ready - error + SandboxMetric: + description: Metric entry with timestamp and line + required: + - timestamp + - cpuPct + - cpuCount + - memMiBUsed + - memMiBTotal + properties: + timestamp: + type: string + format: date-time + description: Timestamp of the log entry + cpuPct: + type: number + format: float + description: CPU usage percentage + cpuCount: + type: integer + description: Number of CPU cores + memMiBUsed: + type: integer + format: int64 + description: Memory used in MiB + memMiBTotal: + type: integer + format: int64 + description: Total memory in MiB + Error: required: - code @@ -516,6 +545,32 @@ paths: $ref: "#/components/responses/404" "500": $ref: "#/components/responses/500" + format: int32 + + /sandboxes/{sandboxID}/metrics: + get: + description: Get sandbox metrics + tags: [sandboxes] + security: + - ApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + responses: + "200": + description: Successfully returned the sandbox metrics + content: + application/json: + schema: + type: array + items: + type: object + $ref: "#/components/schemas/SandboxMetric" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" /sandboxes/{sandboxID}/refreshes: post: From 198ac2eb43c3d77c1e10ad0be093bfb3640a2a6c Mon Sep 17 00:00:00 2001 From: 0div Date: Mon, 30 Dec 2024 15:20:24 +0100 Subject: [PATCH 08/39] add metrics command to CLI --- packages/cli/src/commands/sandbox/index.ts | 6 +- packages/cli/src/commands/sandbox/metrics.ts | 230 +++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/commands/sandbox/metrics.ts diff --git a/packages/cli/src/commands/sandbox/index.ts b/packages/cli/src/commands/sandbox/index.ts index 538ad5bff..e1547703b 100644 --- a/packages/cli/src/commands/sandbox/index.ts +++ b/packages/cli/src/commands/sandbox/index.ts @@ -1,10 +1,11 @@ import * as commander from 'commander' import { connectCommand } from './connect' -import { listCommand } from './list' import { killCommand } from './kill' -import { spawnCommand } from './spawn' +import { listCommand } from './list' import { logsCommand } from './logs' +import { metricsCommand } from './metrics' +import { spawnCommand } from './spawn' export const sandboxCommand = new commander.Command('sandbox') .description('work with sandboxes') @@ -14,3 +15,4 @@ export const sandboxCommand = new commander.Command('sandbox') .addCommand(killCommand) .addCommand(spawnCommand) .addCommand(logsCommand) + .addCommand(metricsCommand) diff --git a/packages/cli/src/commands/sandbox/metrics.ts b/packages/cli/src/commands/sandbox/metrics.ts new file mode 100644 index 000000000..ee985fc6f --- /dev/null +++ b/packages/cli/src/commands/sandbox/metrics.ts @@ -0,0 +1,230 @@ +import * as chalk from 'chalk' +import * as commander from 'commander' +import * as e2b from 'e2b' +import * as util from 'util' + +import { client, connectionConfig } from 'src/api' +import { asBold, asTimestamp, withUnderline } from 'src/utils/format' +import { wait } from 'src/utils/wait' +import { handleE2BRequestError } from '../../utils/errors' +import { listSandboxes } from './list' + +const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds + +function getShortID(sandboxID: string) { + return sandboxID.split('-')[0] +} + +function waitForSandboxEnd(sandboxID: string) { + let isRunning = true + + async function monitor() { + const startTime = new Date().getTime() + + // eslint-disable-next-line no-constant-condition + while (true) { + const currentTime = new Date().getTime() + const elapsedTime = currentTime - startTime // Time elapsed in milliseconds + + // Check if 24 hours (in milliseconds) have passed + if (elapsedTime >= maxRuntime) { + break + } + + const response = await listSandboxes() + const sandbox = response.find( + (s) => s.sandboxID === getShortID(sandboxID) + ) + if (!sandbox) { + isRunning = false + break + } + await wait(5000) + } + } + + monitor() + + return () => isRunning +} + +function formatEnum(e: { [key: string]: string }) { + return Object.values(e) + .map((level) => asBold(level)) + .join(', ') +} + +enum LogFormat { + JSON = 'json', + PRETTY = 'pretty', +} + +function cleanLogger(logger?: string) { + if (!logger) { + return '' + } + + return logger.replaceAll('Svc', '') +} + +export const metricsCommand = new commander.Command('metrics') + .description('show metrics for sandbox') + .argument( + '', + `show metrics for sandbox specified by ${asBold('')}` + ) + .alias('mt') + .option('-f, --follow', 'keep streaming metrics until the sandbox is closed') + .option( + '--format ', + `specify format for printing metrics (${formatEnum(LogFormat)})`, + LogFormat.PRETTY + ) + .action( + async ( + sandboxID: string, + opts?: { + level: string + follow: boolean + format: LogFormat + loggers?: string[] + } + ) => { + try { + const format = opts?.format.toLowerCase() as LogFormat | undefined + if (format && !Object.values(LogFormat).includes(format)) { + throw new Error(`Invalid log format: ${format}`) + } + + const getIsRunning = opts?.follow + ? waitForSandboxEnd(sandboxID) + : () => false + + let start: number | undefined + let isFirstRun = true + let firstMetricsPrinted = false + + if (format === LogFormat.PRETTY) { + console.log(`\nMetrics for sandbox ${asBold(sandboxID)}:`) + } + + const isRunningPromise = listSandboxes() + .then((r) => r.find((s) => s.sandboxID === getShortID(sandboxID))) + .then((s) => !!s) + + do { + const metrics = await getSandboxMetrics({ sandboxID }) + + if (metrics.length !== 0 && firstMetricsPrinted === false) { + firstMetricsPrinted = true + process.stdout.write('\n') + } + + for (const metric of metrics) { + printMetric(metric.timestamp, metric.line, format) + } + + const isRunning = await isRunningPromise + + if (!isRunning && metrics.length === 0 && isFirstRun) { + if (format === LogFormat.PRETTY) { + console.log( + `\nStopped printing metrics — sandbox ${withUnderline( + 'not found' + )}` + ) + } + break + } + + if (!isRunning) { + if (format === LogFormat.PRETTY) { + console.log( + `\nStopped printing metrics — sandbox is ${withUnderline( + 'closed' + )}` + ) + } + break + } + + const lastMetric = + metrics.length > 0 ? metrics[metrics.length - 1] : undefined + if (lastMetric) { + // TODO: Use the timestamp from the last metric instead of the current time? + start = new Date(lastMetric.timestamp).getTime() + 1 + } + + await wait(400) + isFirstRun = false + } while (getIsRunning() && opts?.follow) + } catch (err: any) { + console.error(err) + process.exit(1) + } + } + ) + +function printMetric( + timestamp: string, + line: string, + format: LogFormat | undefined +) { + const metric = JSON.parse(line) + const level = chalk.default.green() + + metric.logger = cleanLogger(metric.logger) + + delete metric['traceID'] + delete metric['instanceID'] + delete metric['source_type'] + delete metric['teamID'] + delete metric['source'] + delete metric['service'] + delete metric['envID'] + delete metric['sandboxID'] + + if (format === LogFormat.JSON) { + console.log( + JSON.stringify({ + timestamp: new Date(timestamp).toISOString(), + level, + ...metric, + }) + ) + } else { + const time = `[${new Date(timestamp).toISOString().replace(/T/, ' ')}]` + delete metric['level'] + console.log( + `${asTimestamp(time)} ${level} ` + + util.inspect(metric, { + colors: true, + depth: null, + maxArrayLength: Infinity, + sorted: true, + compact: true, + breakLength: Infinity, + }) + ) + } +} + +export async function getSandboxMetrics({ + sandboxID, +}: { + sandboxID: string +}): Promise { + const signal = connectionConfig.getSignal() + const res = await client.api.GET('/sandboxes/{sandboxID}/metrics', { + signal, + params: { + path: { + sandboxID, + }, + }, + }) + + handleE2BRequestError(res.error, 'Error while getting sandbox metrics') + + return res.data +} From c7ae7161fd7b86b4da77c2027ec2df9d989bd00b Mon Sep 17 00:00:00 2001 From: 0div <98087403+0div@users.noreply.github.com> Date: Fri, 17 Jan 2025 04:45:17 +0100 Subject: [PATCH 09/39] Update packages/js-sdk/tests/setup.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Novák --- packages/js-sdk/tests/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 30889c60f..5e5955f7f 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -1,7 +1,7 @@ import { Sandbox } from '../src' import { test as base } from 'vitest' -export const template = 'jonas_base' +export const template = 'base' interface SandboxFixture { sandbox: Sandbox From a76ce3f6f23e4b660704d02ff50c76136634445c Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 16 Jan 2025 20:16:42 -0800 Subject: [PATCH 10/39] used create-next-app in template; remove old draft test --- .../js-sdk/tests/integration/stress.test.ts | 42 +------------------ .../tests/integration/template/e2b.Dockerfile | 4 +- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts index 7f2e3bd78..69da1472d 100644 --- a/packages/js-sdk/tests/integration/stress.test.ts +++ b/packages/js-sdk/tests/integration/stress.test.ts @@ -1,7 +1,7 @@ import { test } from 'vitest' import Sandbox from '../../src/index.js' -import { wait, isIntegrationTest } from '../setup.js' +import { isIntegrationTest, wait } from '../setup.js' const heavyArray = new ArrayBuffer(256 * 1024 * 1024) // 256 MiB = 256 * 1024 * 1024 bytes const view = new Uint8Array(heavyArray) @@ -33,46 +33,6 @@ test.skipIf(!isIntegrationTest)( } ) -test.skipIf(!isIntegrationTest)('stress network ingress', async ({}) => { - const promises: Array> = [] - - for (let i = 0; i < sanboxCount; i++) { - promises.push( - Sandbox.create(integrationTestTemplate, { timeoutMs: 60 }).then((sbx) => { - console.log('created sandbox', sbx.sandboxId) - sbx.files - .write('heavy-file', heavyArray) - .then(() => { - sbx.commands.run('python -m http.server 8000', { background: true }) - }) - .then(() => { - new Promise((resolve, reject) => { - try { - resolve(sbx.getHost(8000)) - } catch (e) { - console.error('error getting sbx host', e) - reject(e) - } - }).then((host) => { - const url = `https://${host}` - console.log('fetching url', url) - fetch(url) - }) - - try { - sbx.kill() - } catch (e) { - console.error('error killing sbx', e) - } - }) - }) - ) - } - - await wait(10_000) - await Promise.all(promises) -}) - test.skipIf(!isIntegrationTest)('stress requests to nextjs app', async ({}) => { const hostPromises: Array> = [] diff --git a/packages/js-sdk/tests/integration/template/e2b.Dockerfile b/packages/js-sdk/tests/integration/template/e2b.Dockerfile index 65ceb7d65..2284ca7d3 100644 --- a/packages/js-sdk/tests/integration/template/e2b.Dockerfile +++ b/packages/js-sdk/tests/integration/template/e2b.Dockerfile @@ -1,7 +1,7 @@ FROM e2bdev/code-interpreter:latest -# Clone the Next.js app repository -RUN git clone https://github.com/ezesundayeze/basic-nextjs-app +# Create a basic Next.js app +RUN npx -y create-next-app@latest test --yes --ts --use-npm # Install dependencies RUN cd basic-nextjs-app && npm install From e855adb38f9c443766cd426c4aa11d4832bb9c6c Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Fri, 17 Jan 2025 17:58:04 -0800 Subject: [PATCH 11/39] Add docs about filter on Sandbox.list --- .../src/app/(docs)/docs/sandbox/list/page.mdx | 50 +++++++++++++++++++ .../app/(docs)/docs/sandbox/metadata/page.mdx | 4 ++ 2 files changed, 54 insertions(+) diff --git a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx index c1b68e288..7a32437f2 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx @@ -1,3 +1,5 @@ +import Link from 'next/link' + # List running sandboxes You can list all running sandboxes using the `Sandbox.list()` method. @@ -58,3 +60,51 @@ Running sandbox started at: 2024-10-15 21:13:07.311861+00:00 Running sandbox template id: 3e4rngfa34txe0gxc1zf ``` + + +## Filtering sandboxes + + This feature is in private beta. + + +You can filter sandboxes by: +- Metadata key value pairs + +This can be useful when you have a large number of sandboxes and want to find only specific ones. + + +```js +import { Sandbox } from '@e2b/code-interpreter' + +// Create sandbox with metadata. +const sandbox = await Sandbox.create({ + metadata: { + env: 'dev', // $HighlightLine + app: 'my-app', // $HighlightLine + userId: '123', // $HighlightLine + }, +}) + +// List running sandboxes filtered by metadata. +const runningSandboxes = await Sandbox.list({ + filters: { userId: '123', env: 'dev' } // $HighlightLine +}) +``` +```python +from e2b_code_interpreter import Sandbox + +# Create sandbox with metadata. +sandbox = Sandbox( + metadata={ + "env": "dev", # $HighlightLine + "app": "my-app", # $HighlightLine + "user_id": "123", # $HighlightLine + }, +) + +# List running sandboxes filtered by metadata. +running_sandboxes = Sandbox.list(filters={ + "userId": "123", "env": "dev" # $HighlightLine +}) +``` + diff --git a/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx index 0db8a8e01..bb150e2e9 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx @@ -1,3 +1,5 @@ +import Link from 'next/link' + # Sandbox metadata Metadata is a way to attach arbitrary key-value pairs for a sandbox. @@ -9,6 +11,8 @@ This is useful in various scenarios, for example: You specify metadata when creating a sandbox and can access it later through listing running sandboxes with `Sandbox.list()` method. +You can also filter sandboxes by metadata, you can find more about it here + ```js import { Sandbox } from '@e2b/code-interpreter' From fb89e9623ab9c563b8444bc9aa412f726dda6cb5 Mon Sep 17 00:00:00 2001 From: Vasek Mlejnsky Date: Fri, 17 Jan 2025 18:12:51 -0800 Subject: [PATCH 12/39] Add slight changes to wording in docs --- apps/web/src/app/(docs)/docs/sandbox/list/page.mdx | 2 +- apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx index 7a32437f2..6d6b986b8 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx @@ -70,7 +70,7 @@ Running sandbox template id: 3e4rngfa34txe0gxc1zf You can filter sandboxes by: - Metadata key value pairs -This can be useful when you have a large number of sandboxes and want to find only specific ones. +This can be useful when you have a large number of sandboxes and want to find only specific ones. The filtering is performed on the server side. ```js diff --git a/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx index bb150e2e9..3eb890e91 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/metadata/page.mdx @@ -11,8 +11,6 @@ This is useful in various scenarios, for example: You specify metadata when creating a sandbox and can access it later through listing running sandboxes with `Sandbox.list()` method. -You can also filter sandboxes by metadata, you can find more about it here - ```js import { Sandbox } from '@e2b/code-interpreter' @@ -51,3 +49,6 @@ running_sandboxes = Sandbox.list() print(running_sandboxes[0].metadata) ``` + +## Filtering sandboxes by metadata +You can also filter sandboxes by metadata, you can find more about it here. \ No newline at end of file From 6aff0a1e24806e76fb9b60a6addfb159fdfa0fe0 Mon Sep 17 00:00:00 2001 From: Vasek Mlejnsky Date: Sat, 18 Jan 2025 14:57:10 -0800 Subject: [PATCH 13/39] Clarify how filtering works with multiple key-value pairs --- apps/web/src/app/(docs)/docs/sandbox/list/page.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx index 6d6b986b8..db5e7c3cc 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx @@ -64,11 +64,11 @@ Running sandbox template id: 3e4rngfa34txe0gxc1zf ## Filtering sandboxes - This feature is in private beta. +This feature is in a private beta. -You can filter sandboxes by: -- Metadata key value pairs +You can filter sandboxes by specifying Metadata key value pairs. +Specifying multiple key value pairs will return sandboxes that match all of them. This can be useful when you have a large number of sandboxes and want to find only specific ones. The filtering is performed on the server side. @@ -85,7 +85,7 @@ const sandbox = await Sandbox.create({ }, }) -// List running sandboxes filtered by metadata. +// List running sandboxes that has `userId` key with value `123` and `env` key with value `dev`. const runningSandboxes = await Sandbox.list({ filters: { userId: '123', env: 'dev' } // $HighlightLine }) @@ -102,7 +102,7 @@ sandbox = Sandbox( }, ) -# List running sandboxes filtered by metadata. +# List running sandboxes that has `userId` key with value `123` and `env` key with value `dev`. running_sandboxes = Sandbox.list(filters={ "userId": "123", "env": "dev" # $HighlightLine }) From 8703049692453299e01ceca3190819156b8449ee Mon Sep 17 00:00:00 2001 From: Vasek Mlejnsky Date: Sat, 18 Jan 2025 15:02:22 -0800 Subject: [PATCH 14/39] Update text --- apps/web/src/app/(docs)/docs/sandbox/list/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx index db5e7c3cc..7aa236695 100644 --- a/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx +++ b/apps/web/src/app/(docs)/docs/sandbox/list/page.mdx @@ -70,7 +70,7 @@ This feature is in a private beta. You can filter sandboxes by specifying Metadata key value pairs. Specifying multiple key value pairs will return sandboxes that match all of them. -This can be useful when you have a large number of sandboxes and want to find only specific ones. The filtering is performed on the server side. +This can be useful when you have a large number of sandboxes and want to find only specific ones. The filtering is performed on the server. ```js From 1c1a6816928710268e76862f7f682f0ac0f3412d Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Mon, 20 Jan 2025 14:45:22 -0800 Subject: [PATCH 15/39] Fix default user for the user --- apps/web/src/utils/useUser.tsx | 41 ++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/apps/web/src/utils/useUser.tsx b/apps/web/src/utils/useUser.tsx index 8d1decd0a..0883b4c6b 100644 --- a/apps/web/src/utils/useUser.tsx +++ b/apps/web/src/utils/useUser.tsx @@ -15,13 +15,16 @@ export type Team = { apiKeys: string[] } +interface APIKey { api_key: string; } interface UserTeam { - id: string; - name: string; is_default: boolean; - tier: string; - email: string; - team_api_keys: { api_key: string; }[]; + teams: { + tier: string; + email: string; + team_api_keys: { api_key: string; }[]; + id: string; + name: string; + } } export type E2BUser = (User & { @@ -110,20 +113,30 @@ export const CustomUserContextProvider = (props) => { if (!session) return if (!session.user.id) return - // @ts-ignore - const { data: userTeams, teamsError } = await supabase + const { data: userTeams, error: teamsError } = await supabase .from('users_teams') - .select('teams (id, name, is_default, tier, email, team_api_keys (api_key))') + .select('is_default, teams (id, name, tier, email, team_api_keys (api_key))') .eq('user_id', session?.user.id) // Due to RLS, we could also safely just fetch all, but let's be explicit for sure if (teamsError) Sentry.captureException(teamsError) - // TODO: Adjust when user can be part of multiple teams - // @ts-ignore - const teams = userTeams?.map(userTeam => userTeam.teams).map((team: UserTeam) => ({ - ...team, - apiKeys: team.team_api_keys.map(apiKey => apiKey.api_key) - } as Team)) + if (userTeams === undefined || userTeams === null) { + console.log('No user teams found') + Sentry.captureEvent({ message: 'No user teams found' }) + return + } + + const typedUserTeams = userTeams as unknown as UserTeam[] + const teams: Team[] = typedUserTeams.map((userTeam: UserTeam): Team => { + return { + id: userTeam.teams.id, + name: userTeam.teams.name, + tier: userTeam.teams.tier, + is_default: userTeam.is_default, + email: userTeam.teams.email, + apiKeys: userTeam.teams.team_api_keys.map((apiKey: APIKey) => apiKey.api_key), + } + }) const defaultTeam = teams?.find(team => team.is_default) if (!defaultTeam) { From fcd40f11599103c0204e3a1a50cd616e01d204ca Mon Sep 17 00:00:00 2001 From: 0div Date: Mon, 20 Jan 2025 18:54:01 -0800 Subject: [PATCH 16/39] fix js-sdk getMetrics --- packages/cli/src/commands/sandbox/metrics.ts | 4 +- packages/js-sdk/src/api/schema.gen.ts | 50 +++++++++++++++++++ packages/js-sdk/src/sandbox/index.ts | 18 +++++++ packages/js-sdk/src/sandbox/sandboxApi.ts | 40 ++++++++++++++- packages/js-sdk/tests/sandbox/metrics.test.ts | 12 +++++ 5 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 packages/js-sdk/tests/sandbox/metrics.test.ts diff --git a/packages/cli/src/commands/sandbox/metrics.ts b/packages/cli/src/commands/sandbox/metrics.ts index ee985fc6f..a564f23c0 100644 --- a/packages/cli/src/commands/sandbox/metrics.ts +++ b/packages/cli/src/commands/sandbox/metrics.ts @@ -121,7 +121,7 @@ export const metricsCommand = new commander.Command('metrics') } for (const metric of metrics) { - printMetric(metric.timestamp, metric.line, format) + printMetric(metric.timestamp, JSON.stringify(metric), format) } const isRunning = await isRunningPromise @@ -226,5 +226,5 @@ export async function getSandboxMetrics({ handleE2BRequestError(res.error, 'Error while getting sandbox metrics') - return res.data + return res.data as e2b.components['schemas']['SandboxMetric'][] } diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 6fe8e4642..12ac9a051 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -111,6 +111,27 @@ export interface paths { }; }; }; + "/sandboxes/{sandboxID}/metrics": { + /** @description Get sandbox metrics */ + get: { + parameters: { + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + }; + responses: { + /** @description Successfully returned the sandbox metrics */ + 200: { + content: { + "application/json": components["schemas"]["SandboxMetric"][]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + }; "/sandboxes/{sandboxID}/pause": { /** @description Pause the sandbox */ post: { @@ -210,6 +231,7 @@ export interface paths { }; 401: components["responses"]["401"]; 404: components["responses"]["404"]; + /** Format: int32 */ 500: components["responses"]["500"]; }; }; @@ -497,6 +519,34 @@ export interface components { SandboxMetadata: { [key: string]: string; }; + /** @description Metric entry with timestamp and line */ + SandboxMetric: { + /** + * Format: int32 + * @description Number of CPU cores + */ + cpuCount: number; + /** + * Format: float + * @description CPU usage percentage + */ + cpuPct: number; + /** + * Format: int64 + * @description Total memory in MiB + */ + memMiBTotal: number; + /** + * Format: int64 + * @description Memory used in MiB + */ + memMiBUsed: number; + /** + * Format: date-time + * @description Timestamp of the metric entry + */ + timestamp: string; + }; Team: { /** @description API key for the team */ apiKey: string; diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 37edbcae6..37bacb076 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -1,5 +1,6 @@ import { createConnectTransport } from '@connectrpc/connect-web' +import { components } from '../api' import { ConnectionConfig, ConnectionOpts, @@ -278,6 +279,23 @@ export class Sandbox extends SandboxApi { return true } + /** + * Get the metrics of the sandbox. + * + * @param timeoutMs timeout in **milliseconds**. + * @param opts connection options. + * + * @returns metrics of the sandbox. + */ + async getMetrics( + opts?: Pick + ): Promise { + return await Sandbox.getMetrics(this.sandboxId, { + ...this.connectionConfig, + ...opts, + }) + } + /** * Set the timeout of the sandbox. * After the timeout expires the sandbox will be automatically killed. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 1cf997f4d..28ba13e10 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -1,6 +1,6 @@ +import { compareVersions } from 'compare-versions' import { ApiClient, components, handleApiError } from '../api' import { ConnectionConfig, ConnectionOpts } from '../connectionConfig' -import { compareVersions } from 'compare-versions' import { TemplateError } from '../errors' /** @@ -114,6 +114,44 @@ export class SandboxApi { ) } + /** + * Get the metrics of the sandbox. + * + * @param sandboxId sandbox ID. + * @param timeoutMs timeout in **milliseconds**. + * @param opts connection options. + * + * @returns metrics of the sandbox. + */ + static async getMetrics( + sandboxId: string, + opts?: SandboxApiOpts + ): Promise { + const config = new ConnectionConfig(opts) + const client = new ApiClient(config) + + const res = await client.api.GET('/sandboxes/{sandboxID}/metrics', { + params: { + path: { + sandboxID: sandboxId, + }, + }, + signal: config.getSignal(opts?.requestTimeoutMs), + }) + + const err = handleApiError(res) + if (err) { + throw err + } + + return ( + res.data?.map((metric: components['schemas']['SandboxMetric']) => ({ + ...metric, + timestamp: new Date(metric.timestamp).toISOString(), + })) ?? [] + ) + } + /** * Set the timeout of the specified sandbox. * After the timeout expires the sandbox will be automatically killed. diff --git a/packages/js-sdk/tests/sandbox/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts new file mode 100644 index 000000000..524f22c23 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/metrics.test.ts @@ -0,0 +1,12 @@ +import { assert } from 'vitest' + +import { sandboxTest } from '../setup.js' + +sandboxTest('get sandbox metrics', async ({ sandbox }) => { + const metrics = await sandbox.getMetrics() + + assert.isAtLeast(metrics.length, 1) + assert.isAtLeast(metrics[0]?.cpuPct, 0) + assert.isAtLeast(metrics[0]?.memMiBTotal, 0) + assert.isAtLeast(metrics[0]?.memMiBUsed, 0) +}) From 91b06e3165172d53dbb59ae8e3f37be0012357de Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Wed, 22 Jan 2025 09:50:58 -0800 Subject: [PATCH 17/39] Fix create checkout for users on another clusters --- apps/web/src/components/Pricing/SwitchToProButton.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/web/src/components/Pricing/SwitchToProButton.tsx b/apps/web/src/components/Pricing/SwitchToProButton.tsx index 4d5fc4819..0323701d8 100644 --- a/apps/web/src/components/Pricing/SwitchToProButton.tsx +++ b/apps/web/src/components/Pricing/SwitchToProButton.tsx @@ -9,18 +9,9 @@ import Spinner from '@/components/Spinner' import { TierActiveTag } from './TierActiveTag' import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' -import { toast } from '@/components/ui/use-toast' function createCheckout(domain: string, tierID: string, teamID: string) { - if (domain !== 'e2b.dev') { - console.error('Managing billing is allowed only at e2b.dev.') - toast({ - title: 'Error', - description: 'Managing billing is allowed only at e2b.dev.', - }) - } - - return fetch(getBillingUrl(domain, '/checkouts'), { + return fetch(getBillingUrl('e2b.dev', '/checkouts'), { method: 'POST', headers: { 'Content-Type': 'application/json', From f59f2cb5db77e207ec0f8127daa9708a01a9b730 Mon Sep 17 00:00:00 2001 From: 0div Date: Wed, 22 Jan 2025 16:03:04 -0800 Subject: [PATCH 18/39] update openapi spec for updated metrics type --- packages/js-sdk/src/api/schema.gen.ts | 4 ++-- .../js-sdk/tests/sandbox/commands/envVars.test.ts | 5 +++++ packages/js-sdk/tests/sandbox/metrics.test.ts | 11 ++++++++--- spec/openapi.yml | 8 ++++---- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 12ac9a051..8449822f1 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -535,12 +535,12 @@ export interface components { * Format: int64 * @description Total memory in MiB */ - memMiBTotal: number; + memTotalMiB: number; /** * Format: int64 * @description Memory used in MiB */ - memMiBUsed: number; + memUsedMiB: number; /** * Format: date-time * @description Timestamp of the metric entry diff --git a/packages/js-sdk/tests/sandbox/commands/envVars.test.ts b/packages/js-sdk/tests/sandbox/commands/envVars.test.ts index 148b0a4f5..5d583cdae 100644 --- a/packages/js-sdk/tests/sandbox/commands/envVars.test.ts +++ b/packages/js-sdk/tests/sandbox/commands/envVars.test.ts @@ -22,3 +22,8 @@ sandboxTest.skipIf(isDebug)('env vars on sandbox', async () => { await sandbox.kill() } }) + +sandboxTest.skipIf(isDebug)('default env vars present', async ({ sandbox }) => { + const result = await sandbox.commands.run('echo $E2B_SANDBOX') + assert.equal(result?.stdout.trim(), 'true') +}) diff --git a/packages/js-sdk/tests/sandbox/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts index 524f22c23..89d8175a9 100644 --- a/packages/js-sdk/tests/sandbox/metrics.test.ts +++ b/packages/js-sdk/tests/sandbox/metrics.test.ts @@ -1,12 +1,17 @@ import { assert } from 'vitest' -import { sandboxTest } from '../setup.js' +import { wait, sandboxTest } from '../setup.js' sandboxTest('get sandbox metrics', async ({ sandbox }) => { + console.log('Getting metrics for sandbox ID:', sandbox.sandboxId) + + await wait(5_000) + const metrics = await sandbox.getMetrics() + console.log('Metrics:', metrics) assert.isAtLeast(metrics.length, 1) assert.isAtLeast(metrics[0]?.cpuPct, 0) - assert.isAtLeast(metrics[0]?.memMiBTotal, 0) - assert.isAtLeast(metrics[0]?.memMiBUsed, 0) + assert.isAtLeast(metrics[0]?.memTotalMiB, 0) + assert.isAtLeast(metrics[0]?.memUsedMiB, 0) }) diff --git a/spec/openapi.yml b/spec/openapi.yml index 509e54021..18b80ce72 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -428,8 +428,8 @@ components: - timestamp - cpuCount - cpuPct - - memMiBUsed - - memMiBTotal + - memUsedMiB + - memTotalMiB properties: timestamp: type: string @@ -443,11 +443,11 @@ components: type: number format: float description: CPU usage percentage - memMiBUsed: + memUsedMiB: type: integer format: int64 description: Memory used in MiB - memMiBTotal: + memTotalMiB: type: integer format: int64 description: Total memory in MiB From a3a61e073c090f2bf31b81f7d8fa0b589867545c Mon Sep 17 00:00:00 2001 From: 0div Date: Wed, 22 Jan 2025 17:13:47 -0800 Subject: [PATCH 19/39] update python-sdk models --- Makefile | 1 + .../get_sandboxes_sandbox_id_metrics.py | 6 +-- .../e2b/api/client/models/sandbox_metric.py | 48 +++++++++---------- .../e2b/sandbox_async/sandbox_api.py | 4 +- .../tests/async/sandbox_async/test_metrics.py | 9 +++- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index af4b39302..3b6688e97 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ generate-js: cd packages/js-sdk && pnpm generate-envd-api cd spec/envd && buf generate --template buf-js.gen.yaml +# `brew install protobuf` beforehand generate-python: $(MAKE) -C packages/connect-python build cd packages/python-sdk && make generate-api diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py index ee1ae717b..76905092d 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py @@ -32,12 +32,12 @@ def _parse_response( response_200.append(response_200_item) return response_200 - if response.status_code == 404: - response_404 = cast(Any, None) - return response_404 if response.status_code == 401: response_401 = cast(Any, None) return response_401 + if response.status_code == 404: + response_404 = cast(Any, None) + return response_404 if response.status_code == 500: response_500 = cast(Any, None) return response_500 diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py index 5867253ce..939aa7caf 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py @@ -13,40 +13,40 @@ class SandboxMetric: """Metric entry with timestamp and line Attributes: - timestamp (datetime.datetime): Timestamp of the log entry - cpu_pct (float): CPU usage percentage cpu_count (int): Number of CPU cores - mem_mi_b_used (int): Memory used in MiB - mem_mi_b_total (int): Total memory in MiB + cpu_pct (float): CPU usage percentage + mem_total_mi_b (int): Total memory in MiB + mem_used_mi_b (int): Memory used in MiB + timestamp (datetime.datetime): Timestamp of the metric entry """ - timestamp: datetime.datetime - cpu_pct: float cpu_count: int - mem_mi_b_used: int - mem_mi_b_total: int + cpu_pct: float + mem_total_mi_b: int + mem_used_mi_b: int + timestamp: datetime.datetime additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - timestamp = self.timestamp.isoformat() + cpu_count = self.cpu_count cpu_pct = self.cpu_pct - cpu_count = self.cpu_count + mem_total_mi_b = self.mem_total_mi_b - mem_mi_b_used = self.mem_mi_b_used + mem_used_mi_b = self.mem_used_mi_b - mem_mi_b_total = self.mem_mi_b_total + timestamp = self.timestamp.isoformat() field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { - "timestamp": timestamp, - "cpuPct": cpu_pct, "cpuCount": cpu_count, - "memMiBUsed": mem_mi_b_used, - "memMiBTotal": mem_mi_b_total, + "cpuPct": cpu_pct, + "memTotalMiB": mem_total_mi_b, + "memUsedMiB": mem_used_mi_b, + "timestamp": timestamp, } ) @@ -55,22 +55,22 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: d = src_dict.copy() - timestamp = isoparse(d.pop("timestamp")) + cpu_count = d.pop("cpuCount") cpu_pct = d.pop("cpuPct") - cpu_count = d.pop("cpuCount") + mem_total_mi_b = d.pop("memTotalMiB") - mem_mi_b_used = d.pop("memMiBUsed") + mem_used_mi_b = d.pop("memUsedMiB") - mem_mi_b_total = d.pop("memMiBTotal") + timestamp = isoparse(d.pop("timestamp")) sandbox_metric = cls( - timestamp=timestamp, - cpu_pct=cpu_pct, cpu_count=cpu_count, - mem_mi_b_used=mem_mi_b_used, - mem_mi_b_total=mem_mi_b_total, + cpu_pct=cpu_pct, + mem_total_mi_b=mem_total_mi_b, + mem_used_mi_b=mem_used_mi_b, + timestamp=timestamp, ) sandbox_metric.additional_properties = d diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index fbdd8b02c..9f00eb7ea 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -169,8 +169,8 @@ async def _cls_get_metrics( timestamp=metric.timestamp, cpu_pct=metric.cpu_pct, cpu_count=metric.cpu_count, - mem_mib_used=metric.mem_mi_b_used, - mem_mib_total=metric.mem_mi_b_total, + mem_used_mib=metric.mem_used_mi_b, + mem_total_mib=metric.mem_total_mi_b, ) for metric in res.parsed ] diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py index 57d1fb9ce..d32b7b98d 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py @@ -5,8 +5,13 @@ @pytest.mark.skip_debug() async def test_get_metrics(async_sandbox: AsyncSandbox): metrics = await async_sandbox.get_metrics() + + import asyncio + + await asyncio.sleep(10) + assert len(metrics) > 0 assert metrics[0].cpu_pct is not None assert metrics[0].cpu_count is not None - assert metrics[0].mem_mib_used is not None - assert metrics[0].mem_mib_total is not None + assert metrics[0].mem_used_mib is not None + assert metrics[0].mem_total_mib is not None From 881b4c8f047f2a791e4f0f7e51233f252af04e79 Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Wed, 22 Jan 2025 17:24:07 -0800 Subject: [PATCH 20/39] Regenerate python --- .../sandboxes/delete_sandboxes_sandbox_id.py | 8 +++-- .../get_sandboxes_sandbox_id_metrics.py | 34 +++++++++--------- .../post_sandboxes_sandbox_id_pause.py | 8 +++-- .../post_sandboxes_sandbox_id_refreshes.py | 12 +++++-- .../post_sandboxes_sandbox_id_timeout.py | 12 +++++-- packages/python-sdk/e2b/api/client/client.py | 36 ++++++++++++++----- .../e2b/api/client/models/__init__.py | 6 +++- .../e2b/api/client/models/sandbox_metric.py | 12 +++---- packages/python-sdk/e2b/sandbox/main.py | 9 +++-- .../sandbox_async/commands/command_handle.py | 4 +-- .../sandbox_sync/commands/command_handle.py | 4 +-- .../async/sandbox_async/commands/test_run.py | 10 ++++-- .../sync/sandbox_sync/commands/test_run.py | 6 ++-- 13 files changed, 106 insertions(+), 55 deletions(-) diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py b/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py index 30a419c00..11b5a4cda 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py @@ -19,7 +19,9 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -34,7 +36,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py index 76905092d..85851eda1 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union, cast import httpx @@ -11,8 +11,8 @@ def _get_kwargs( sandbox_id: str, -) -> dict[str, Any]: - _kwargs: dict[str, Any] = { +) -> Dict[str, Any]: + _kwargs: Dict[str, Any] = { "method": "get", "url": f"/sandboxes/{sandbox_id}/metrics", } @@ -22,8 +22,8 @@ def _get_kwargs( def _parse_response( *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[Any, list["SandboxMetric"]]]: - if response.status_code == 200: +) -> Optional[Union[Any, List["SandboxMetric"]]]: + if response.status_code == HTTPStatus.OK: response_200 = [] _response_200 = response.json() for response_200_item_data in _response_200: @@ -32,13 +32,13 @@ def _parse_response( response_200.append(response_200_item) return response_200 - if response.status_code == 401: + if response.status_code == HTTPStatus.UNAUTHORIZED: response_401 = cast(Any, None) return response_401 - if response.status_code == 404: + if response.status_code == HTTPStatus.NOT_FOUND: response_404 = cast(Any, None) return response_404 - if response.status_code == 500: + if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: response_500 = cast(Any, None) return response_500 if client.raise_on_unexpected_status: @@ -49,7 +49,7 @@ def _parse_response( def _build_response( *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[Any, list["SandboxMetric"]]]: +) -> Response[Union[Any, List["SandboxMetric"]]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -62,7 +62,7 @@ def sync_detailed( sandbox_id: str, *, client: AuthenticatedClient, -) -> Response[Union[Any, list["SandboxMetric"]]]: +) -> Response[Union[Any, List["SandboxMetric"]]]: """Get sandbox metrics Args: @@ -73,7 +73,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, list['SandboxMetric']]] + Response[Union[Any, List['SandboxMetric']]] """ kwargs = _get_kwargs( @@ -91,7 +91,7 @@ def sync( sandbox_id: str, *, client: AuthenticatedClient, -) -> Optional[Union[Any, list["SandboxMetric"]]]: +) -> Optional[Union[Any, List["SandboxMetric"]]]: """Get sandbox metrics Args: @@ -102,7 +102,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, list['SandboxMetric']] + Union[Any, List['SandboxMetric']] """ return sync_detailed( @@ -115,7 +115,7 @@ async def asyncio_detailed( sandbox_id: str, *, client: AuthenticatedClient, -) -> Response[Union[Any, list["SandboxMetric"]]]: +) -> Response[Union[Any, List["SandboxMetric"]]]: """Get sandbox metrics Args: @@ -126,7 +126,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, list['SandboxMetric']]] + Response[Union[Any, List['SandboxMetric']]] """ kwargs = _get_kwargs( @@ -142,7 +142,7 @@ async def asyncio( sandbox_id: str, *, client: AuthenticatedClient, -) -> Optional[Union[Any, list["SandboxMetric"]]]: +) -> Optional[Union[Any, List["SandboxMetric"]]]: """Get sandbox metrics Args: @@ -153,7 +153,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, list['SandboxMetric']] + Union[Any, List['SandboxMetric']] """ return ( diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py index 8133ed132..e47e05229 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py @@ -19,7 +19,9 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -36,7 +38,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py index 163bcf177..39dfc678a 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py @@ -5,7 +5,9 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.post_sandboxes_sandbox_id_refreshes_body import PostSandboxesSandboxIDRefreshesBody +from ...models.post_sandboxes_sandbox_id_refreshes_body import ( + PostSandboxesSandboxIDRefreshesBody, +) from ...types import Response @@ -30,7 +32,9 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -43,7 +47,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py index 148d97c87..615963abf 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py @@ -5,7 +5,9 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody +from ...models.post_sandboxes_sandbox_id_timeout_body import ( + PostSandboxesSandboxIDTimeoutBody, +) from ...types import Response @@ -30,7 +32,9 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -45,7 +49,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/client.py b/packages/python-sdk/e2b/api/client/client.py index 63a2493b9..38b07d057 100644 --- a/packages/python-sdk/e2b/api/client/client.py +++ b/packages/python-sdk/e2b/api/client/client.py @@ -38,9 +38,15 @@ class Client: _base_url: str = field(alias="base_url") _cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") - _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _timeout: Optional[httpx.Timeout] = field( + default=None, kw_only=True, alias="timeout" + ) + _verify_ssl: Union[str, bool, ssl.SSLContext] = field( + default=True, kw_only=True, alias="verify_ssl" + ) + _follow_redirects: bool = field( + default=False, kw_only=True, alias="follow_redirects" + ) _httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") _client: Optional[httpx.Client] = field(default=None, init=False) _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) @@ -168,9 +174,15 @@ class AuthenticatedClient: _base_url: str = field(alias="base_url") _cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") - _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _timeout: Optional[httpx.Timeout] = field( + default=None, kw_only=True, alias="timeout" + ) + _verify_ssl: Union[str, bool, ssl.SSLContext] = field( + default=True, kw_only=True, alias="verify_ssl" + ) + _follow_redirects: bool = field( + default=False, kw_only=True, alias="follow_redirects" + ) _httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") _client: Optional[httpx.Client] = field(default=None, init=False) _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) @@ -214,7 +226,9 @@ def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": def get_httpx_client(self) -> httpx.Client: """Get the underlying httpx.Client, constructing a new one if not previously set""" if self._client is None: - self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._headers[self.auth_header_name] = ( + f"{self.prefix} {self.token}" if self.prefix else self.token + ) self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -235,7 +249,9 @@ def __exit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for internal httpx.Client (see httpx docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": + def set_async_httpx_client( + self, async_client: httpx.AsyncClient + ) -> "AuthenticatedClient": """Manually the underlying httpx.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. @@ -246,7 +262,9 @@ def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Authentica def get_async_httpx_client(self) -> httpx.AsyncClient: """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: - self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._headers[self.auth_header_name] = ( + f"{self.prefix} {self.token}" if self.prefix else self.token + ) self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index ec61ba395..f0630f3c6 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -6,13 +6,16 @@ from .node_detail import NodeDetail from .node_status import NodeStatus from .node_status_change import NodeStatusChange -from .post_sandboxes_sandbox_id_refreshes_body import PostSandboxesSandboxIDRefreshesBody +from .post_sandboxes_sandbox_id_refreshes_body import ( + PostSandboxesSandboxIDRefreshesBody, +) from .post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody from .resumed_sandbox import ResumedSandbox from .running_sandbox import RunningSandbox from .sandbox import Sandbox from .sandbox_log import SandboxLog from .sandbox_logs import SandboxLogs +from .sandbox_metric import SandboxMetric from .team import Team from .team_user import TeamUser from .template import Template @@ -35,6 +38,7 @@ "Sandbox", "SandboxLog", "SandboxLogs", + "SandboxMetric", "Team", "TeamUser", "Template", diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py index 939aa7caf..14fd2a029 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, TypeVar +from typing import Any, Dict, List, Type, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -25,9 +25,9 @@ class SandboxMetric: mem_total_mi_b: int mem_used_mi_b: int timestamp: datetime.datetime - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: cpu_count = self.cpu_count cpu_pct = self.cpu_pct @@ -38,7 +38,7 @@ def to_dict(self) -> dict[str, Any]: timestamp = self.timestamp.isoformat() - field_dict: dict[str, Any] = {} + field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { @@ -53,7 +53,7 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() cpu_count = d.pop("cpuCount") @@ -77,7 +77,7 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: return sandbox_metric @property - def additional_keys(self) -> list[str]: + def additional_keys(self) -> List[str]: return list(self.additional_properties.keys()) def __getitem__(self, key: str) -> Any: diff --git a/packages/python-sdk/e2b/sandbox/main.py b/packages/python-sdk/e2b/sandbox/main.py index a87cbe07e..539a0ae7f 100644 --- a/packages/python-sdk/e2b/sandbox/main.py +++ b/packages/python-sdk/e2b/sandbox/main.py @@ -22,15 +22,18 @@ class SandboxSetup(ABC): @property @abstractmethod - def connection_config(self) -> ConnectionConfig: ... + def connection_config(self) -> ConnectionConfig: + ... @property @abstractmethod - def envd_api_url(self) -> str: ... + def envd_api_url(self) -> str: + ... @property @abstractmethod - def sandbox_id(self) -> str: ... + def sandbox_id(self) -> str: + ... def _file_url(self, path: Optional[str] = None) -> str: url = urllib.parse.urljoin(self.envd_api_url, ENVD_API_FILES_ROUTE) diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py index f8dafbe0b..dfe34e838 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py @@ -112,11 +112,11 @@ async def _iterate_events( async for event in self._events: if event.event.HasField("data"): if event.event.data.stdout: - out = event.event.data.stdout.decode('utf-8', 'replace') + out = event.event.data.stdout.decode("utf-8", "replace") self._stdout += out yield out, None, None if event.event.data.stderr: - out = event.event.data.stderr.decode('utf-8', 'replace') + out = event.event.data.stderr.decode("utf-8", "replace") self._stderr += out yield None, out, None if event.event.data.pty: diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py index 331d7b80e..9d177da30 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py @@ -65,11 +65,11 @@ def _handle_events( for event in self._events: if event.event.HasField("data"): if event.event.data.stdout: - out = event.event.data.stdout.decode('utf-8', 'replace') + out = event.event.data.stdout.decode("utf-8", "replace") self._stdout += out yield out, None, None if event.event.data.stderr: - out = event.event.data.stderr.decode('utf-8', 'replace') + out = event.event.data.stderr.decode("utf-8", "replace") self._stderr += out yield None, out, None if event.event.data.pty: diff --git a/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py b/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py index d8f855482..f871ebdc6 100644 --- a/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py +++ b/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py @@ -18,16 +18,20 @@ async def test_run_with_special_characters(async_sandbox: AsyncSandbox): cmd = await async_sandbox.commands.run(f'echo "{text}"') assert cmd.exit_code == 0 - # assert cmd.stdout == f"{text}\n" + + +# assert cmd.stdout == f"{text}\n" + async def test_run_with_broken_utf8(async_sandbox: AsyncSandbox): # Create a string with 8191 'a' characters followed by the problematic byte 0xe2 - long_str = 'a' * 8191 + '\\xe2' + long_str = "a" * 8191 + "\\xe2" result = await async_sandbox.commands.run(f'printf "{long_str}"') assert result.exit_code == 0 # The broken UTF-8 bytes should be replaced with the Unicode replacement character - assert result.stdout == ('a' * 8191 + '\ufffd') + assert result.stdout == ("a" * 8191 + "\ufffd") + async def test_run_with_multiline_string(async_sandbox: AsyncSandbox): text = "Hello,\nWorld!" diff --git a/packages/python-sdk/tests/sync/sandbox_sync/commands/test_run.py b/packages/python-sdk/tests/sync/sandbox_sync/commands/test_run.py index 67072cd78..dc4e44d6b 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/commands/test_run.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/commands/test_run.py @@ -20,14 +20,16 @@ def test_run_with_special_characters(sandbox: Sandbox): assert cmd.exit_code == 0 assert cmd.stdout == f"{text}\n" + def test_run_with_broken_utf8(sandbox: Sandbox): # Create a string with 8191 'a' characters followed by the problematic byte 0xe2 - long_str = 'a' * 8191 + '\\xe2' + long_str = "a" * 8191 + "\\xe2" result = sandbox.commands.run(f'printf "{long_str}"') assert result.exit_code == 0 # The broken UTF-8 bytes should be replaced with the Unicode replacement character - assert result.stdout == ('a' * 8191 + '\ufffd') + assert result.stdout == ("a" * 8191 + "\ufffd") + def test_run_with_multiline_string(sandbox): text = "Hello,\nWorld!" From f2ab7b133e9ab8b14c17f9e9673053938b8be5cb Mon Sep 17 00:00:00 2001 From: 0div Date: Wed, 22 Jan 2025 17:29:08 -0800 Subject: [PATCH 21/39] add missing gen files --- packages/python-sdk/e2b/api/client/models/__init__.py | 2 ++ packages/python-sdk/e2b/sandbox/sandbox_api.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index ec61ba395..4f8bae902 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -13,6 +13,7 @@ from .sandbox import Sandbox from .sandbox_log import SandboxLog from .sandbox_logs import SandboxLogs +from .sandbox_metric import SandboxMetric from .team import Team from .team_user import TeamUser from .template import Template @@ -35,6 +36,7 @@ "Sandbox", "SandboxLog", "SandboxLogs", + "SandboxMetric", "Team", "TeamUser", "Template", diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index 2f96f4510..bcfd2bfc3 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -32,9 +32,9 @@ class SandboxMetrics: """CPU usage in percentage.""" cpu_count: int """Number of CPU cores.""" - mem_mib_used: int + mem_used_mib: int """Memory usage in bytes.""" - mem_mib_total: int + mem_total_mib: int """Total memory available""" From 41eb007f05e72a9c5e804ab293a286c0b01009a8 Mon Sep 17 00:00:00 2001 From: 0div Date: Wed, 22 Jan 2025 18:02:36 -0800 Subject: [PATCH 22/39] fix get_metrics tests in python-sdk --- packages/python-sdk/e2b/sandbox_sync/main.py | 4 ++-- packages/python-sdk/e2b/sandbox_sync/sandbox_api.py | 4 ++-- .../tests/async/sandbox_async/test_metrics.py | 7 +++---- .../python-sdk/tests/sync/sandbox_sync/test_metrics.py | 10 +++++++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 4d2b9094c..6d149c20e 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -356,8 +356,8 @@ def set_timeout( # type: ignore **self.connection_config.__dict__, ) - @class_method_variant("_get_metrics") - async def get_metrics( # type: ignore + @class_method_variant("_cls_get_metrics") + def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 1ff476916..81e032a27 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -175,8 +175,8 @@ def _cls_get_metrics( timestamp=metric.timestamp, cpu_pct=metric.cpu_pct, cpu_count=metric.cpu_count, - mem_mib_used=metric.mem_mi_b_used, - mem_mib_total=metric.mem_mi_b_total, + mem_used_mib=metric.mem_used_mi_b, + mem_total_mib=metric.mem_total_mi_b, ) for metric in res.parsed ] diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py index d32b7b98d..247c2a381 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py @@ -1,14 +1,13 @@ +import asyncio import pytest from e2b import AsyncSandbox @pytest.mark.skip_debug() async def test_get_metrics(async_sandbox: AsyncSandbox): - metrics = await async_sandbox.get_metrics() - - import asyncio - await asyncio.sleep(10) + await asyncio.sleep(2) + metrics = await async_sandbox.get_metrics() assert len(metrics) > 0 assert metrics[0].cpu_pct is not None diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py index a2b977cdd..29df6aa23 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py @@ -1,11 +1,15 @@ import pytest +import time +from e2b import Sandbox @pytest.mark.skip_debug() -async def test_get_metrics(sandbox): +def test_get_metrics(sandbox: Sandbox): + time.sleep(5) + metrics = sandbox.get_metrics() assert len(metrics) > 0 assert metrics[0].cpu_pct is not None assert metrics[0].cpu_count is not None - assert metrics[0].mem_mib_used is not None - assert metrics[0].mem_mib_total is not None + assert metrics[0].mem_used_mib is not None + assert metrics[0].mem_total_mib is not None From f25519f7f5615a257415ab3d14bbcde1e5cc748a Mon Sep 17 00:00:00 2001 From: 0div Date: Wed, 22 Jan 2025 18:25:16 -0800 Subject: [PATCH 23/39] update sleep time in python sdk metrics test --- packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py index 29df6aa23..19d938b6f 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py @@ -5,7 +5,7 @@ @pytest.mark.skip_debug() def test_get_metrics(sandbox: Sandbox): - time.sleep(5) + time.sleep(2) metrics = sandbox.get_metrics() assert len(metrics) > 0 From 5da2d4f921798bb2abf385b4c77c421a8c24b8fd Mon Sep 17 00:00:00 2001 From: 0div Date: Wed, 22 Jan 2025 18:41:47 -0800 Subject: [PATCH 24/39] adress PR comments --- packages/cli/src/commands/sandbox/logs.ts | 2 +- packages/cli/src/commands/sandbox/metrics.ts | 35 ++----------------- .../async/sandbox_async/commands/test_run.py | 3 +- spec/openapi.yml | 1 - 4 files changed, 4 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/commands/sandbox/logs.ts b/packages/cli/src/commands/sandbox/logs.ts index 28f916357..b4849daf4 100644 --- a/packages/cli/src/commands/sandbox/logs.ts +++ b/packages/cli/src/commands/sandbox/logs.ts @@ -15,7 +15,7 @@ function getShortID(sandboxID: string) { return sandboxID.split('-')[0] } -function waitForSandboxEnd(sandboxID: string) { +export function waitForSandboxEnd(sandboxID: string) { let isRunning = true async function monitor() { diff --git a/packages/cli/src/commands/sandbox/metrics.ts b/packages/cli/src/commands/sandbox/metrics.ts index a564f23c0..3713e7596 100644 --- a/packages/cli/src/commands/sandbox/metrics.ts +++ b/packages/cli/src/commands/sandbox/metrics.ts @@ -8,6 +8,7 @@ import { asBold, asTimestamp, withUnderline } from 'src/utils/format' import { wait } from 'src/utils/wait' import { handleE2BRequestError } from '../../utils/errors' import { listSandboxes } from './list' +import { waitForSandboxEnd } from './logs' const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds @@ -15,39 +16,6 @@ function getShortID(sandboxID: string) { return sandboxID.split('-')[0] } -function waitForSandboxEnd(sandboxID: string) { - let isRunning = true - - async function monitor() { - const startTime = new Date().getTime() - - // eslint-disable-next-line no-constant-condition - while (true) { - const currentTime = new Date().getTime() - const elapsedTime = currentTime - startTime // Time elapsed in milliseconds - - // Check if 24 hours (in milliseconds) have passed - if (elapsedTime >= maxRuntime) { - break - } - - const response = await listSandboxes() - const sandbox = response.find( - (s) => s.sandboxID === getShortID(sandboxID) - ) - if (!sandbox) { - isRunning = false - break - } - await wait(5000) - } - } - - monitor() - - return () => isRunning -} - function formatEnum(e: { [key: string]: string }) { return Object.values(e) .map((level) => asBold(level)) @@ -183,6 +151,7 @@ function printMetric( delete metric['service'] delete metric['envID'] delete metric['sandboxID'] + delete metric['logger'] if (format === LogFormat.JSON) { console.log( diff --git a/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py b/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py index f871ebdc6..877ca049c 100644 --- a/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py +++ b/packages/python-sdk/tests/async/sandbox_async/commands/test_run.py @@ -19,8 +19,7 @@ async def test_run_with_special_characters(async_sandbox: AsyncSandbox): assert cmd.exit_code == 0 - -# assert cmd.stdout == f"{text}\n" + assert cmd.stdout == f"{text}\n" async def test_run_with_broken_utf8(async_sandbox: AsyncSandbox): diff --git a/spec/openapi.yml b/spec/openapi.yml index 18b80ce72..8e4412ecf 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -714,7 +714,6 @@ paths: $ref: "#/components/responses/404" "500": $ref: "#/components/responses/500" - format: int32 /sandboxes/{sandboxID}/metrics: get: From 9282976628f6a3a087092888993e49ea980ed6c0 Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Wed, 22 Jan 2025 20:50:48 -0800 Subject: [PATCH 25/39] Add envd version --- packages/js-sdk/src/envd/api.ts | 9 +++++- packages/js-sdk/src/sandbox/index.ts | 28 +++++++++++-------- packages/js-sdk/src/sandbox/sandboxApi.ts | 16 +++++++---- packages/python-sdk/e2b/api/__init__.py | 7 +++++ packages/python-sdk/e2b/sandbox_async/main.py | 16 +++++++---- .../e2b/sandbox_async/sandbox_api.py | 13 +++++---- packages/python-sdk/e2b/sandbox_sync/main.py | 6 +++- .../e2b/sandbox_sync/sandbox_api.py | 13 +++++---- 8 files changed, 75 insertions(+), 33 deletions(-) diff --git a/packages/js-sdk/src/envd/api.ts b/packages/js-sdk/src/envd/api.ts index 0ae3a6389..7f55bf7eb 100644 --- a/packages/js-sdk/src/envd/api.ts +++ b/packages/js-sdk/src/envd/api.ts @@ -95,12 +95,19 @@ export async function handleWatchDirStartEvent( class EnvdApiClient { readonly api: ReturnType> + readonly version: string | undefined - constructor(config: Pick) { + constructor( + config: Pick, + metadata: { + version?: string + } + ) { this.api = createClient({ baseUrl: config.apiUrl, // keepalive: true, // TODO: Return keepalive }) + this.version = metadata.version if (config.logger) { this.api.use(createApiLogger(config.logger)) diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 37edbcae6..ac8249d4f 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -99,6 +99,7 @@ export class Sandbox extends SandboxApi { constructor( opts: Omit & { sandboxId: string + envdVersion?: string } ) { super() @@ -115,10 +116,15 @@ export class Sandbox extends SandboxApi { interceptors: opts?.logger ? [createRpcLogger(opts.logger)] : undefined, }) - this.envdApi = new EnvdApiClient({ - apiUrl: this.envdApiUrl, - logger: opts?.logger, - }) + this.envdApi = new EnvdApiClient( + { + apiUrl: this.envdApiUrl, + logger: opts?.logger, + }, + { + version: opts?.envdVersion + } + ) this.files = new Filesystem( rpcTransport, this.envdApi, @@ -177,16 +183,16 @@ export class Sandbox extends SandboxApi { const config = new ConnectionConfig(sandboxOpts) - const sandboxId = config.debug - ? 'debug_sandbox_id' - : await this.createSandbox( + 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 - ) - - const sbx = new this({ sandboxId, ...config }) as InstanceType - return sbx + ) + return new this({ ...sandbox, ...config }) as InstanceType + } } /** diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 1cf997f4d..edddc15e2 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -159,7 +159,10 @@ export class SandboxApi { metadata?: Record envs?: Record } - ): Promise { + ): Promise<{ + sandboxId: string + envdVersion: string + }> { const config = new ConnectionConfig(opts) const client = new ApiClient(config) @@ -191,10 +194,13 @@ export class SandboxApi { 'You can do this by running `e2b template build` in the directory with the template.' ) } - return this.getSandboxId({ - sandboxId: res.data!.sandboxID, - clientId: res.data!.clientID, - }) + return { + sandboxId: this.getSandboxId({ + sandboxId: res.data!.sandboxID, + clientId: res.data!.clientID, + }), + envdVersion: res.data!.envdVersion + } } private static timeoutToSeconds(timeout: number): number { diff --git a/packages/python-sdk/e2b/api/__init__.py b/packages/python-sdk/e2b/api/__init__.py index 665c3dc78..2d5aae547 100644 --- a/packages/python-sdk/e2b/api/__init__.py +++ b/packages/python-sdk/e2b/api/__init__.py @@ -1,5 +1,6 @@ import json import logging +from dataclasses import dataclass from typing import Optional, Union from httpx import HTTPTransport, AsyncHTTPTransport @@ -17,6 +18,12 @@ logger = logging.getLogger(__name__) +@dataclass +class SandboxCreateResponse: + sandbox_id: str + envd_version: str + + def handle_api_exception(e: Response): try: body = json.loads(e.content) if e.content else {} diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 066162d85..ae6a5c597 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -31,6 +31,7 @@ async def handle_async_request(self, request): class AsyncSandboxOpts(TypedDict): sandbox_id: str + envd_version: Optional[str] connection_config: ConnectionConfig @@ -103,6 +104,7 @@ def __init__(self, **opts: Unpack[AsyncSandboxOpts]): self._connection_config = opts["connection_config"] self._envd_api_url = f"{'http' if self.connection_config.debug else 'https'}://{self.get_host(self.envd_port)}" + self._envd_version = opts["envd_version"] self._transport = AsyncTransportWithLogger(limits=self._limits) self._envd_api = httpx.AsyncClient( @@ -198,10 +200,11 @@ async def create( request_timeout=request_timeout, ) - sandbox_id = ( - "debug_sandbox_id" - if connection_config.debug - else await SandboxApi._create_sandbox( + if connection_config.debug: + sandbox_id = "debug_sandbox_id" + envd_version = None + else: + response = await SandboxApi._create_sandbox( template=template or cls.default_template, api_key=api_key, timeout=timeout or cls.default_sandbox_timeout, @@ -211,10 +214,12 @@ async def create( request_timeout=request_timeout, env_vars=envs, ) - ) + sandbox_id = response.sandbox_id + envd_version = response.envd_version return cls( sandbox_id=sandbox_id, + envd_version=envd_version, connection_config=connection_config, ) @@ -251,6 +256,7 @@ async def connect( return cls( sandbox_id=sandbox_id, + envd_version=None, connection_config=connection_config, ) diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index ea99105cc..f3f30f31d 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -3,7 +3,7 @@ from e2b.sandbox.sandbox_api import SandboxInfo, SandboxApiBase from e2b.exceptions import TemplateException -from e2b.api import AsyncApiClient +from e2b.api import AsyncApiClient, SandboxCreateResponse from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody from e2b.api.client.api.sandboxes import ( post_sandboxes_sandbox_id_timeout, @@ -142,7 +142,7 @@ async def _create_sandbox( domain: Optional[str] = None, debug: Optional[bool] = None, request_timeout: Optional[float] = None, - ) -> str: + ) -> SandboxCreateResponse: config = ConnectionConfig( api_key=api_key, domain=domain, @@ -179,9 +179,12 @@ async def _create_sandbox( "You can do this by running `e2b template build` in the directory with the template." ) - return SandboxApi._get_sandbox_id( - res.parsed.sandbox_id, - res.parsed.client_id, + return SandboxCreateResponse( + sandbox_id=SandboxApi._get_sandbox_id( + res.parsed.sandbox_id, + res.parsed.client_id, + ), + envd_version=res.parsed.envd_version, ) @staticmethod diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 2fa54ea86..a7a8a9c0b 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -129,12 +129,14 @@ def __init__( if self.connection_config.debug: self._sandbox_id = "debug_sandbox_id" + self._envd_version = None elif sandbox_id is not None: self._sandbox_id = sandbox_id + self._envd_version = None else: template = template or self.default_template timeout = timeout or self.default_sandbox_timeout - self._sandbox_id = SandboxApi._create_sandbox( + response = SandboxApi._create_sandbox( template=template, api_key=api_key, timeout=timeout, @@ -144,6 +146,8 @@ def __init__( debug=debug, request_timeout=request_timeout, ) + self._sandbox_id = response.sandbox_id + self._envd_version = response.envd_version self._envd_api_url = f"{'http' if self.connection_config.debug else 'https'}://{self.get_host(self.envd_port)}" diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 8e37aab02..f7557b694 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -4,7 +4,7 @@ from e2b.sandbox.sandbox_api import SandboxInfo, SandboxApiBase from e2b.exceptions import TemplateException -from e2b.api import ApiClient +from e2b.api import ApiClient, SandboxCreateResponse from e2b.api.client.models import NewSandbox, PostSandboxesSandboxIDTimeoutBody from e2b.api.client.api.sandboxes import ( post_sandboxes_sandbox_id_timeout, @@ -149,7 +149,7 @@ def _create_sandbox( domain: Optional[str] = None, debug: Optional[bool] = None, request_timeout: Optional[float] = None, - ) -> str: + ) -> SandboxCreateResponse: config = ConnectionConfig( api_key=api_key, domain=domain, @@ -188,7 +188,10 @@ def _create_sandbox( "You can do this by running `e2b template build` in the directory with the template." ) - return SandboxApi._get_sandbox_id( - res.parsed.sandbox_id, - res.parsed.client_id, + return SandboxCreateResponse( + sandbox_id=SandboxApi._get_sandbox_id( + res.parsed.sandbox_id, + res.parsed.client_id, + ), + envd_version=res.parsed.envd_version, ) From 2e5398c4b6c5b7f2de3230f381ca9e84e8be2c7d Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Wed, 22 Jan 2025 20:55:34 -0800 Subject: [PATCH 26/39] Add python tests --- packages/python-sdk/tests/async/sandbox_async/test_create.py | 1 + packages/python-sdk/tests/sync/sandbox_sync/test_create.py | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/python-sdk/tests/async/sandbox_async/test_create.py b/packages/python-sdk/tests/async/sandbox_async/test_create.py index f4a75c9fa..2574d5d17 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_create.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_create.py @@ -8,6 +8,7 @@ async def test_start(template): sbx = await AsyncSandbox.create(template, timeout=5) try: assert await sbx.is_running() + assert sbx._envd_version is not None finally: await sbx.kill() diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index 18d5f9dba..56450138f 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -8,6 +8,7 @@ def test_start(template): sbx = Sandbox(template, timeout=5) try: assert sbx.is_running() + assert sbx._envd_version is not None finally: sbx.kill() From 323c22183293e9a91bf0123ff01eb511e61ef6a9 Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Wed, 22 Jan 2025 20:57:43 -0800 Subject: [PATCH 27/39] Add TS test --- packages/js-sdk/tests/sandbox/create.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/js-sdk/tests/sandbox/create.test.ts b/packages/js-sdk/tests/sandbox/create.test.ts index 934f6a30b..47a9b371e 100644 --- a/packages/js-sdk/tests/sandbox/create.test.ts +++ b/packages/js-sdk/tests/sandbox/create.test.ts @@ -7,6 +7,8 @@ test.skipIf(isDebug)('create', async () => { const sbx = await Sandbox.create(template, { timeoutMs: 5_000 }) try { const isRunning = await sbx.isRunning() + // @ts-ignore It's only for testing + assert.isDefined(sbx.envdApi.version) assert.isTrue(isRunning) } finally { await sbx.kill() From ef11a4916f502a7daa82e07b1cd05d1daf438789 Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 13:43:36 -0800 Subject: [PATCH 28/39] adress PR comments; check envd version when calling get metrics --- packages/cli/src/commands/sandbox/logs.ts | 6 ++--- packages/cli/src/commands/sandbox/metrics.ts | 12 +-------- packages/js-sdk/src/sandbox/index.ts | 27 +++++++++++++------ packages/js-sdk/tests/sandbox/metrics.test.ts | 2 +- packages/python-sdk/e2b/sandbox_async/main.py | 7 ++++- packages/python-sdk/e2b/sandbox_sync/main.py | 6 +++++ 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/commands/sandbox/logs.ts b/packages/cli/src/commands/sandbox/logs.ts index b4849daf4..52bc75713 100644 --- a/packages/cli/src/commands/sandbox/logs.ts +++ b/packages/cli/src/commands/sandbox/logs.ts @@ -1,13 +1,13 @@ +import * as chalk from 'chalk' import * as commander from 'commander' import * as e2b from 'e2b' import * as util from 'util' -import * as chalk from 'chalk' import { client, connectionConfig } from 'src/api' import { asBold, asTimestamp, withUnderline } from 'src/utils/format' -import { listSandboxes } from './list' import { wait } from 'src/utils/wait' import { handleE2BRequestError } from '../../utils/errors' +import { listSandboxes } from './list' const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds @@ -87,7 +87,7 @@ enum LogFormat { PRETTY = 'pretty', } -function cleanLogger(logger?: string) { +export function cleanLogger(logger?: string) { if (!logger) { return '' } diff --git a/packages/cli/src/commands/sandbox/metrics.ts b/packages/cli/src/commands/sandbox/metrics.ts index 3713e7596..5ae5c417e 100644 --- a/packages/cli/src/commands/sandbox/metrics.ts +++ b/packages/cli/src/commands/sandbox/metrics.ts @@ -8,9 +8,7 @@ import { asBold, asTimestamp, withUnderline } from 'src/utils/format' import { wait } from 'src/utils/wait' import { handleE2BRequestError } from '../../utils/errors' import { listSandboxes } from './list' -import { waitForSandboxEnd } from './logs' - -const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds +import { cleanLogger, waitForSandboxEnd } from './logs' function getShortID(sandboxID: string) { return sandboxID.split('-')[0] @@ -27,14 +25,6 @@ enum LogFormat { PRETTY = 'pretty', } -function cleanLogger(logger?: string) { - if (!logger) { - return '' - } - - return logger.replaceAll('Svc', '') -} - export const metricsCommand = new commander.Command('metrics') .description('show metrics for sandbox') .argument( diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index eefd3af92..157cc20d4 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -1,5 +1,6 @@ import { createConnectTransport } from '@connectrpc/connect-web' +import { compareVersions } from 'compare-versions' import { components } from '../api' import { ConnectionConfig, @@ -11,7 +12,6 @@ import { createRpcLogger } from '../logs' import { Commands, Pty } from './commands' import { Filesystem } from './filesystem' import { SandboxApi } from './sandboxApi' - /** * Options for creating a new Sandbox. */ @@ -123,7 +123,7 @@ export class Sandbox extends SandboxApi { logger: opts?.logger, }, { - version: opts?.envdVersion + version: opts?.envdVersion, } ) this.files = new Filesystem( @@ -185,12 +185,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 } @@ -296,6 +299,14 @@ export class Sandbox extends SandboxApi { async getMetrics( opts?: Pick ): Promise { + if ( + this.envdApi.version && + compareVersions(this.envdApi.version, '0.1.5') < 0 + ) { + throw new Error( + 'Metrics are not supported in this version of the sandbox, please update to latest version' + ) + } return await Sandbox.getMetrics(this.sandboxId, { ...this.connectionConfig, ...opts, diff --git a/packages/js-sdk/tests/sandbox/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts index 89d8175a9..3baeeabbe 100644 --- a/packages/js-sdk/tests/sandbox/metrics.test.ts +++ b/packages/js-sdk/tests/sandbox/metrics.test.ts @@ -1,6 +1,6 @@ import { assert } from 'vitest' -import { wait, sandboxTest } from '../setup.js' +import { sandboxTest, wait } from '../setup.js' sandboxTest('get sandbox metrics', async ({ sandbox }) => { console.log('Getting metrics for sandbox ID:', sandbox.sandboxId) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 43c81fd27..44211e29e 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -4,13 +4,14 @@ import httpx from e2b.connection_config import ConnectionConfig from e2b.envd.api import ENVD_API_HEALTH_ROUTE, ahandle_envd_api_exception -from e2b.exceptions import format_request_timeout_error +from e2b.exceptions import SandboxException, format_request_timeout_error from e2b.sandbox.main import SandboxSetup 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.sandbox_api import SandboxApi, SandboxMetrics +from packaging.version import Version from typing_extensions import Unpack logger = logging.getLogger(__name__) @@ -375,6 +376,10 @@ async def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: + if Version(self._envd_version) < Version("0.1.5"): + raise SandboxException( + "Metrics are not supported in this version of the sandbox, please update to latest version" + ) config_dict = self.connection_config.__dict__ config_dict.pop("access_token", None) config_dict.pop("api_url", None) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 6532544a0..b810a28b0 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -11,6 +11,7 @@ from e2b.sandbox_sync.commands.pty import Pty from e2b.sandbox_sync.filesystem.filesystem import Filesystem from e2b.sandbox_sync.sandbox_api import SandboxApi, SandboxMetrics +from packaging.version import Version logger = logging.getLogger(__name__) @@ -365,6 +366,11 @@ def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: + if Version(self._envd_version) < Version("0.1.5"): + raise SandboxException( + "Metrics are not supported in this version of the sandbox, please update to latest version" + ) + config_dict = self.connection_config.__dict__ config_dict.pop("access_token", None) config_dict.pop("api_url", None) From fba984101322a274033bcfedd37293fb69917d7a Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 14:14:30 -0800 Subject: [PATCH 29/39] adress nits --- packages/cli/src/commands/sandbox/metrics.ts | 1 - packages/js-sdk/src/sandbox/index.ts | 2 +- packages/js-sdk/tests/sandbox/metrics.test.ts | 1 - packages/python-sdk/e2b/sandbox_async/main.py | 2 +- packages/python-sdk/e2b/sandbox_sync/main.py | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/sandbox/metrics.ts b/packages/cli/src/commands/sandbox/metrics.ts index 5ae5c417e..a1fbaee7b 100644 --- a/packages/cli/src/commands/sandbox/metrics.ts +++ b/packages/cli/src/commands/sandbox/metrics.ts @@ -109,7 +109,6 @@ export const metricsCommand = new commander.Command('metrics') const lastMetric = metrics.length > 0 ? metrics[metrics.length - 1] : undefined if (lastMetric) { - // TODO: Use the timestamp from the last metric instead of the current time? start = new Date(lastMetric.timestamp).getTime() + 1 } diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 157cc20d4..92b708cf8 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -304,7 +304,7 @@ export class Sandbox extends SandboxApi { compareVersions(this.envdApi.version, '0.1.5') < 0 ) { throw new Error( - 'Metrics are not supported in this version of the sandbox, please update to latest version' + 'Metrics are not supported in this version of the sandbox, please rebuild your template.' ) } return await Sandbox.getMetrics(this.sandboxId, { diff --git a/packages/js-sdk/tests/sandbox/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts index 3baeeabbe..e5ab7d1c2 100644 --- a/packages/js-sdk/tests/sandbox/metrics.test.ts +++ b/packages/js-sdk/tests/sandbox/metrics.test.ts @@ -9,7 +9,6 @@ sandboxTest('get sandbox metrics', async ({ sandbox }) => { const metrics = await sandbox.getMetrics() - console.log('Metrics:', metrics) assert.isAtLeast(metrics.length, 1) assert.isAtLeast(metrics[0]?.cpuPct, 0) assert.isAtLeast(metrics[0]?.memTotalMiB, 0) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 44211e29e..b9670dcc2 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -378,7 +378,7 @@ async def get_metrics( # type: ignore ) -> List[SandboxMetrics]: if Version(self._envd_version) < Version("0.1.5"): raise SandboxException( - "Metrics are not supported in this version of the sandbox, please update to latest version" + "Metrics are not supported in this version of the sandbox, please rebuild your template." ) config_dict = self.connection_config.__dict__ config_dict.pop("access_token", None) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index b810a28b0..4f9534553 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -368,7 +368,7 @@ def get_metrics( # type: ignore ) -> List[SandboxMetrics]: if Version(self._envd_version) < Version("0.1.5"): raise SandboxException( - "Metrics are not supported in this version of the sandbox, please update to latest version" + "Metrics are not supported in this version of the sandbox, please rebuild your template." ) config_dict = self.connection_config.__dict__ From 4605a62ab7fb99e148818b9e3a725c225ae7f342 Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 15:00:30 -0800 Subject: [PATCH 30/39] overload get_metircs with staticmethod --- packages/python-sdk/e2b/sandbox_async/main.py | 20 +++++++++++++++++++ packages/python-sdk/e2b/sandbox_sync/main.py | 20 +++++++++++++++++++ .../tests/async/sandbox_async/test_metrics.py | 11 +++++++++- .../tests/sync/sandbox_sync/test_metrics.py | 13 ++++++++++-- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index b9670dcc2..380c63254 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -371,6 +371,26 @@ async def set_timeout( # type: ignore **self.connection_config.__dict__, ) + @overload + @staticmethod + async def get_metrics( + sandbox_id: str, + api_key: Optional[str] = None, + domain: Optional[str] = None, + debug: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> List[SandboxMetrics]: + """ + Get the metrics of the sandbox specified by sandbox ID. + + :param sandbox_id: Sandbox ID + :param api_key: E2B API Key to use for authentication, defaults to `E2B_API_KEY` environment variable + :param request_timeout: Timeout for the request in **seconds** + + :return: List of sandbox metrics containing CPU and memory usage information + """ + ... + @class_method_variant("_cls_get_metrics") async def get_metrics( # type: ignore self, diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 4f9534553..f4ad5bcb8 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -361,6 +361,26 @@ def set_timeout( # type: ignore **self.connection_config.__dict__, ) + @overload + @staticmethod + def get_metrics( + sandbox_id: str, + api_key: Optional[str] = None, + domain: Optional[str] = None, + debug: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> List[SandboxMetrics]: + """ + Get the metrics of the sandbox specified by sandbox ID. + + :param sandbox_id: Sandbox ID + :param api_key: E2B API Key to use for authentication, defaults to `E2B_API_KEY` environment variable + :param request_timeout: Timeout for the request in **seconds** + + :return: List of sandbox metrics containing CPU and memory usage information + """ + ... + @class_method_variant("_cls_get_metrics") def get_metrics( # type: ignore self, diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py index 247c2a381..8b68d3ee9 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py @@ -1,10 +1,11 @@ import asyncio + import pytest from e2b import AsyncSandbox @pytest.mark.skip_debug() -async def test_get_metrics(async_sandbox: AsyncSandbox): +async def test_get_metrics(async_sandbox): await asyncio.sleep(2) metrics = await async_sandbox.get_metrics() @@ -14,3 +15,11 @@ async def test_get_metrics(async_sandbox: AsyncSandbox): assert metrics[0].cpu_count is not None assert metrics[0].mem_used_mib is not None assert metrics[0].mem_total_mib is not None + + # test static method + metrics2 = await AsyncSandbox.get_metrics(async_sandbox.sandbox_id) + assert len(metrics2) > 0 + assert metrics2[0].cpu_pct is not None + assert metrics2[0].cpu_count is not None + assert metrics2[0].mem_used_mib is not None + assert metrics2[0].mem_total_mib is not None diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py index 19d938b6f..e0809b6a8 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py @@ -1,10 +1,11 @@ -import pytest import time +import pytest from e2b import Sandbox + @pytest.mark.skip_debug() -def test_get_metrics(sandbox: Sandbox): +def test_get_metrics(sandbox): time.sleep(2) metrics = sandbox.get_metrics() @@ -13,3 +14,11 @@ def test_get_metrics(sandbox: Sandbox): assert metrics[0].cpu_count is not None assert metrics[0].mem_used_mib is not None assert metrics[0].mem_total_mib is not None + + # test static method + metrics2 = Sandbox.get_metrics(sandbox.sandbox_id) + assert len(metrics2) > 0 + assert metrics2[0].cpu_pct is not None + assert metrics2[0].cpu_count is not None + assert metrics2[0].mem_used_mib is not None + assert metrics2[0].mem_total_mib is not None From 9cb16624574c83a982c1fd0b6fdfecc0d3aa08fe Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 15:21:18 -0800 Subject: [PATCH 31/39] add class method test in js-sdk --- packages/js-sdk/src/sandbox/sandboxApi.ts | 2 +- packages/js-sdk/tests/sandbox/metrics.test.ts | 9 ++++++++- packages/python-sdk/e2b/sandbox_async/main.py | 2 +- packages/python-sdk/e2b/sandbox_sync/main.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 15b6cd789..3a278cade 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -237,7 +237,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/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts index e5ab7d1c2..e67070298 100644 --- a/packages/js-sdk/tests/sandbox/metrics.test.ts +++ b/packages/js-sdk/tests/sandbox/metrics.test.ts @@ -1,11 +1,12 @@ import { assert } from 'vitest' +import Sandbox from '../../src/index.js' import { sandboxTest, wait } from '../setup.js' sandboxTest('get sandbox metrics', async ({ sandbox }) => { console.log('Getting metrics for sandbox ID:', sandbox.sandboxId) - await wait(5_000) + await wait(2_000) const metrics = await sandbox.getMetrics() @@ -13,4 +14,10 @@ sandboxTest('get sandbox metrics', async ({ sandbox }) => { assert.isAtLeast(metrics[0]?.cpuPct, 0) assert.isAtLeast(metrics[0]?.memTotalMiB, 0) assert.isAtLeast(metrics[0]?.memUsedMiB, 0) + + const metrics2 = await Sandbox.getMetrics(sandbox.sandboxId) + assert.isAtLeast(metrics2.length, 1) + assert.isAtLeast(metrics2[0]?.cpuPct, 0) + assert.isAtLeast(metrics2[0]?.memTotalMiB, 0) + assert.isAtLeast(metrics2[0]?.memUsedMiB, 0) }) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 380c63254..6d80e5d5f 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -396,7 +396,7 @@ async def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: - if Version(self._envd_version) < Version("0.1.5"): + if self._envd_version and Version(self._envd_version) < Version("0.1.5"): raise SandboxException( "Metrics are not supported in this version of the sandbox, please rebuild your template." ) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index f4ad5bcb8..570d9e3a9 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -386,7 +386,7 @@ def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: - if Version(self._envd_version) < Version("0.1.5"): + if self._envd_version and Version(self._envd_version) < Version("0.1.5"): raise SandboxException( "Metrics are not supported in this version of the sandbox, please rebuild your template." ) From 61d147906ebe8d618fa223a6027481cfa481d436 Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Thu, 23 Jan 2025 15:20:50 -0800 Subject: [PATCH 32/39] Add missing backticks --- packages/python-sdk/e2b/sandbox_sync/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index a7a8a9c0b..363568c96 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -234,6 +234,7 @@ def connect( # Another code block same_sandbox = Sandbox.connect(sandbox_id) + ``` """ return cls( sandbox_id=sandbox_id, From b24a39e67bf82917b852d3793024bd8d70ef35f4 Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 15:29:56 -0800 Subject: [PATCH 33/39] add missing docstring --- packages/python-sdk/e2b/sandbox_async/main.py | 7 +++++++ packages/python-sdk/e2b/sandbox_sync/main.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 6d80e5d5f..734bd3316 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -396,6 +396,13 @@ async def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: + """ + Get the metrics of the sandbox specified by sandbox ID. + + :param request_timeout: Timeout for the request in **seconds** + + :return: List of sandbox metrics containing CPU and memory usage information + """ if self._envd_version and Version(self._envd_version) < Version("0.1.5"): raise SandboxException( "Metrics are not supported in this version of the sandbox, please rebuild your template." diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 570d9e3a9..b1c951150 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -386,6 +386,13 @@ def get_metrics( # type: ignore self, request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: + """ + Get the metrics of the sandbox specified by sandbox ID. + + :param request_timeout: Timeout for the request in **seconds** + + :return: List of sandbox metrics containing CPU and memory usage information + """ if self._envd_version and Version(self._envd_version) < Version("0.1.5"): raise SandboxException( "Metrics are not supported in this version of the sandbox, please rebuild your template." From 66de628a2103c964f91b843ff10edf17123dbf1c Mon Sep 17 00:00:00 2001 From: 0div <98087403+0div@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:35:52 +0100 Subject: [PATCH 34/39] Update packages/js-sdk/src/sandbox/sandboxApi.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Novák --- packages/js-sdk/src/sandbox/sandboxApi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 3a278cade..3cfe4286a 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -118,7 +118,6 @@ export class SandboxApi { * Get the metrics of the sandbox. * * @param sandboxId sandbox ID. - * @param timeoutMs timeout in **milliseconds**. * @param opts connection options. * * @returns metrics of the sandbox. From c2c80659aa79a67263809ac82111c1a46f7429b8 Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Thu, 23 Jan 2025 16:00:46 -0800 Subject: [PATCH 35/39] Fix docstrings --- packages/python-sdk/e2b/sandbox_async/main.py | 15 ++++++++++++++- packages/python-sdk/e2b/sandbox_sync/main.py | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 1e26013f1..dc4b68260 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -437,6 +437,19 @@ async def pause( return self.sandbox_id + @overload + async def get_metrics( + self, request_timeout: Optional[float] = None + ) -> List[SandboxMetrics]: + """ + Get the metrics of the current sandbox. + + :param request_timeout: Timeout for the request in **seconds** + + :return: List of sandbox metrics containing CPU and memory usage information + """ + ... + @overload @staticmethod async def get_metrics( @@ -463,7 +476,7 @@ async def get_metrics( # type: ignore request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: """ - Get the metrics of the sandbox specified by sandbox ID. + Get the metrics of the current sandbox. :param request_timeout: Timeout for the request in **seconds** diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 5c706d8c8..01693d3ee 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -427,6 +427,19 @@ def pause( return self.sandbox_id + @overload + def get_metrics( + self, request_timeout: Optional[float] = None + ) -> List[SandboxMetrics]: + """ + Get the metrics of the current sandbox. + + :param request_timeout: Timeout for the request in **seconds** + + :return: List of sandbox metrics containing CPU and memory usage information + """ + ... + @overload @staticmethod def get_metrics( @@ -453,7 +466,7 @@ def get_metrics( # type: ignore request_timeout: Optional[float] = None, ) -> List[SandboxMetrics]: """ - Get the metrics of the sandbox specified by sandbox ID. + Get the metrics of the current sandbox. :param request_timeout: Timeout for the request in **seconds** From 3055d0e0242ea777e24bdf6c5cae752526490fcb Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 18:39:24 -0800 Subject: [PATCH 36/39] add stress test for getMetrics --- .../js-sdk/tests/integration/stress.test.ts | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/js-sdk/tests/integration/stress.test.ts b/packages/js-sdk/tests/integration/stress.test.ts index 69da1472d..e5177c1bd 100644 --- a/packages/js-sdk/tests/integration/stress.test.ts +++ b/packages/js-sdk/tests/integration/stress.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest' -import Sandbox from '../../src/index.js' +import Sandbox, { components } from '../../src/index.js' import { isIntegrationTest, wait } from '../setup.js' const heavyArray = new ArrayBuffer(256 * 1024 * 1024) // 256 MiB = 256 * 1024 * 1024 bytes @@ -10,7 +10,58 @@ for (let i = 0; i < view.length; i++) { } const integrationTestTemplate = 'integration-test-v1' -const sanboxCount = 10 +const sanboxCount = 1_000 +const batchSize = 10 + +test.skipIf(!isIntegrationTest)( + 'create a bunch of sandboxes and get metrics', + async () => { + const promises: Array> = [] + for (let i = 0; i < sanboxCount; i++) { + promises.push( + Sandbox.create('base', { timeoutMs: 600 }).then(async (sbx) => { + if (await sbx.isRunning()) { + sbx.commands.run('yes', { background: true }) + } else { + console.log('sandbox is not running', sbx.sandboxId) + } + return sbx + }) + ) + } + const sbxs = await Promise.all(promises) + + await wait(10_000) + + for (let i = 0; i < sbxs.length; i++) { + // Process metrics in groups of 10 sandboxes + if (i % batchSize === 0) { + const metricsPromises: Array< + Promise + > = sbxs.slice(i, i + batchSize).map((sbx) => { + if (sbx) return sbx.getMetrics() + return Promise.resolve([]) + }) + const metricsResults = await Promise.all(metricsPromises) + + console.log('metricsResults', metricsResults) + // Log metrics for each sandbox in the group + metricsResults.forEach((metrics, idx) => { + const sbx = sbxs[i + idx] + if (sbx) { + console.log('##### metrics START#####->', i + idx) + console.log('~~~', sbx.sandboxId) + console.log(metrics) + console.log('##### metrics END #####->', i + idx) + } + }) + } + i++ + continue + } + }, + { timeout: 600_000 } +) test.skipIf(!isIntegrationTest)( 'stress test heavy file writes and reads', From fa32e39fdae8732f16c234735b4a49f928c3be8d Mon Sep 17 00:00:00 2001 From: 0div Date: Thu, 23 Jan 2025 19:44:57 -0800 Subject: [PATCH 37/39] update cpuPct to cpuUsedPCt --- packages/js-sdk/tests/sandbox/metrics.test.ts | 4 ++-- .../e2b/api/client/models/sandbox_metric.py | 12 ++++++------ packages/python-sdk/e2b/sandbox/sandbox_api.py | 2 +- packages/python-sdk/e2b/sandbox_async/sandbox_api.py | 4 ++-- packages/python-sdk/e2b/sandbox_sync/sandbox_api.py | 10 ++++------ .../tests/async/sandbox_async/test_metrics.py | 4 ++-- .../tests/sync/sandbox_sync/test_metrics.py | 4 ++-- spec/openapi.yml | 4 ++-- 8 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/js-sdk/tests/sandbox/metrics.test.ts b/packages/js-sdk/tests/sandbox/metrics.test.ts index e67070298..c8643ad01 100644 --- a/packages/js-sdk/tests/sandbox/metrics.test.ts +++ b/packages/js-sdk/tests/sandbox/metrics.test.ts @@ -11,13 +11,13 @@ sandboxTest('get sandbox metrics', async ({ sandbox }) => { const metrics = await sandbox.getMetrics() assert.isAtLeast(metrics.length, 1) - assert.isAtLeast(metrics[0]?.cpuPct, 0) + assert.isAtLeast(metrics[0]?.cpuUsedPct, 0) assert.isAtLeast(metrics[0]?.memTotalMiB, 0) assert.isAtLeast(metrics[0]?.memUsedMiB, 0) const metrics2 = await Sandbox.getMetrics(sandbox.sandboxId) assert.isAtLeast(metrics2.length, 1) - assert.isAtLeast(metrics2[0]?.cpuPct, 0) + assert.isAtLeast(metrics2[0]?.cpuUsedPct, 0) assert.isAtLeast(metrics2[0]?.memTotalMiB, 0) assert.isAtLeast(metrics2[0]?.memUsedMiB, 0) }) diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py index 14fd2a029..2f636976f 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_metric.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_metric.py @@ -14,14 +14,14 @@ class SandboxMetric: Attributes: cpu_count (int): Number of CPU cores - cpu_pct (float): CPU usage percentage + cpu_used_pct (float): CPU usage percentage mem_total_mi_b (int): Total memory in MiB mem_used_mi_b (int): Memory used in MiB timestamp (datetime.datetime): Timestamp of the metric entry """ cpu_count: int - cpu_pct: float + cpu_used_pct: float mem_total_mi_b: int mem_used_mi_b: int timestamp: datetime.datetime @@ -30,7 +30,7 @@ class SandboxMetric: def to_dict(self) -> Dict[str, Any]: cpu_count = self.cpu_count - cpu_pct = self.cpu_pct + cpu_used_pct = self.cpu_used_pct mem_total_mi_b = self.mem_total_mi_b @@ -43,7 +43,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict.update( { "cpuCount": cpu_count, - "cpuPct": cpu_pct, + "cpuUsedPct": cpu_used_pct, "memTotalMiB": mem_total_mi_b, "memUsedMiB": mem_used_mi_b, "timestamp": timestamp, @@ -57,7 +57,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() cpu_count = d.pop("cpuCount") - cpu_pct = d.pop("cpuPct") + cpu_used_pct = d.pop("cpuUsedPct") mem_total_mi_b = d.pop("memTotalMiB") @@ -67,7 +67,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: sandbox_metric = cls( cpu_count=cpu_count, - cpu_pct=cpu_pct, + cpu_used_pct=cpu_used_pct, mem_total_mi_b=mem_total_mi_b, mem_used_mi_b=mem_used_mi_b, timestamp=timestamp, diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index bcfd2bfc3..5030e174c 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -28,7 +28,7 @@ class SandboxMetrics: timestamp: datetime """Timestamp of the metrics.""" - cpu_pct: float + cpu_used_pct: float """CPU usage in percentage.""" cpu_count: int """Number of CPU cores.""" diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index e112b01ae..bcb4f5e63 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -1,5 +1,4 @@ import urllib.parse -from packaging.version import Version from typing import Dict, List, Optional from e2b.api import AsyncApiClient, SandboxCreateResponse, handle_api_exception @@ -20,6 +19,7 @@ from e2b.connection_config import ConnectionConfig from e2b.exceptions import TemplateException from e2b.sandbox.sandbox_api import SandboxApiBase, SandboxInfo, SandboxMetrics +from packaging.version import Version class SandboxApi(SandboxApiBase): @@ -185,7 +185,7 @@ async def _cls_get_metrics( return [ SandboxMetrics( timestamp=metric.timestamp, - cpu_pct=metric.cpu_pct, + cpu_used_pct=metric.cpu_used_pct, cpu_count=metric.cpu_count, mem_used_mib=metric.mem_used_mi_b, mem_total_mib=metric.mem_total_mi_b, diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 3eb67a598..bc49b4efe 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -1,9 +1,6 @@ import urllib.parse from typing import Dict, List, Optional -from httpx import HTTPTransport -from packaging.version import Version - from e2b.api import ApiClient, SandboxCreateResponse, handle_api_exception from e2b.api.client.api.sandboxes import ( delete_sandboxes_sandbox_id, @@ -20,9 +17,10 @@ ResumedSandbox, ) from e2b.connection_config import ConnectionConfig -from e2b.exceptions import TemplateException, NotFoundException +from e2b.exceptions import NotFoundException, TemplateException from e2b.sandbox.sandbox_api import SandboxApiBase, SandboxInfo, SandboxMetrics - +from httpx import HTTPTransport +from packaging.version import Version class SandboxApi(SandboxApiBase): @@ -192,7 +190,7 @@ def _cls_get_metrics( return [ SandboxMetrics( timestamp=metric.timestamp, - cpu_pct=metric.cpu_pct, + cpu_used_pct=metric.cpu_used_pct, cpu_count=metric.cpu_count, mem_used_mib=metric.mem_used_mi_b, mem_total_mib=metric.mem_total_mi_b, diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py index 8b68d3ee9..f1f813442 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py @@ -11,7 +11,7 @@ async def test_get_metrics(async_sandbox): metrics = await async_sandbox.get_metrics() assert len(metrics) > 0 - assert metrics[0].cpu_pct is not None + assert metrics[0].cpu_used_pct is not None assert metrics[0].cpu_count is not None assert metrics[0].mem_used_mib is not None assert metrics[0].mem_total_mib is not None @@ -19,7 +19,7 @@ async def test_get_metrics(async_sandbox): # test static method metrics2 = await AsyncSandbox.get_metrics(async_sandbox.sandbox_id) assert len(metrics2) > 0 - assert metrics2[0].cpu_pct is not None + assert metrics2[0].cpu_used_pct is not None assert metrics2[0].cpu_count is not None assert metrics2[0].mem_used_mib is not None assert metrics2[0].mem_total_mib is not None diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py index e0809b6a8..e15cb75f3 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_metrics.py @@ -10,7 +10,7 @@ def test_get_metrics(sandbox): metrics = sandbox.get_metrics() assert len(metrics) > 0 - assert metrics[0].cpu_pct is not None + assert metrics[0].cpu_used_pct is not None assert metrics[0].cpu_count is not None assert metrics[0].mem_used_mib is not None assert metrics[0].mem_total_mib is not None @@ -18,7 +18,7 @@ def test_get_metrics(sandbox): # test static method metrics2 = Sandbox.get_metrics(sandbox.sandbox_id) assert len(metrics2) > 0 - assert metrics2[0].cpu_pct is not None + assert metrics2[0].cpu_used_pct is not None assert metrics2[0].cpu_count is not None assert metrics2[0].mem_used_mib is not None assert metrics2[0].mem_total_mib is not None diff --git a/spec/openapi.yml b/spec/openapi.yml index d8e3bfce8..c920cf79b 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -427,7 +427,7 @@ components: required: - timestamp - cpuCount - - cpuPct + - cpuUsedPct - memUsedMiB - memTotalMiB properties: @@ -439,7 +439,7 @@ components: type: integer format: int32 description: Number of CPU cores - cpuPct: + cpuUsedPct: type: number format: float description: CPU usage percentage From 58fa7d4ef2f9acec4114a62306e7d0eb2e28e55c Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Thu, 23 Jan 2025 19:51:43 -0800 Subject: [PATCH 38/39] Generate API --- packages/js-sdk/src/api/schema.gen.ts | 3 +- .../sandboxes/delete_sandboxes_sandbox_id.py | 8 ++--- .../post_sandboxes_sandbox_id_pause.py | 8 ++--- .../post_sandboxes_sandbox_id_refreshes.py | 12 ++----- .../post_sandboxes_sandbox_id_timeout.py | 12 ++----- packages/python-sdk/e2b/api/client/client.py | 36 +++++-------------- .../e2b/api/client/models/__init__.py | 4 +-- .../tests/async/sandbox_async/test_metrics.py | 1 - 8 files changed, 21 insertions(+), 63 deletions(-) diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index ef1b5d2b7..e7d120e6e 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -231,7 +231,6 @@ export interface paths { }; 401: components["responses"]["401"]; 404: components["responses"]["404"]; - /** Format: int32 */ 500: components["responses"]["500"]; }; }; @@ -530,7 +529,7 @@ export interface components { * Format: float * @description CPU usage percentage */ - cpuPct: number; + cpuUsedPct: number; /** * Format: int64 * @description Total memory in MiB diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py b/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py index 11b5a4cda..30a419c00 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py @@ -19,9 +19,7 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Any]: +def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -36,9 +34,7 @@ def _parse_response( return None -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Any]: +def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py index e47e05229..8133ed132 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py @@ -19,9 +19,7 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Any]: +def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -38,9 +36,7 @@ def _parse_response( return None -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Any]: +def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py index 39dfc678a..163bcf177 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py @@ -5,9 +5,7 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.post_sandboxes_sandbox_id_refreshes_body import ( - PostSandboxesSandboxIDRefreshesBody, -) +from ...models.post_sandboxes_sandbox_id_refreshes_body import PostSandboxesSandboxIDRefreshesBody from ...types import Response @@ -32,9 +30,7 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Any]: +def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -47,9 +43,7 @@ def _parse_response( return None -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Any]: +def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py index 615963abf..148d97c87 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py @@ -5,9 +5,7 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.post_sandboxes_sandbox_id_timeout_body import ( - PostSandboxesSandboxIDTimeoutBody, -) +from ...models.post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody from ...types import Response @@ -32,9 +30,7 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Any]: +def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: if response.status_code == HTTPStatus.NO_CONTENT: return None if response.status_code == HTTPStatus.UNAUTHORIZED: @@ -49,9 +45,7 @@ def _parse_response( return None -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Any]: +def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, diff --git a/packages/python-sdk/e2b/api/client/client.py b/packages/python-sdk/e2b/api/client/client.py index 38b07d057..63a2493b9 100644 --- a/packages/python-sdk/e2b/api/client/client.py +++ b/packages/python-sdk/e2b/api/client/client.py @@ -38,15 +38,9 @@ class Client: _base_url: str = field(alias="base_url") _cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field( - default=None, kw_only=True, alias="timeout" - ) - _verify_ssl: Union[str, bool, ssl.SSLContext] = field( - default=True, kw_only=True, alias="verify_ssl" - ) - _follow_redirects: bool = field( - default=False, kw_only=True, alias="follow_redirects" - ) + _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") _client: Optional[httpx.Client] = field(default=None, init=False) _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) @@ -174,15 +168,9 @@ class AuthenticatedClient: _base_url: str = field(alias="base_url") _cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field( - default=None, kw_only=True, alias="timeout" - ) - _verify_ssl: Union[str, bool, ssl.SSLContext] = field( - default=True, kw_only=True, alias="verify_ssl" - ) - _follow_redirects: bool = field( - default=False, kw_only=True, alias="follow_redirects" - ) + _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") _client: Optional[httpx.Client] = field(default=None, init=False) _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) @@ -226,9 +214,7 @@ def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": def get_httpx_client(self) -> httpx.Client: """Get the underlying httpx.Client, constructing a new one if not previously set""" if self._client is None: - self._headers[self.auth_header_name] = ( - f"{self.prefix} {self.token}" if self.prefix else self.token - ) + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -249,9 +235,7 @@ def __exit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for internal httpx.Client (see httpx docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client( - self, async_client: httpx.AsyncClient - ) -> "AuthenticatedClient": + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": """Manually the underlying httpx.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. @@ -262,9 +246,7 @@ def set_async_httpx_client( def get_async_httpx_client(self) -> httpx.AsyncClient: """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: - self._headers[self.auth_header_name] = ( - f"{self.prefix} {self.token}" if self.prefix else self.token - ) + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index f0630f3c6..4f8bae902 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -6,9 +6,7 @@ from .node_detail import NodeDetail from .node_status import NodeStatus from .node_status_change import NodeStatusChange -from .post_sandboxes_sandbox_id_refreshes_body import ( - PostSandboxesSandboxIDRefreshesBody, -) +from .post_sandboxes_sandbox_id_refreshes_body import PostSandboxesSandboxIDRefreshesBody from .post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody from .resumed_sandbox import ResumedSandbox from .running_sandbox import RunningSandbox diff --git a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py index f1f813442..dbb0eb98c 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_metrics.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_metrics.py @@ -6,7 +6,6 @@ @pytest.mark.skip_debug() async def test_get_metrics(async_sandbox): - await asyncio.sleep(2) metrics = await async_sandbox.get_metrics() From d920f54d7102905d1214b570f30322694c731c7c Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Thu, 23 Jan 2025 19:53:31 -0800 Subject: [PATCH 39/39] Update spec --- spec/openapi.yml | 109 ++++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/spec/openapi.yml b/spec/openapi.yml index c920cf79b..ca440f363 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -167,6 +167,36 @@ components: items: $ref: "#/components/schemas/SandboxLog" + SandboxMetric: + description: Metric entry with timestamp and line + required: + - timestamp + - cpuCount + - cpuUsedPct + - memUsedMiB + - memTotalMiB + properties: + timestamp: + type: string + format: date-time + description: Timestamp of the metric entry + cpuCount: + type: integer + format: int32 + description: Number of CPU cores + cpuUsedPct: + type: number + format: float + description: CPU usage percentage + memUsedMiB: + type: integer + format: int64 + description: Memory used in MiB + memTotalMiB: + type: integer + format: int64 + description: Total memory in MiB + Sandbox: required: - templateID @@ -422,35 +452,6 @@ components: items: type: string - SandboxMetric: - description: Metric entry with timestamp and line - required: - - timestamp - - cpuCount - - cpuUsedPct - - memUsedMiB - - memTotalMiB - properties: - timestamp: - type: string - format: date-time - description: Timestamp of the metric entry - cpuCount: - type: integer - format: int32 - description: Number of CPU cores - cpuUsedPct: - type: number - format: float - description: CPU usage percentage - memUsedMiB: - type: integer - format: int64 - description: Memory used in MiB - memTotalMiB: - type: integer - format: int64 - description: Total memory in MiB Error: required: @@ -592,6 +593,31 @@ paths: "500": $ref: "#/components/responses/500" + /sandboxes/{sandboxID}/metrics: + get: + description: Get sandbox metrics + tags: [sandboxes] + security: + - ApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + responses: + "200": + description: Successfully returned the sandbox metrics + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SandboxMetric" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /sandboxes/{sandboxID}: get: description: Get a sandbox by id @@ -712,31 +738,6 @@ paths: $ref: "#/components/responses/404" "500": $ref: "#/components/responses/500" - - /sandboxes/{sandboxID}/metrics: - get: - description: Get sandbox metrics - tags: [sandboxes] - security: - - ApiKeyAuth: [] - parameters: - - $ref: "#/components/parameters/sandboxID" - responses: - "200": - description: Successfully returned the sandbox metrics - content: - application/json: - schema: - type: array - items: - type: object - $ref: "#/components/schemas/SandboxMetric" - "404": - $ref: "#/components/responses/404" - "401": - $ref: "#/components/responses/401" - "500": - $ref: "#/components/responses/500" /sandboxes/{sandboxID}/refreshes: post: