From a1dce7828fc468f27f1b8f0fd687540bb35696b1 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 30 Sep 2025 16:48:23 +0200 Subject: [PATCH 01/19] pretty default logs for build --- .changeset/selfish-tools-add.md | 7 + .../cli/src/templates/python-build-async.hbs | 3 +- .../cli/src/templates/python-build-sync.hbs | 3 +- .../cli/src/templates/typescript-build.hbs | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- .../expected/python-async/build_dev.py | 3 +- .../expected/python-async/build_prod.py | 3 +- .../expected/python-sync/build_dev.py | 3 +- .../expected/python-sync/build_prod.py | 3 +- .../expected/typescript/build.dev.ts | 3 +- .../expected/typescript/build.prod.ts | 3 +- packages/js-sdk/package.json | 1 + packages/js-sdk/src/index.ts | 5 + packages/js-sdk/src/template/buildApi.ts | 2 +- packages/js-sdk/src/template/index.ts | 23 +- packages/js-sdk/src/template/logger.ts | 136 ++++++++++ packages/js-sdk/src/template/types.ts | 16 +- packages/js-sdk/tests/setup.ts | 17 +- packages/js-sdk/tests/template/build.test.ts | 8 +- packages/python-sdk/e2b/__init__.py | 8 + packages/python-sdk/e2b/template/logger.py | 136 ++++++++++ packages/python-sdk/e2b/template/types.py | 17 +- .../python-sdk/e2b/template_async/main.py | 235 ++++++++++-------- packages/python-sdk/e2b/template_sync/main.py | 234 +++++++++-------- packages/python-sdk/poetry.lock | 77 +++++- packages/python-sdk/pyproject.toml | 3 +- packages/python-sdk/pyrightconfig.json | 7 + .../tests/async/template_async/test_build.py | 4 +- packages/python-sdk/tests/conftest.py | 18 +- .../tests/sync/template_sync/test_build.py | 4 +- pnpm-lock.yaml | 3 + 66 files changed, 809 insertions(+), 287 deletions(-) create mode 100644 .changeset/selfish-tools-add.md create mode 100644 packages/js-sdk/src/template/logger.ts create mode 100644 packages/python-sdk/e2b/template/logger.py create mode 100644 packages/python-sdk/pyrightconfig.json diff --git a/.changeset/selfish-tools-add.md b/.changeset/selfish-tools-add.md new file mode 100644 index 0000000000..386704b053 --- /dev/null +++ b/.changeset/selfish-tools-add.md @@ -0,0 +1,7 @@ +--- +'@e2b/python-sdk': patch +'e2b': patch +'@e2b/cli': patch +--- + +add template build logs pretty output helper diff --git a/packages/cli/src/templates/python-build-async.hbs b/packages/cli/src/templates/python-build-async.hbs index cc0a2a3a83..c067716bd4 100644 --- a/packages/cli/src/templates/python-build-async.hbs +++ b/packages/cli/src/templates/python-build-async.hbs @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -13,6 +13,7 @@ async def main(): {{#if memoryMB}} memory_mb={{memoryMB}}, {{/if}} + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/src/templates/python-build-sync.hbs b/packages/cli/src/templates/python-build-sync.hbs index 517dcd21b9..233fb3294a 100644 --- a/packages/cli/src/templates/python-build-sync.hbs +++ b/packages/cli/src/templates/python-build-sync.hbs @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -12,4 +12,5 @@ if __name__ == "__main__": {{#if memoryMB}} memory_mb={{memoryMB}}, {{/if}} + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/src/templates/typescript-build.hbs b/packages/cli/src/templates/typescript-build.hbs index 05998e2fcb..345e4a3a96 100644 --- a/packages/cli/src/templates/typescript-build.hbs +++ b/packages/cli/src/templates/typescript-build.hbs @@ -1,4 +1,4 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { @@ -10,6 +10,7 @@ async function main() { {{#if memoryMB}} memoryMB: {{memoryMB}}, {{/if}} + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_dev.py index f69bf19012..64fd36d11b 100644 --- a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="complex-python-app-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_prod.py index c47bc588ae..89c6fbe6df 100644 --- a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="complex-python-app", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_dev.py index 8eefb972cc..6900707219 100644 --- a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="complex-python-app-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_prod.py index 40faea9ecf..c24b743f13 100644 --- a/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/complex-python/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="complex-python-app", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.dev.ts index 70d937e2d3..ab0970e382 100644 --- a/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.dev.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'complex-python-app-dev', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.prod.ts index 4a0c008cbd..f5d7df4119 100644 --- a/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/complex-python/expected/typescript/build.prod.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'complex-python-app', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_dev.py index 45b8806bce..4f84e88b3b 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="copy-test-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_prod.py index bbb7cdce90..c9623f9f77 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="copy-test", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_dev.py index 862806d80b..bfa5130c4b 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="copy-test-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_prod.py index 93a6b4e917..d7e50a8e4d 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="copy-test", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.dev.ts index 813b63eba6..e1322523a8 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.dev.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'copy-test-dev', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.prod.ts index a6170c0192..ea6273eef2 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/build.prod.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'copy-test', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_dev.py index 9dbb697fef..99799c0491 100644 --- a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="custom-app-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_prod.py index 849bbd3a73..8172c99da9 100644 --- a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="custom-app", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_dev.py index 6c80575d0f..eff404af6a 100644 --- a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="custom-app-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_prod.py index e1f9176e58..207bf32839 100644 --- a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="custom-app", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.dev.ts index 5600f1fdf9..745ab073b2 100644 --- a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.dev.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'custom-app-dev', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.prod.ts index 66b3c4b92c..e40611794d 100644 --- a/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/custom-commands/expected/typescript/build.prod.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'custom-app', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_dev.py index 38a2b004d4..c08181d518 100644 --- a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="minimal-template-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_prod.py index 301686b78f..e3b2c97e42 100644 --- a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="minimal-template", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_dev.py index 2efce7de8a..2d71c1c19a 100644 --- a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="minimal-template-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_prod.py index dc16523438..0e79d75d4d 100644 --- a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="minimal-template", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.dev.ts index 35c519ef3b..8c4f9cc336 100644 --- a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.dev.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'minimal-template-dev', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.prod.ts index 10c082c342..758d9dce0b 100644 --- a/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/minimal-dockerfile/expected/typescript/build.prod.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'minimal-template', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_dev.py index 5c138f4814..33af3d7d23 100644 --- a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="multi-stage-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_prod.py index ce0a21ee4e..ebe798da71 100644 --- a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="multi-stage", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_dev.py index f9bbb9570a..59fffa7d9a 100644 --- a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="multi-stage-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_prod.py index 6eabffd163..fc53b30033 100644 --- a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="multi-stage", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.dev.ts index 7592e07b7b..0fdecd176a 100644 --- a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.dev.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'multi-stage-dev', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.prod.ts index 63451d27ed..8dd3e57810 100644 --- a/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/multi-stage/expected/typescript/build.prod.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'multi-stage', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_dev.py index 28fd58fa14..3cb18ece42 100644 --- a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="env-test-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_prod.py index 7c02c0f8bd..37cd47d06b 100644 --- a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -7,6 +7,7 @@ async def main(): await AsyncTemplate.build( template, alias="env-test", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_dev.py index 5e21966c22..63301bafd2 100644 --- a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="env-test-dev", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_prod.py index 34147e0b57..621445029a 100644 --- a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -6,4 +6,5 @@ Template.build( template, alias="env-test", + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.dev.ts index 47d2831781..10c0e292eb 100644 --- a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.dev.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'env-test-dev', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.prod.ts index d8fc65ccf9..df7f440579 100644 --- a/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/multiple-env/expected/typescript/build.prod.ts @@ -1,9 +1,10 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { await Template.build(template, { alias: 'env-test', + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_dev.py b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_dev.py index b0776da759..c0cd96d96a 100644 --- a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_dev.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -9,6 +9,7 @@ async def main(): alias="start-cmd-dev", cpu_count=2, memory_mb=1024, + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_prod.py b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_prod.py index 32534d8f44..8da80c6e4b 100644 --- a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-async/build_prod.py @@ -1,5 +1,5 @@ import asyncio -from e2b import AsyncTemplate +from e2b import AsyncTemplate, default_build_logger from template import template @@ -9,6 +9,7 @@ async def main(): alias="start-cmd", cpu_count=2, memory_mb=1024, + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_dev.py b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_dev.py index 692e7bb810..2241a6c2de 100644 --- a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_dev.py +++ b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_dev.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -8,4 +8,5 @@ alias="start-cmd-dev", cpu_count=2, memory_mb=1024, + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_prod.py b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_prod.py index f6c1b53581..8fd2e2ad73 100644 --- a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_prod.py +++ b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/python-sync/build_prod.py @@ -1,4 +1,4 @@ -from e2b import Template +from e2b import Template, default_build_logger from template import template @@ -8,4 +8,5 @@ alias="start-cmd", cpu_count=2, memory_mb=1024, + on_build_logs=default_build_logger(), ) diff --git a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.dev.ts b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.dev.ts index ad6fa939f6..5872ff5ff7 100644 --- a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.dev.ts +++ b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.dev.ts @@ -1,4 +1,4 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { @@ -6,6 +6,7 @@ async function main() { alias: 'start-cmd-dev', cpuCount: 2, memoryMB: 1024, + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.prod.ts b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.prod.ts index 0d98fc8c1e..ed353067aa 100644 --- a/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.prod.ts +++ b/packages/cli/tests/commands/template/fixtures/start-cmd/expected/typescript/build.prod.ts @@ -1,4 +1,4 @@ -import { Template } from 'e2b' +import { Template, defaultBuildLogger } from 'e2b' import { template } from './template' async function main() { @@ -6,6 +6,7 @@ async function main() { alias: 'start-cmd', cpuCount: 2, memoryMB: 1024, + onBuildLogs: defaultBuildLogger(), }); } diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index ad10a643f9..b3ee56c01b 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -91,6 +91,7 @@ "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", + "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^11.0.3", diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 79af066aaa..762a2dce6b 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -79,4 +79,9 @@ export { type TemplateClass, type TemplateBuilder, type TemplateBase, + LogEntry, + LogEntryStart, + LogEntryEnd, + type LogEntryLevel, + defaultBuildLogger, } from './template' diff --git a/packages/js-sdk/src/template/buildApi.ts b/packages/js-sdk/src/template/buildApi.ts index 86fc534640..c6872def06 100644 --- a/packages/js-sdk/src/template/buildApi.ts +++ b/packages/js-sdk/src/template/buildApi.ts @@ -193,7 +193,7 @@ export async function waitForBuildFinish( }: { templateID: string buildID: string - onBuildLogs?: (logEntry: InstanceType) => void + onBuildLogs?: (logEntry: LogEntry) => void logsRefreshFrequency: number stackTraces: (string | undefined)[] } diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 1fc5f8a9b6..05e39e1353 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -17,6 +17,8 @@ import { Instruction, InstructionType, LogEntry, + LogEntryEnd, + LogEntryStart, RegistryConfig, TemplateBuilder, TemplateFinal, @@ -32,7 +34,15 @@ import { } from './utils' import type { PathLike } from 'node:fs' -export { type TemplateBuilder } from './types' +export { + LogEntry, + LogEntryEnd, + LogEntryStart, + type LogEntryLevel, + type TemplateBuilder, +} from './types' + +export { defaultBuildLogger } from './logger' type TemplateOptions = { fileContextPath?: PathLike @@ -44,7 +54,7 @@ type BasicBuildOptions = { cpuCount?: number memoryMB?: number skipCache?: boolean - onBuildLogs?: (logEntry: InstanceType) => void + onBuildLogs?: (logEntry: LogEntry) => void } export type BuildOptions = BasicBuildOptions & { @@ -67,7 +77,7 @@ export class TemplateBase private forceNextLayer: boolean = false private instructions: Instruction[] = [] private fileContextPath: PathLike = - runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.') + runtime === 'browser' ? '.' : getCallerDirectory(STACK_TRACE_DEPTH) ?? '.' private fileIgnorePatterns: string[] = [] private logsRefreshFrequency: number = 200 private stackTraces: (string | undefined)[] = [] @@ -91,7 +101,12 @@ export class TemplateBase } static build(template: TemplateClass, options: BuildOptions): Promise { - return (template as TemplateBase).build(options) + try { + options.onBuildLogs?.(new LogEntryStart(new Date(), 'Build started')) + return (template as TemplateBase).build(options) + } finally { + options.onBuildLogs?.(new LogEntryEnd(new Date(), 'Build finished')) + } } // Built-in image mixins diff --git a/packages/js-sdk/src/template/logger.ts b/packages/js-sdk/src/template/logger.ts new file mode 100644 index 0000000000..140c6f9bd9 --- /dev/null +++ b/packages/js-sdk/src/template/logger.ts @@ -0,0 +1,136 @@ +import chalk from 'chalk' +import { LogEntry, LogEntryEnd, LogEntryLevel, LogEntryStart } from './types' + +const TIMER_UPDATE_INTERVAL_MS = 150 + +const DEFAULT_LEVEL: LogEntryLevel = 'info' + +const levels: Record = { + error: chalk.red('ERROR'), + warn: chalk.hex('#FF4400')('WARN '), + info: chalk.hex('#FF8800')('INFO '), + debug: chalk.gray('DEBUG'), +} + +// Level ordering for comparison (assuming lower = less severe) +const level_order: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +} + +interface BuildLoggerState { + startTime: number + animationFrame: number + timerInterval: NodeJS.Timeout | undefined +} + +class BuildLogger { + private minLevel: LogEntryLevel + private state: BuildLoggerState + + constructor(minLevel?: LogEntryLevel) { + this.minLevel = minLevel ?? DEFAULT_LEVEL + this.state = this.getInitialState() + } + + logger(logEntry: LogEntry) { + if (logEntry instanceof LogEntryStart) { + this.startTimer() + return + } + + if (logEntry instanceof LogEntryEnd) { + clearInterval(this.state.timerInterval) + return + } + + // Filter by minimum level + if (level_order[logEntry.level] < level_order[this.minLevel]) { + return + } + + const formattedLine = this.formatLogLine(logEntry) + process.stdout.write(`${formattedLine}\n`) + + // Redraw the timer line + this.updateTimer() + } + + private getInitialState(timerInterval?: NodeJS.Timeout): BuildLoggerState { + return { + startTime: Date.now(), + animationFrame: 0, + timerInterval: timerInterval, + } + } + + private formatTimerLine() { + const elapsedSeconds = ((Date.now() - this.state.startTime) / 1000).toFixed( + 1 + ) + return `${elapsedSeconds}s` + } + + private animateStatus() { + const frames = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] + const idx = this.state.animationFrame % frames.length + return `${frames[idx]}` + } + + private formatLogLine(line: LogEntry) { + const timer = this.formatTimerLine().padEnd(5) + + const timestamp = chalk.dim( + line.timestamp.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + ) + + const level = levels[line.level] || levels[DEFAULT_LEVEL] + + const msg = line.message + + return `${timer} | ${timestamp} ${level} ${msg}` + } + + private startTimer() { + if (process.env.CI) { + return + } + + // Start the timer interval + const timerInterval = setInterval( + this.updateTimer.bind(this), + TIMER_UPDATE_INTERVAL_MS + ) + + this.state = this.getInitialState(timerInterval) + + // Initial timer display + this.updateTimer() + } + + private updateTimer() { + if (process.env.CI) { + return + } + + this.state.animationFrame++ + const jumpingSquares = this.animateStatus() + process.stdout.write( + `${jumpingSquares} Building ${this.formatTimerLine()}\r` + ) + } +} + +export function defaultBuildLogger(options?: { + minLevel?: LogEntryLevel +}): (logEntry: LogEntry) => void { + const buildLogger = new BuildLogger(options?.minLevel) + + return buildLogger.logger.bind(buildLogger) +} diff --git a/packages/js-sdk/src/template/types.ts b/packages/js-sdk/src/template/types.ts index e68e157444..b121527886 100644 --- a/packages/js-sdk/src/template/types.ts +++ b/packages/js-sdk/src/template/types.ts @@ -145,10 +145,12 @@ export interface TemplateBuilder { // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface TemplateFinal {} +export type LogEntryLevel = 'debug' | 'info' | 'warn' | 'error' + export class LogEntry { constructor( public readonly timestamp: Date, - public readonly level: 'debug' | 'info' | 'warn' | 'error', + public readonly level: LogEntryLevel, public readonly message: string ) {} @@ -159,6 +161,18 @@ export class LogEntry { } } +export class LogEntryStart extends LogEntry { + constructor(timestamp: Date, message: string) { + super(timestamp, 'debug', message) + } +} + +export class LogEntryEnd extends LogEntry { + constructor(timestamp: Date, message: string) { + super(timestamp, 'debug', message) + } +} + export type GenericDockerRegistry = { type: 'registry' username: string diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index a5d119beca..81264e0a97 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -1,7 +1,7 @@ -import { Sandbox, Template, TemplateClass } from '../src' +import { randomUUID } from 'node:crypto' import { test as base } from 'vitest' +import { LogEntry, Sandbox, Template, TemplateClass } from '../src' import { template } from './template' -import { randomUUID } from 'node:crypto' interface SandboxFixture { sandbox: Sandbox @@ -10,15 +10,24 @@ interface SandboxFixture { } interface BuildTemplateFixture { - buildTemplate: (template: TemplateClass, skipCache?: boolean) => Promise + buildTemplate: ( + template: TemplateClass, + skipCache?: boolean, + onBuildLogs?: (logEntry: LogEntry) => void + ) => Promise } -function buildTemplate(template: TemplateClass, skipCache?: boolean) { +function buildTemplate( + template: TemplateClass, + skipCache?: boolean, + onBuildLogs?: (logEntry: LogEntry) => void +) { return Template.build(template, { alias: randomUUID(), cpuCount: 1, memoryMB: 1024, skipCache: skipCache, + onBuildLogs: onBuildLogs, }) } diff --git a/packages/js-sdk/tests/template/build.test.ts b/packages/js-sdk/tests/template/build.test.ts index 281eacb67f..b7465ded67 100644 --- a/packages/js-sdk/tests/template/build.test.ts +++ b/packages/js-sdk/tests/template/build.test.ts @@ -1,8 +1,8 @@ +import fs from 'node:fs' +import path from 'node:path' import { afterAll, beforeAll } from 'vitest' +import { defaultBuildLogger, Template, waitForTimeout } from '../../src' import { buildTemplateTest } from '../setup' -import { Template, waitForTimeout } from '../../src' -import path from 'node:path' -import fs from 'node:fs' const folderPath = path.join(__dirname, 'folder') @@ -38,7 +38,7 @@ buildTemplateTest( .setWorkdir('/app') .setStartCmd('echo "Hello, world!"', waitForTimeout(10_000)) - await buildTemplate(template) + await buildTemplate(template, undefined, defaultBuildLogger()) } ) diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index 04ae84813d..bfb757636f 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -86,6 +86,9 @@ wait_for_timeout, ) +from .template.types import LogEntry, LogEntryLevel, LogEntryStart, LogEntryEnd +from .template.logger import default_build_logger + __all__ = [ # API "ApiClient", @@ -145,6 +148,11 @@ "wait_for_port", "wait_for_process", "wait_for_timeout", + "LogEntry", + "LogEntryStart", + "LogEntryEnd", + "LogEntryLevel", + "default_build_logger", # MCP "McpServer", ] diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py new file mode 100644 index 0000000000..4a04331733 --- /dev/null +++ b/packages/python-sdk/e2b/template/logger.py @@ -0,0 +1,136 @@ +import os +import threading +import time +from typing import Optional, TypedDict, Callable, Dict + +from rich.console import Console +from rich.style import Style +from rich.text import Text + +from e2b.template.types import LogEntryLevel, LogEntry, LogEntryStart, LogEntryEnd + +TIMER_UPDATE_INTERVAL_MS = 150 + +DEFAULT_LEVEL: LogEntryLevel = "info" + +# Level labels with Rich styles +levels: Dict[LogEntryLevel, tuple[str, Style]] = { + "error": ("ERROR", Style(color="red")), + "warn": ("WARN ", Style(color="#FF4400")), + "info": ("INFO ", Style(color="#FF8800")), + "debug": ("DEBUG", Style(color="bright_black")), +} + +# Level ordering for comparison +level_order = { + "error": 0, + "warn": 1, + "info": 2, + "debug": 3, +} + + +class InitialState(TypedDict): + start_time: float + animation_frame: int + timer: Optional[threading.Timer] + + +class BuildLogger: + __console = Console() + + __min_level: LogEntryLevel + __state: InitialState + + def __init__(self, min_level: Optional[LogEntryLevel] = None): + self.__min_level = min_level if min_level is not None else DEFAULT_LEVEL + self.__reset_initial_state() + + def logger(self, log): + if isinstance(log, LogEntryStart): + self.__start_timer() + return + + if isinstance(log, LogEntryEnd): + if self.__state["timer"] is not None: + self.__state["timer"].cancel() + return + + # Filter by minimum level + if level_order[log.level] < level_order[self.__min_level]: + return + + formatted_line = self.__format_log_line(log) + self.__console.print(formatted_line) + + # Redraw the timer line + self.__update_timer() + + def __reset_initial_state(self, timer: Optional[threading.Timer] = None): + self.__state = { + "start_time": time.time(), + "animation_frame": 0, + "timer": timer, + } + + def __format_timer_line(self) -> str: + elapsed_seconds = time.time() - self.__state["start_time"] + return f"{elapsed_seconds:.1f}s" + + def __animate_status(self) -> str: + frames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"] + idx = self.__state["animation_frame"] % len(frames) + return frames[idx] + + def __format_log_line(self, line: LogEntry) -> Text: + timer = self.__format_timer_line().ljust(5) + timestamp = line.timestamp.strftime("%H:%M:%S") + level_text, level_style = levels.get(line.level, levels[DEFAULT_LEVEL]) + + # Build a rich Text object + text = Text() + text.append(timer) + text.append(" | ") + text.append(timestamp, style="dim") + text.append(" ") + text.append(level_text, style=level_style) + text.append(" ") + text.append(line.message) + + return text + + def __start_timer(self): + if os.getenv("CI"): + return + + # Start the timer interval + timer = threading.Timer(TIMER_UPDATE_INTERVAL_MS / 1000.0, self.__update_timer) + timer.start() + + self.__reset_initial_state(timer) + + # Initial timer display + self.__update_timer() + + def __update_timer(self): + if os.getenv("CI"): + return + + self.__state["animation_frame"] += 1 + jumping_squares = self.__animate_status() + + timer_text = Text() + timer_text.append(jumping_squares) + timer_text.append(" Building ") + timer_text.append(self.__format_timer_line()) + + # Print with carriage return + self.__console.print(timer_text, end="\r") + + +def default_build_logger( + min_level: Optional[LogEntryLevel] = None, +) -> Callable[[LogEntry], None]: + build_logger = BuildLogger(min_level) + + return build_logger.logger diff --git a/packages/python-sdk/e2b/template/types.py b/packages/python-sdk/e2b/template/types.py index 80a046fa5f..6b91b7f0f5 100644 --- a/packages/python-sdk/e2b/template/types.py +++ b/packages/python-sdk/e2b/template/types.py @@ -34,10 +34,13 @@ class Instruction(TypedDict): resolveSymlinks: NotRequired[Optional[bool]] +LogEntryLevel = Literal["debug", "info", "warn", "error"] + + @dataclass class LogEntry: timestamp: datetime - level: Literal["debug", "info", "warn", "error"] + level: LogEntryLevel message: str def __post_init__(self): @@ -47,6 +50,18 @@ def __str__(self) -> str: return f"[{self.timestamp.isoformat()}] [{self.level}] {self.message}" +@dataclass +class LogEntryStart(LogEntry): + def __init__(self, timestamp: datetime, message: str): + super().__init__(timestamp, "debug", message) + + +@dataclass +class LogEntryEnd(LogEntry): + def __init__(self, timestamp: datetime, message: str): + super().__init__(timestamp, "debug", message) + + class GenericDockerRegistry(TypedDict): type: Literal["registry"] username: str diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index 187bcd0598..71b5625167 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -7,7 +7,7 @@ from e2b.connection_config import ConnectionConfig from e2b.api import AsyncApiClient -from e2b.template.types import LogEntry, InstructionType +from e2b.template.types import LogEntry, InstructionType, LogEntryEnd, LogEntryStart from .build_api import ( get_file_upload_link, request_build, @@ -30,142 +30,159 @@ async def build( api_key: Optional[str] = None, domain: Optional[str] = None, ) -> None: - domain = domain or os.environ.get("E2B_DOMAIN", "e2b.dev") - config = ConnectionConfig( - domain=domain, api_key=api_key or os.environ.get("E2B_API_KEY") - ) - client = AsyncApiClient( - config, - require_api_key=True, - require_access_token=False, - limits=TemplateBase._limits, - ) - - if skip_cache: - template._template._force = True - - with client as api_client: - # Create template + try: if on_build_logs: on_build_logs( - LogEntry( + LogEntryStart( timestamp=datetime.now(), - level="info", - message=f"Requesting build for template: {alias}", + message="Build started", ) ) - response = await request_build( - api_client, - name=alias, - cpu_count=cpu_count, - memory_mb=memory_mb, + domain = domain or os.environ.get("E2B_DOMAIN", "e2b.dev") + config = ConnectionConfig( + domain=domain, api_key=api_key or os.environ.get("E2B_API_KEY") + ) + client = AsyncApiClient( + config, + require_api_key=True, + require_access_token=False, + limits=TemplateBase._limits, ) - template_id = response.template_id - build_id = response.build_id - - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message=f"Template created with ID: {template_id}, Build ID: {build_id}", + if skip_cache: + template._template._force = True + + with client as api_client: + # Create template + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Requesting build for template: {alias}", + ) ) + + response = await request_build( + api_client, + name=alias, + cpu_count=cpu_count, + memory_mb=memory_mb, ) - instructions_with_hashes = template._template._instructions_with_hashes() + template_id = response.template_id + build_id = response.build_id + + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Template created with ID: {template_id}, Build ID: {build_id}", + ) + ) - # Upload files - for index, file_upload in enumerate(instructions_with_hashes): - if file_upload["type"] != InstructionType.COPY: - continue + instructions_with_hashes = template._template._instructions_with_hashes() - args = file_upload.get("args", []) - src = args[0] if len(args) > 0 else None - force_upload = file_upload.get("forceUpload") - files_hash = file_upload.get("filesHash", None) - resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS) + # Upload files + for index, file_upload in enumerate(instructions_with_hashes): + if file_upload["type"] != InstructionType.COPY: + continue - if src is None or files_hash is None: - raise ValueError("Source path and files hash are required") + args = file_upload.get("args", []) + src = args[0] if len(args) > 0 else None + force_upload = file_upload.get("forceUpload") + files_hash = file_upload.get("filesHash", None) + resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS) - stack_trace = None - if index + 1 < len(template._template._stack_traces): - stack_trace = template._template._stack_traces[index + 1] + if src is None or files_hash is None: + raise ValueError("Source path and files hash are required") - file_info = await get_file_upload_link( - api_client, template_id, files_hash, stack_trace - ) + stack_trace = None + if index + 1 < len(template._template._stack_traces): + stack_trace = template._template._stack_traces[index + 1] - if (force_upload and file_info.url) or ( - file_info.present is False and file_info.url - ): - await upload_file( - src, - template._template._file_context_path, - file_info.url, - resolve_symlinks, - stack_trace, + file_info = await get_file_upload_link( + api_client, template_id, files_hash, stack_trace ) - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message=f"Uploaded '{src}'", - ) + + if (force_upload and file_info.url) or ( + file_info.present is False and file_info.url + ): + await upload_file( + src, + template._template._file_context_path, + file_info.url, + resolve_symlinks, + stack_trace, ) - else: - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message=f"Skipping upload of '{src}', already cached", + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Uploaded '{src}'", + ) + ) + else: + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Skipping upload of '{src}', already cached", + ) ) - ) - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message="All file uploads completed", + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message="All file uploads completed", + ) ) - ) - # Start build - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message="Starting building...", + # Start build + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message="Starting building...", + ) ) + + await trigger_build( + api_client, + template_id, + build_id, + template._template._serialize(instructions_with_hashes), ) - await trigger_build( - api_client, - template_id, - build_id, - template._template._serialize(instructions_with_hashes), - ) + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message="Waiting for logs...", + ) + ) + await wait_for_build_finish( + api_client, + template_id, + build_id, + on_build_logs, + logs_refresh_frequency=TemplateBase._logs_refresh_frequency, + stack_traces=template._template._stack_traces, + ) + finally: if on_build_logs: on_build_logs( - LogEntry( + LogEntryEnd( timestamp=datetime.now(), - level="info", - message="Waiting for logs...", + message="Build finished", ) - ) - - await wait_for_build_finish( - api_client, - template_id, - build_id, - on_build_logs, - logs_refresh_frequency=TemplateBase._logs_refresh_frequency, - stack_traces=template._template._stack_traces, - ) + ) \ No newline at end of file diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index 671109c7dc..63761541ac 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -2,7 +2,7 @@ from e2b.template.consts import RESOLVE_SYMLINKS from e2b.template.main import TemplateBase, TemplateClass -from e2b.template.types import LogEntry, InstructionType +from e2b.template.types import LogEntry, InstructionType, LogEntryStart, LogEntryEnd import os from datetime import datetime @@ -30,142 +30,160 @@ def build( api_key: Optional[str] = None, domain: Optional[str] = None, ) -> None: - domain = domain or os.environ.get("E2B_DOMAIN", "e2b.dev") - config = ConnectionConfig( - domain=domain, api_key=api_key or os.environ.get("E2B_API_KEY") - ) - client = ApiClient( - config, - require_api_key=True, - require_access_token=False, - limits=TemplateBase._limits, - ) - - if skip_cache: - template._template._force = True - - with client as api_client: - # Create template + try: if on_build_logs: on_build_logs( - LogEntry( + LogEntryStart( timestamp=datetime.now(), - level="info", - message=f"Requesting build for template: {alias}", + message="Build started", ) ) - response = request_build( - api_client, - name=alias, - cpu_count=cpu_count, - memory_mb=memory_mb, + domain = domain or os.environ.get("E2B_DOMAIN", "e2b.dev") + config = ConnectionConfig( + domain=domain, api_key=api_key or os.environ.get("E2B_API_KEY") + ) + client = ApiClient( + config, + require_api_key=True, + require_access_token=False, + limits=TemplateBase._limits, ) - template_id = response.template_id - build_id = response.build_id - - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message=f"Template created with ID: {template_id}, Build ID: {build_id}", + if skip_cache: + template._template._force = True + + with client as api_client: + # Create template + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Requesting build for template: {alias}", + ) ) + + response = request_build( + api_client, + name=alias, + cpu_count=cpu_count, + memory_mb=memory_mb, ) - instructions_with_hashes = template._template._instructions_with_hashes() + template_id = response.template_id + build_id = response.build_id + + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Template created with ID: {template_id}, Build ID: {build_id}", + ) + ) - # Upload files - for index, file_upload in enumerate(instructions_with_hashes): - if file_upload["type"] != InstructionType.COPY: - continue + instructions_with_hashes = template._template._instructions_with_hashes() - args = file_upload.get("args", []) - src = args[0] if len(args) > 0 else None - force_upload = file_upload.get("forceUpload") - files_hash = file_upload.get("filesHash", None) - resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS) + # Upload files + for index, file_upload in enumerate(instructions_with_hashes): + if file_upload["type"] != InstructionType.COPY: + continue - if src is None or files_hash is None: - raise ValueError("Source path and files hash are required") + args = file_upload.get("args", []) + src = args[0] if len(args) > 0 else None + force_upload = file_upload.get("forceUpload") + files_hash = file_upload.get("filesHash", None) + resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS) - stack_trace = None - if index + 1 < len(template._template._stack_traces): - stack_trace = template._template._stack_traces[index + 1] + if src is None or files_hash is None: + raise ValueError("Source path and files hash are required") - file_info = get_file_upload_link( - api_client, template_id, files_hash, stack_trace - ) + stack_trace = None + if index + 1 < len(template._template._stack_traces): + stack_trace = template._template._stack_traces[index + 1] - if (force_upload and file_info.url) or ( - file_info.present is False and file_info.url - ): - upload_file( - src, - template._template._file_context_path, - file_info.url, - resolve_symlinks, - stack_trace, + file_info = get_file_upload_link( + api_client, template_id, files_hash, stack_trace ) - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message=f"Uploaded '{src}'", - ) + + if (force_upload and file_info.url) or ( + file_info.present is False and file_info.url + ): + upload_file( + src, + template._template._file_context_path, + file_info.url, + resolve_symlinks, + stack_trace, ) - else: - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message=f"Skipping upload of '{src}', already cached", + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Uploaded '{src}'", + ) + ) + else: + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message=f"Skipping upload of '{src}', already cached", + ) ) + + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message="All file uploads completed", ) + ) - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message="All file uploads completed", + # Start build + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message="Starting building...", + ) ) + + trigger_build( + api_client, + template_id, + build_id, + template._template._serialize(instructions_with_hashes), ) - # Start build - if on_build_logs: - on_build_logs( - LogEntry( - timestamp=datetime.now(), - level="info", - message="Starting building...", + if on_build_logs: + on_build_logs( + LogEntry( + timestamp=datetime.now(), + level="info", + message="Waiting for logs...", + ) ) - ) - trigger_build( - api_client, - template_id, - build_id, - template._template._serialize(instructions_with_hashes), - ) + wait_for_build_finish( + api_client, + template_id, + build_id, + on_build_logs, + logs_refresh_frequency=TemplateBase._logs_refresh_frequency, + stack_traces=template._template._stack_traces, + ) + finally: if on_build_logs: on_build_logs( - LogEntry( + LogEntryEnd( timestamp=datetime.now(), - level="info", - message="Waiting for logs...", + message="Build finished", ) - ) - - wait_for_build_finish( - api_client, - template_id, - build_id, - on_build_logs, - logs_refresh_frequency=TemplateBase._logs_refresh_frequency, - stack_traces=template._template._stack_traces, - ) + ) \ No newline at end of file diff --git a/packages/python-sdk/poetry.lock b/packages/python-sdk/poetry.lock index c2d5127490..c733d6d91f 100644 --- a/packages/python-sdk/poetry.lock +++ b/packages/python-sdk/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -647,6 +647,31 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -717,6 +742,18 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "more-itertools" version = "10.8.0" @@ -1019,6 +1056,21 @@ tomli_w = ">=1.0.0,<2.0.0" watchdog = "*" yapf = ">=0.30.0" +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "7.4.4" @@ -1213,6 +1265,25 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.11.12" @@ -1248,7 +1319,7 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, @@ -1599,4 +1670,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "ca49c7725d00f596779188ed2818fa4408c3757944dacf1427b8f05f3738e5e5" +content-hash = "04dab9a9328d4cd18faf57e661d272b78bee7ede93c10eb328fed49f0ddd3d0b" diff --git a/packages/python-sdk/pyproject.toml b/packages/python-sdk/pyproject.toml index da54807092..bf350b3886 100644 --- a/packages/python-sdk/pyproject.toml +++ b/packages/python-sdk/pyproject.toml @@ -20,6 +20,7 @@ attrs = ">=23.2.0" packaging = ">=24.1" typing-extensions = ">=4.1.0" dockerfile-parse = "^2.0.1" +rich = ">=14.0.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" @@ -41,4 +42,4 @@ build-backend = "poetry.core.masonry.api" [tool.ruff] exclude = [ "e2b/envd/filesystem/filesystem_pb2.py" -] \ No newline at end of file +] diff --git a/packages/python-sdk/pyrightconfig.json b/packages/python-sdk/pyrightconfig.json new file mode 100644 index 0000000000..6a3b86905f --- /dev/null +++ b/packages/python-sdk/pyrightconfig.json @@ -0,0 +1,7 @@ +{ + "include": ["e2b"], + + "reportDeprecated": false, + "reportUnannotatedClassAttribute": false, + "reportUnusedCallResult": false +} diff --git a/packages/python-sdk/tests/async/template_async/test_build.py b/packages/python-sdk/tests/async/template_async/test_build.py index d0e87c4959..88c67aa045 100644 --- a/packages/python-sdk/tests/async/template_async/test_build.py +++ b/packages/python-sdk/tests/async/template_async/test_build.py @@ -4,7 +4,7 @@ import os import shutil -from e2b import AsyncTemplate, wait_for_timeout +from e2b import AsyncTemplate, wait_for_timeout, default_build_logger @pytest.fixture(scope="module") @@ -51,7 +51,7 @@ async def test_build_template(async_build, setup_test_folder): .set_start_cmd("echo 'Hello, world!'", wait_for_timeout(10_000)) ) - await async_build(template) + await async_build(template, False, default_build_logger()) @pytest.mark.skip_debug() diff --git a/packages/python-sdk/tests/conftest.py b/packages/python-sdk/tests/conftest.py index 5e7289dd0a..a961d10f76 100644 --- a/packages/python-sdk/tests/conftest.py +++ b/packages/python-sdk/tests/conftest.py @@ -1,4 +1,5 @@ import asyncio +from typing import Callable, Optional import uuid import pytest @@ -18,6 +19,7 @@ Template, TemplateClass, ) +from e2b.template.types import LogEntry @pytest.fixture(scope="session") @@ -66,12 +68,18 @@ async def async_sandbox(template, debug, sandbox_test_id): @pytest.fixture def build(): - def _build(template: TemplateClass): + def _build( + template: TemplateClass, + skip_cache: bool = False, + on_build_logs: Optional[Callable[[LogEntry], None]] = None, + ): return Template.build( template, alias=str(uuid4()), cpu_count=1, memory_mb=1024, + skip_cache=skip_cache, + on_build_logs=on_build_logs, ) return _build @@ -79,12 +87,18 @@ def _build(template: TemplateClass): @pytest_asyncio.fixture def async_build(): - async def _async_build(template: TemplateClass): + async def _async_build( + template: TemplateClass, + skip_cache: bool = False, + on_build_logs: Optional[Callable[[LogEntry], None]] = None, + ): return await AsyncTemplate.build( template, alias=str(uuid4()), cpu_count=1, memory_mb=1024, + skip_cache=skip_cache, + on_build_logs=on_build_logs, ) return _async_build diff --git a/packages/python-sdk/tests/sync/template_sync/test_build.py b/packages/python-sdk/tests/sync/template_sync/test_build.py index a7904abea1..19f815cfff 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_build.py +++ b/packages/python-sdk/tests/sync/template_sync/test_build.py @@ -4,7 +4,7 @@ import os import shutil -from e2b import Template, wait_for_timeout +from e2b import Template, wait_for_timeout, default_build_logger @pytest.fixture(scope="module") @@ -51,7 +51,7 @@ def test_build_template(build, setup_test_folder): .set_start_cmd("echo 'Hello, world!'", wait_for_timeout(10_000)) ) - build(template) + build(template, False, default_build_logger()) @pytest.mark.skip_debug() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d689ef8e54..3a7f8e803e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -356,6 +356,9 @@ importers: '@connectrpc/connect-web': specifier: 2.0.0-rc.3 version: 2.0.0-rc.3(@bufbuild/protobuf@2.6.2)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.6.2)) + chalk: + specifier: ^5.3.0 + version: 5.3.0 compare-versions: specifier: ^6.1.0 version: 6.1.1 From 2dee9b1c9398599d6d556e90f7405a98514d3186 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Wed, 1 Oct 2025 16:23:20 +0200 Subject: [PATCH 02/19] fix python level ordering and timer --- packages/python-sdk/e2b/template/logger.py | 36 ++++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index 4a04331733..00c77ffff5 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -23,17 +23,32 @@ # Level ordering for comparison level_order = { - "error": 0, - "warn": 1, - "info": 2, - "debug": 3, + "debug": 0, + "info": 1, + "warn": 2, + "error": 3, } +def set_interval(func, interval): + """ + Returns a stop function that can be called to cancel the interval. + Similar to JavaScript's setInterval. + """ + stopped = threading.Event() + + def loop(): + while not stopped.wait(interval): # wait returns True if stopped + func() + + threading.Thread(target=loop, daemon=True).start() + return stopped.set # Return the stop function + + class InitialState(TypedDict): start_time: float animation_frame: int - timer: Optional[threading.Timer] + timer: Optional[Callable[[], None]] class BuildLogger: @@ -53,7 +68,7 @@ def logger(self, log): if isinstance(log, LogEntryEnd): if self.__state["timer"] is not None: - self.__state["timer"].cancel() + self.__state["timer"]() return # Filter by minimum level @@ -66,7 +81,7 @@ def logger(self, log): # Redraw the timer line self.__update_timer() - def __reset_initial_state(self, timer: Optional[threading.Timer] = None): + def __reset_initial_state(self, timer: Optional[Callable[[], None]] = None): self.__state = { "start_time": time.time(), "animation_frame": 0, @@ -104,10 +119,11 @@ def __start_timer(self): return # Start the timer interval - timer = threading.Timer(TIMER_UPDATE_INTERVAL_MS / 1000.0, self.__update_timer) - timer.start() + stop_timer = set_interval( + self.__update_timer, TIMER_UPDATE_INTERVAL_MS / 1000.0 + ) - self.__reset_initial_state(timer) + self.__reset_initial_state(stop_timer) # Initial timer display self.__update_timer() From 7b087ec20e7389864fd8f71d7a5c7ce9399eab16 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 20:51:04 +0200 Subject: [PATCH 03/19] move types to logger --- packages/js-sdk/src/template/index.ts | 14 +++---- packages/js-sdk/src/template/logger.ts | 30 ++++++++++++- packages/js-sdk/src/template/types.ts | 29 ------------- packages/python-sdk/e2b/__init__.py | 42 +++++++++---------- packages/python-sdk/e2b/template/logger.py | 34 ++++++++++++++- packages/python-sdk/e2b/template/types.py | 30 ------------- .../e2b/template_async/build_api.py | 3 +- .../python-sdk/e2b/template_async/main.py | 23 +++++----- .../python-sdk/e2b/template_sync/build_api.py | 3 +- packages/python-sdk/e2b/template_sync/main.py | 22 +++++----- 10 files changed, 116 insertions(+), 114 deletions(-) diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 05e39e1353..1883e40d43 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -1,3 +1,4 @@ +import type { PathLike } from 'node:fs' import { ApiClient } from '../api' import { ConnectionConfig } from '../connectionConfig' import { runtime } from '../utils' @@ -11,14 +12,12 @@ import { } from './buildApi' import { RESOLVE_SYMLINKS, STACK_TRACE_DEPTH } from './consts' import { parseDockerfile } from './dockerfileParser' +import { LogEntry, LogEntryEnd, LogEntryStart } from './logger' import { ReadyCmd } from './readycmd' import { CopyItem, Instruction, InstructionType, - LogEntry, - LogEntryEnd, - LogEntryStart, RegistryConfig, TemplateBuilder, TemplateFinal, @@ -32,17 +31,16 @@ import { readDockerignore, readGCPServiceAccountJSON, } from './utils' -import type { PathLike } from 'node:fs' + +export { type TemplateBuilder } from './types' export { + defaultBuildLogger, LogEntry, LogEntryEnd, LogEntryStart, type LogEntryLevel, - type TemplateBuilder, -} from './types' - -export { defaultBuildLogger } from './logger' +} from './logger' type TemplateOptions = { fileContextPath?: PathLike diff --git a/packages/js-sdk/src/template/logger.ts b/packages/js-sdk/src/template/logger.ts index 140c6f9bd9..212e230326 100644 --- a/packages/js-sdk/src/template/logger.ts +++ b/packages/js-sdk/src/template/logger.ts @@ -1,5 +1,33 @@ import chalk from 'chalk' -import { LogEntry, LogEntryEnd, LogEntryLevel, LogEntryStart } from './types' +import { stripAnsi } from '../utils' + +export type LogEntryLevel = 'debug' | 'info' | 'warn' | 'error' + +export class LogEntry { + constructor( + public readonly timestamp: Date, + public readonly level: LogEntryLevel, + public readonly message: string + ) {} + + toString() { + return `[${this.timestamp.toISOString()}] [${this.level}] ${stripAnsi( + this.message + )}` + } +} + +export class LogEntryStart extends LogEntry { + constructor(timestamp: Date, message: string) { + super(timestamp, 'debug', message) + } +} + +export class LogEntryEnd extends LogEntry { + constructor(timestamp: Date, message: string) { + super(timestamp, 'debug', message) + } +} const TIMER_UPDATE_INTERVAL_MS = 150 diff --git a/packages/js-sdk/src/template/types.ts b/packages/js-sdk/src/template/types.ts index b121527886..45652fe3fc 100644 --- a/packages/js-sdk/src/template/types.ts +++ b/packages/js-sdk/src/template/types.ts @@ -1,4 +1,3 @@ -import { stripAnsi } from '../utils' import { ReadyCmd } from './readycmd' import type { PathLike } from 'node:fs' @@ -145,34 +144,6 @@ export interface TemplateBuilder { // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface TemplateFinal {} -export type LogEntryLevel = 'debug' | 'info' | 'warn' | 'error' - -export class LogEntry { - constructor( - public readonly timestamp: Date, - public readonly level: LogEntryLevel, - public readonly message: string - ) {} - - toString() { - return `[${this.timestamp.toISOString()}] [${this.level}] ${stripAnsi( - this.message - )}` - } -} - -export class LogEntryStart extends LogEntry { - constructor(timestamp: Date, message: string) { - super(timestamp, 'debug', message) - } -} - -export class LogEntryEnd extends LogEntry { - constructor(timestamp: Date, message: string) { - super(timestamp, 'debug', message) - } -} - export type GenericDockerRegistry = { type: 'registry' username: string diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index bfb757636f..9226431884 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -42,8 +42,6 @@ NotEnoughSpaceException, TemplateException, ) -from .sandbox.sandbox_api import SandboxInfo, SandboxQuery, SandboxState, SandboxMetrics -from .sandbox.commands.main import ProcessInfo from .sandbox.commands.command_handle import ( CommandResult, Stderr, @@ -52,32 +50,32 @@ PtyOutput, PtySize, ) +from .sandbox.commands.main import ProcessInfo +from .sandbox.filesystem.filesystem import EntryInfo, WriteInfo, FileType from .sandbox.filesystem.watch_handle import ( FilesystemEvent, FilesystemEventType, ) -from .sandbox.filesystem.filesystem import EntryInfo, WriteInfo, FileType from .sandbox.mcp import McpServer - -from .sandbox_sync.main import Sandbox -from .sandbox_sync.filesystem.watch_handle import WatchHandle -from .sandbox_sync.commands.command_handle import CommandHandle +from .sandbox.sandbox_api import SandboxInfo, SandboxQuery, SandboxState, SandboxMetrics +from .sandbox_async.commands.command_handle import AsyncCommandHandle +from .sandbox_async.filesystem.watch_handle import AsyncWatchHandle +from .sandbox_async.main import AsyncSandbox from .sandbox_async.paginator import AsyncSandboxPaginator - from .sandbox_async.utils import OutputHandler -from .sandbox_async.main import AsyncSandbox -from .sandbox_async.filesystem.watch_handle import AsyncWatchHandle -from .sandbox_async.commands.command_handle import AsyncCommandHandle +from .sandbox_sync.commands.command_handle import CommandHandle +from .sandbox_sync.filesystem.watch_handle import WatchHandle +from .sandbox_sync.main import Sandbox from .sandbox_sync.paginator import SandboxPaginator - -from .template.main import TemplateBase, TemplateClass - -from .template.types import CopyItem - -from .template_sync.main import Template -from .template_async.main import AsyncTemplate - from .template.exceptions import BuildException, FileUploadException +from .template.logger import ( + LogEntry, + LogEntryLevel, + LogEntryStart, + LogEntryEnd, + default_build_logger, +) +from .template.main import TemplateBase, TemplateClass from .template.readycmd import ( wait_for_file, wait_for_url, @@ -85,9 +83,9 @@ wait_for_process, wait_for_timeout, ) - -from .template.types import LogEntry, LogEntryLevel, LogEntryStart, LogEntryEnd -from .template.logger import default_build_logger +from .template.types import CopyItem +from .template_async.main import AsyncTemplate +from .template_sync.main import Template __all__ = [ # API diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index 00c77ffff5..c45f2c70b1 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -1,13 +1,43 @@ import os import threading import time -from typing import Optional, TypedDict, Callable, Dict +from dataclasses import dataclass +from datetime import datetime +from typing import Optional, TypedDict, Callable, Dict, Literal from rich.console import Console from rich.style import Style from rich.text import Text -from e2b.template.types import LogEntryLevel, LogEntry, LogEntryStart, LogEntryEnd +from e2b.template.utils import strip_ansi_escape_codes + +LogEntryLevel = Literal["debug", "info", "warn", "error"] + + +@dataclass +class LogEntry: + timestamp: datetime + level: LogEntryLevel + message: str + + def __post_init__(self): + self.message = strip_ansi_escape_codes(self.message) + + def __str__(self) -> str: + return f"[{self.timestamp.isoformat()}] [{self.level}] {self.message}" + + +@dataclass +class LogEntryStart(LogEntry): + def __init__(self, timestamp: datetime, message: str): + super().__init__(timestamp, "debug", message) + + +@dataclass +class LogEntryEnd(LogEntry): + def __init__(self, timestamp: datetime, message: str): + super().__init__(timestamp, "debug", message) + TIMER_UPDATE_INTERVAL_MS = 150 diff --git a/packages/python-sdk/e2b/template/types.py b/packages/python-sdk/e2b/template/types.py index 6b91b7f0f5..24b2b36814 100644 --- a/packages/python-sdk/e2b/template/types.py +++ b/packages/python-sdk/e2b/template/types.py @@ -1,7 +1,5 @@ from typing import List, Optional, TypedDict, Union from typing_extensions import NotRequired -from dataclasses import dataclass -from datetime import datetime from typing import Literal from enum import Enum from pathlib import Path @@ -34,34 +32,6 @@ class Instruction(TypedDict): resolveSymlinks: NotRequired[Optional[bool]] -LogEntryLevel = Literal["debug", "info", "warn", "error"] - - -@dataclass -class LogEntry: - timestamp: datetime - level: LogEntryLevel - message: str - - def __post_init__(self): - self.message = strip_ansi_escape_codes(self.message) - - def __str__(self) -> str: - return f"[{self.timestamp.isoformat()}] [{self.level}] {self.message}" - - -@dataclass -class LogEntryStart(LogEntry): - def __init__(self, timestamp: datetime, message: str): - super().__init__(timestamp, "debug", message) - - -@dataclass -class LogEntryEnd(LogEntry): - def __init__(self, timestamp: datetime, message: str): - super().__init__(timestamp, "debug", message) - - class GenericDockerRegistry(TypedDict): type: Literal["registry"] username: str diff --git a/packages/python-sdk/e2b/template_async/build_api.py b/packages/python-sdk/e2b/template_async/build_api.py index 21bd3163f4..38eace6702 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -24,7 +24,8 @@ Error, ) from e2b.template.exceptions import BuildException, FileUploadException -from e2b.template.types import TemplateType, LogEntry +from e2b.template.logger import LogEntry +from e2b.template.types import TemplateType from e2b.template.utils import get_build_step_index diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index 71b5625167..0592987b9a 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -1,13 +1,13 @@ -from typing import Callable, Optional - -from e2b.template.main import TemplateBase, TemplateClass - import os from datetime import datetime +from typing import Callable, Optional -from e2b.connection_config import ConnectionConfig from e2b.api import AsyncApiClient -from e2b.template.types import LogEntry, InstructionType, LogEntryEnd, LogEntryStart +from e2b.connection_config import ConnectionConfig +from e2b.template.consts import RESOLVE_SYMLINKS +from e2b.template.logger import LogEntry, LogEntryStart, LogEntryEnd +from e2b.template.main import TemplateBase, TemplateClass +from e2b.template.types import InstructionType from .build_api import ( get_file_upload_link, request_build, @@ -15,7 +15,6 @@ upload_file, wait_for_build_finish, ) -from e2b.template.consts import RESOLVE_SYMLINKS class AsyncTemplate(TemplateBase): @@ -83,7 +82,9 @@ async def build( ) ) - instructions_with_hashes = template._template._instructions_with_hashes() + instructions_with_hashes = ( + template._template._instructions_with_hashes() + ) # Upload files for index, file_upload in enumerate(instructions_with_hashes): @@ -94,7 +95,9 @@ async def build( src = args[0] if len(args) > 0 else None force_upload = file_upload.get("forceUpload") files_hash = file_upload.get("filesHash", None) - resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS) + resolve_symlinks = file_upload.get( + "resolveSymlinks", RESOLVE_SYMLINKS + ) if src is None or files_hash is None: raise ValueError("Source path and files hash are required") @@ -185,4 +188,4 @@ async def build( timestamp=datetime.now(), message="Build finished", ) - ) \ No newline at end of file + ) diff --git a/packages/python-sdk/e2b/template_sync/build_api.py b/packages/python-sdk/e2b/template_sync/build_api.py index 37d1655420..51fd4de28b 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -24,7 +24,8 @@ Error, ) from e2b.template.exceptions import BuildException, FileUploadException -from e2b.template.types import TemplateType, LogEntry +from e2b.template.logger import LogEntry +from e2b.template.types import TemplateType from e2b.template.utils import get_build_step_index diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index 63761541ac..20b121798a 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -1,14 +1,13 @@ -from typing import Callable, Optional - -from e2b.template.consts import RESOLVE_SYMLINKS -from e2b.template.main import TemplateBase, TemplateClass -from e2b.template.types import LogEntry, InstructionType, LogEntryStart, LogEntryEnd - import os from datetime import datetime +from typing import Callable, Optional from e2b.api import ApiClient from e2b.connection_config import ConnectionConfig +from e2b.template.consts import RESOLVE_SYMLINKS +from e2b.template.logger import LogEntry, LogEntryStart, LogEntryEnd +from e2b.template.main import TemplateBase, TemplateClass +from e2b.template.types import InstructionType from e2b.template_sync.build_api import ( get_file_upload_link, request_build, @@ -83,7 +82,9 @@ def build( ) ) - instructions_with_hashes = template._template._instructions_with_hashes() + instructions_with_hashes = ( + template._template._instructions_with_hashes() + ) # Upload files for index, file_upload in enumerate(instructions_with_hashes): @@ -94,7 +95,9 @@ def build( src = args[0] if len(args) > 0 else None force_upload = file_upload.get("forceUpload") files_hash = file_upload.get("filesHash", None) - resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS) + resolve_symlinks = file_upload.get( + "resolveSymlinks", RESOLVE_SYMLINKS + ) if src is None or files_hash is None: raise ValueError("Source path and files hash are required") @@ -178,7 +181,6 @@ def build( logs_refresh_frequency=TemplateBase._logs_refresh_frequency, stack_traces=template._template._stack_traces, ) - finally: if on_build_logs: on_build_logs( @@ -186,4 +188,4 @@ def build( timestamp=datetime.now(), message="Build finished", ) - ) \ No newline at end of file + ) From ded05c74d2df67a3d1b967539022eb31097a7162 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 20:56:29 +0200 Subject: [PATCH 04/19] change env CI to isatty --- packages/js-sdk/src/template/logger.ts | 4 ++-- packages/python-sdk/e2b/template/logger.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/js-sdk/src/template/logger.ts b/packages/js-sdk/src/template/logger.ts index 212e230326..f384adc3b1 100644 --- a/packages/js-sdk/src/template/logger.ts +++ b/packages/js-sdk/src/template/logger.ts @@ -126,7 +126,7 @@ class BuildLogger { } private startTimer() { - if (process.env.CI) { + if (!process.stdout.isTTY) { return } @@ -143,7 +143,7 @@ class BuildLogger { } private updateTimer() { - if (process.env.CI) { + if (!process.stdout.isTTY) { return } diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index c45f2c70b1..3c9c3823b0 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -1,4 +1,4 @@ -import os +import sys import threading import time from dataclasses import dataclass @@ -145,7 +145,7 @@ def __format_log_line(self, line: LogEntry) -> Text: return text def __start_timer(self): - if os.getenv("CI"): + if not sys.stdin.isatty: return # Start the timer interval @@ -159,7 +159,7 @@ def __start_timer(self): self.__update_timer() def __update_timer(self): - if os.getenv("CI"): + if not sys.stdin.isatty: return self.__state["animation_frame"] += 1 From 11b84855879d342b7a8d3d846b810969ea6d6431 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 20:59:46 +0200 Subject: [PATCH 05/19] move to Text.assemble --- packages/python-sdk/e2b/template/logger.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index 3c9c3823b0..9b88595a88 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -133,14 +133,15 @@ def __format_log_line(self, line: LogEntry) -> Text: level_text, level_style = levels.get(line.level, levels[DEFAULT_LEVEL]) # Build a rich Text object - text = Text() - text.append(timer) - text.append(" | ") - text.append(timestamp, style="dim") - text.append(" ") - text.append(level_text, style=level_style) - text.append(" ") - text.append(line.message) + text = Text.assemble( + timer, + " | ", + (timestamp, "dim"), + " ", + (level_text, level_style), + " ", + line.message, + ) return text @@ -165,10 +166,9 @@ def __update_timer(self): self.__state["animation_frame"] += 1 jumping_squares = self.__animate_status() - timer_text = Text() - timer_text.append(jumping_squares) - timer_text.append(" Building ") - timer_text.append(self.__format_timer_line()) + timer_text = Text.assemble( + jumping_squares, " Building ", self.__format_timer_line() + ) # Print with carriage return self.__console.print(timer_text, end="\r") From 513bff28e80701320f97a6aab53ae4cd14d007de Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 21:00:34 +0200 Subject: [PATCH 06/19] remove pyrightconfig.json --- packages/python-sdk/pyrightconfig.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 packages/python-sdk/pyrightconfig.json diff --git a/packages/python-sdk/pyrightconfig.json b/packages/python-sdk/pyrightconfig.json deleted file mode 100644 index 6a3b86905f..0000000000 --- a/packages/python-sdk/pyrightconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "include": ["e2b"], - - "reportDeprecated": false, - "reportUnannotatedClassAttribute": false, - "reportUnusedCallResult": false -} From 757651ac63ae45d2067bf9a7e824215627c7dfab Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 21:04:37 +0200 Subject: [PATCH 07/19] fix isatty method call --- packages/python-sdk/e2b/template/logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index 9b88595a88..b5ce49c265 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -146,7 +146,7 @@ def __format_log_line(self, line: LogEntry) -> Text: return text def __start_timer(self): - if not sys.stdin.isatty: + if not sys.stdin.isatty(): return # Start the timer interval @@ -160,7 +160,7 @@ def __start_timer(self): self.__update_timer() def __update_timer(self): - if not sys.stdin.isatty: + if not sys.stdin.isatty(): return self.__state["animation_frame"] += 1 From 73a675f4cfa23d7e31497bd7cc8e1b2e5c4d8e8d Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 21:06:44 +0200 Subject: [PATCH 08/19] fix import --- packages/js-sdk/src/template/buildApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js-sdk/src/template/buildApi.ts b/packages/js-sdk/src/template/buildApi.ts index c6872def06..9369a68b5d 100644 --- a/packages/js-sdk/src/template/buildApi.ts +++ b/packages/js-sdk/src/template/buildApi.ts @@ -1,7 +1,7 @@ import { ApiClient, handleApiError, paths } from '../api' import { stripAnsi } from '../utils' import { BuildError, FileUploadError } from './errors' -import { LogEntry } from './types' +import { LogEntry } from './logger' import { getBuildStepIndex, tarFileStreamUpload } from './utils' type RequestBuildInput = { From ccc41cfb02581b6ebce3cb00562fb0ba900a4d8c Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 21:07:36 +0200 Subject: [PATCH 09/19] fix python import --- packages/python-sdk/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python-sdk/tests/conftest.py b/packages/python-sdk/tests/conftest.py index a961d10f76..16552e9b6e 100644 --- a/packages/python-sdk/tests/conftest.py +++ b/packages/python-sdk/tests/conftest.py @@ -18,8 +18,8 @@ AsyncTemplate, Template, TemplateClass, + LogEntry, ) -from e2b.template.types import LogEntry @pytest.fixture(scope="session") From a21e956d69801c3d60d7eabeca8b2c457603ae9e Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Tue, 7 Oct 2025 21:13:19 +0200 Subject: [PATCH 10/19] fix python stdin to stdout --- packages/python-sdk/e2b/template/logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index b5ce49c265..fd6694c7df 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -146,7 +146,7 @@ def __format_log_line(self, line: LogEntry) -> Text: return text def __start_timer(self): - if not sys.stdin.isatty(): + if not sys.stdout.isatty(): return # Start the timer interval @@ -160,7 +160,7 @@ def __start_timer(self): self.__update_timer() def __update_timer(self): - if not sys.stdin.isatty(): + if not sys.stdout.isatty(): return self.__state["animation_frame"] += 1 From 3b0ea4862f0850728debe6b6372b8c2cc33cbe19 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:05:25 +0200 Subject: [PATCH 11/19] format --- apps/web/src/components/Layout.tsx | 2 +- packages/cli/src/utils/templateSort.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx index 0e98dbf168..2d3f514643 100644 --- a/apps/web/src/components/Layout.tsx +++ b/apps/web/src/components/Layout.tsx @@ -23,7 +23,7 @@ export function Layout({ return (
(aliases: E) { aliases?.sort() } From fa1c80045a5fbf12d789c87ca9fa89455e06209c Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:06:22 +0200 Subject: [PATCH 12/19] await return to handle properly in try/finally --- packages/js-sdk/src/template/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 1883e40d43..f1de4a1d05 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -98,10 +98,13 @@ export class TemplateBase return (template as TemplateBase).toDockerfile() } - static build(template: TemplateClass, options: BuildOptions): Promise { + static async build( + template: TemplateClass, + options: BuildOptions + ): Promise { try { options.onBuildLogs?.(new LogEntryStart(new Date(), 'Build started')) - return (template as TemplateBase).build(options) + return await (template as TemplateBase).build(options) } finally { options.onBuildLogs?.(new LogEntryEnd(new Date(), 'Build finished')) } From 10e1625ddc0e73a4ffa926e4b84743128897c18c Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:09:03 +0200 Subject: [PATCH 13/19] remove unused import --- packages/python-sdk/e2b/template/types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/python-sdk/e2b/template/types.py b/packages/python-sdk/e2b/template/types.py index 24b2b36814..480b0a1199 100644 --- a/packages/python-sdk/e2b/template/types.py +++ b/packages/python-sdk/e2b/template/types.py @@ -1,9 +1,9 @@ -from typing import List, Optional, TypedDict, Union -from typing_extensions import NotRequired -from typing import Literal from enum import Enum from pathlib import Path -from e2b.template.utils import strip_ansi_escape_codes +from typing import List, Optional, TypedDict, Union +from typing import Literal + +from typing_extensions import NotRequired class InstructionType(str, Enum): From e0836ecc3e5cea5783a9b7cce2b3b0eb97e08e0c Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:12:12 +0200 Subject: [PATCH 14/19] add named args --- packages/python-sdk/tests/async/template_async/test_build.py | 2 +- packages/python-sdk/tests/sync/template_sync/test_build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python-sdk/tests/async/template_async/test_build.py b/packages/python-sdk/tests/async/template_async/test_build.py index 88c67aa045..1e334e32a2 100644 --- a/packages/python-sdk/tests/async/template_async/test_build.py +++ b/packages/python-sdk/tests/async/template_async/test_build.py @@ -51,7 +51,7 @@ async def test_build_template(async_build, setup_test_folder): .set_start_cmd("echo 'Hello, world!'", wait_for_timeout(10_000)) ) - await async_build(template, False, default_build_logger()) + await async_build(template, skip_cache=False, on_build_logs=default_build_logger()) @pytest.mark.skip_debug() diff --git a/packages/python-sdk/tests/sync/template_sync/test_build.py b/packages/python-sdk/tests/sync/template_sync/test_build.py index 19f815cfff..cf854b121f 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_build.py +++ b/packages/python-sdk/tests/sync/template_sync/test_build.py @@ -51,7 +51,7 @@ def test_build_template(build, setup_test_folder): .set_start_cmd("echo 'Hello, world!'", wait_for_timeout(10_000)) ) - build(template, False, default_build_logger()) + build(template, skip_cache=False, on_build_logs=default_build_logger()) @pytest.mark.skip_debug() From 73869d09ac85d9decdab74bad9c9331e00c9f92d Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:15:21 +0200 Subject: [PATCH 15/19] use the default build logger in cli build-v2 --- packages/cli/src/commands/template/build-v2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/template/build-v2.ts b/packages/cli/src/commands/template/build-v2.ts index c43df7b060..e2d9b4f8ad 100644 --- a/packages/cli/src/commands/template/build-v2.ts +++ b/packages/cli/src/commands/template/build-v2.ts @@ -1,6 +1,6 @@ import * as boxen from 'boxen' import * as commander from 'commander' -import { Template, TemplateClass } from 'e2b' +import { defaultBuildLogger, Template, TemplateClass } from 'e2b' import { connectionConfig, ensureAccessToken, ensureAPIKey } from 'src/api' import { defaultDockerfileName, @@ -144,7 +144,7 @@ export const buildV2Command = new commander.Command('build-v2') skipCache: opts.noCache, apiKey: apiKey, domain: domain, - onBuildLogs: (logEntry) => console.log(logEntry.toString()), + onBuildLogs: defaultBuildLogger(), }) } catch (error) { console.error('\n❌ Template build failed.') From 94319c8653fb34b633991cdaefa23441cb1d116f Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:15:56 +0200 Subject: [PATCH 16/19] fix formatting --- apps/web/src/components/Layout.tsx | 2 +- packages/cli/src/utils/templateSort.ts | 2 +- packages/js-sdk/src/template/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx index 2d3f514643..0e98dbf168 100644 --- a/apps/web/src/components/Layout.tsx +++ b/apps/web/src/components/Layout.tsx @@ -23,7 +23,7 @@ export function Layout({ return (
(aliases: E) { aliases?.sort() } diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index f1de4a1d05..265e2d6417 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -75,7 +75,7 @@ export class TemplateBase private forceNextLayer: boolean = false private instructions: Instruction[] = [] private fileContextPath: PathLike = - runtime === 'browser' ? '.' : getCallerDirectory(STACK_TRACE_DEPTH) ?? '.' + runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.') private fileIgnorePatterns: string[] = [] private logsRefreshFrequency: number = 200 private stackTraces: (string | undefined)[] = [] From c241b8eee7bbbc7b42cdf7f5b54ed69ca5f7054a Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 12:40:40 +0200 Subject: [PATCH 17/19] fix set interval timing inconsistency --- packages/python-sdk/e2b/template/logger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index fd6694c7df..5f787c6ec0 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -68,8 +68,11 @@ def set_interval(func, interval): stopped = threading.Event() def loop(): - while not stopped.wait(interval): # wait returns True if stopped - func() + while not stopped.is_set(): + if stopped.wait(interval): # wait returns True if stopped + break + if not stopped.is_set(): # Double-check before executing + func() threading.Thread(target=loop, daemon=True).start() return stopped.set # Return the stop function From 1bc8edca52a5bd9208180c19d74b10539105c08b Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 14:07:18 +0200 Subject: [PATCH 18/19] initialize python data struct properly --- packages/python-sdk/e2b/template/logger.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/python-sdk/e2b/template/logger.py b/packages/python-sdk/e2b/template/logger.py index 5f787c6ec0..d865015e87 100644 --- a/packages/python-sdk/e2b/template/logger.py +++ b/packages/python-sdk/e2b/template/logger.py @@ -1,7 +1,7 @@ import sys import threading import time -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime from typing import Optional, TypedDict, Callable, Dict, Literal @@ -29,14 +29,12 @@ def __str__(self) -> str: @dataclass class LogEntryStart(LogEntry): - def __init__(self, timestamp: datetime, message: str): - super().__init__(timestamp, "debug", message) + level: LogEntryLevel = field(default="debug", init=False) @dataclass class LogEntryEnd(LogEntry): - def __init__(self, timestamp: datetime, message: str): - super().__init__(timestamp, "debug", message) + level: LogEntryLevel = field(default="debug", init=False) TIMER_UPDATE_INTERVAL_MS = 150 From 939df55c1c09156530df0b3ce6fd496aa08520f4 Mon Sep 17 00:00:00 2001 From: Jakub Dobry Date: Thu, 9 Oct 2025 14:19:03 +0200 Subject: [PATCH 19/19] move index exports --- packages/js-sdk/src/index.ts | 11 +++++++---- packages/js-sdk/src/template/index.ts | 8 -------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 762a2dce6b..bb06122156 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -75,13 +75,16 @@ export { } from './template/readycmd' export { - Template, - type TemplateClass, - type TemplateBuilder, - type TemplateBase, LogEntry, LogEntryStart, LogEntryEnd, type LogEntryLevel, defaultBuildLogger, +} from './template/logger' + +export { + Template, + type TemplateClass, + type TemplateBuilder, + type TemplateBase, } from './template' diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 265e2d6417..4954079eaa 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -34,14 +34,6 @@ import { export { type TemplateBuilder } from './types' -export { - defaultBuildLogger, - LogEntry, - LogEntryEnd, - LogEntryStart, - type LogEntryLevel, -} from './logger' - type TemplateOptions = { fileContextPath?: PathLike fileIgnorePatterns?: string[]