Files
e2b-dev--e2b/packages/python-sdk/e2b/template/main.py
T
2026-07-13 12:47:58 +08:00

1369 lines
44 KiB
Python

import json
import shlex
from typing import Dict, List, Optional, Union, Literal
from pathlib import Path
from e2b.exceptions import BuildException, InvalidArgumentException
from e2b.template.consts import STACK_TRACE_DEPTH, RESOLVE_SYMLINKS
from e2b.template.dockerfile_parser import parse_dockerfile
from e2b.template.readycmd import ReadyCmd, wait_for_file
from e2b.template.types import (
CopyItem,
Instruction,
TemplateType,
RegistryConfig,
InstructionType,
)
from e2b.template.utils import (
calculate_files_hash,
get_caller_directory,
make_traceback,
pad_octal,
read_dockerignore,
read_gcp_service_account_json,
get_caller_frame,
validate_relative_path,
)
from types import TracebackType
class TemplateBuilder:
"""
Builder class for adding instructions to an E2B template.
All methods return self to allow method chaining.
"""
def __init__(self, template: "TemplateBase"):
self._template = template
def copy(
self,
src: Union[Union[str, Path], List[Union[str, Path]]],
dest: Union[str, Path],
force_upload: Optional[Literal[True]] = None,
user: Optional[str] = None,
mode: Optional[int] = None,
resolve_symlinks: Optional[bool] = None,
gzip: Optional[bool] = None,
) -> "TemplateBuilder":
"""
Copy files or directories from the local filesystem into the template.
:param src: Source file(s) or directory path(s) to copy
:param dest: Destination path in the template
:param force_upload: Force upload even if files are cached
:param user: User and optionally group (user:group) to own the files
:param mode: File permissions in octal format (e.g., 0o755)
:param resolve_symlinks: Whether to resolve symlinks
:param gzip: Whether to gzip the files before upload (default: True)
:return: `TemplateBuilder` class
Example
```python
template.copy('requirements.txt', '/home/user/')
template.copy(['app.py', 'config.py'], '/app/', mode=0o755)
```
"""
srcs = [src] if isinstance(src, (str, Path)) else src
# Get the caller frame for stack trace in validation errors
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace = make_traceback(caller_frame)
for src_item in srcs:
src_string = str(src_item)
# Validate that the source path is a relative path within the context directory
validate_relative_path(src_string, stack_trace)
args = [
src_string,
str(dest),
user or "",
pad_octal(mode) if mode else "",
]
instruction: Instruction = {
"type": InstructionType.COPY,
"args": args,
"force": force_upload or self._template._force_next_layer,
"forceUpload": force_upload,
"resolveSymlinks": resolve_symlinks,
"gzip": gzip,
}
self._template._instructions.append(instruction)
# Collect one stack trace per pushed instruction so build steps
# stay aligned with their stack traces when copying multiple
# sources
self._template._collect_stack_trace()
return self
def copy_items(self, items: List[CopyItem]) -> "TemplateBuilder":
"""
Copy multiple files or directories using a list of copy items.
:param items: List of CopyItem dictionaries with src, dest, and optional parameters
:return: `TemplateBuilder` class
Example
```python
template.copy_items([
{'src': 'app.py', 'dest': '/app/'},
{'src': 'config.py', 'dest': '/app/', 'mode': 0o644}
])
```
"""
# Get the stack trace at the copy_items call site
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace = make_traceback(caller_frame)
def _copy_items():
for item in items:
try:
self.copy(
item["src"],
item["dest"],
item.get("forceUpload"),
item.get("user"),
item.get("mode"),
item.get("resolveSymlinks"),
item.get("gzip"),
)
except Exception as error:
# Re-raise the error with the captured stack trace
if stack_trace is not None:
raise error.with_traceback(stack_trace)
raise
# Use the override so each copied item collects this stack trace,
# keeping build steps aligned with their stack traces
self._template._run_in_stack_trace_override_context(_copy_items, stack_trace)
return self
def remove(
self,
path: Union[Union[str, Path], List[Union[str, Path]]],
force: bool = False,
recursive: bool = False,
user: Optional[str] = None,
) -> "TemplateBuilder":
"""
Remove files or directories in the template.
:param path: File(s) or directory path(s) to remove
:param force: Force removal without prompting
:param recursive: Remove directories recursively
:param user: User to run the command as
:return: `TemplateBuilder` class
Example
```python
template.remove('/tmp/cache', recursive=True, force=True)
template.remove('/tmp/cache', recursive=True, force=True, user='root')
```
"""
paths = [path] if isinstance(path, (str, Path)) else path
args = ["rm"]
if recursive:
args.append("-r")
if force:
args.append("-f")
args.extend([shlex.quote(str(p)) for p in paths])
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user=user)
)
def rename(
self,
src: Union[str, Path],
dest: Union[str, Path],
force: bool = False,
user: Optional[str] = None,
) -> "TemplateBuilder":
"""
Rename or move a file or directory in the template.
:param src: Source path
:param dest: Destination path
:param force: Force rename without prompting
:param user: User to run the command as
:return: `TemplateBuilder` class
Example
```python
template.rename('/tmp/old.txt', '/tmp/new.txt')
template.rename('/tmp/old.txt', '/tmp/new.txt', user='root')
```
"""
args = ["mv", shlex.quote(str(src)), shlex.quote(str(dest))]
if force:
args.append("-f")
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user=user)
)
def make_dir(
self,
path: Union[Union[str, Path], List[Union[str, Path]]],
mode: Optional[int] = None,
user: Optional[str] = None,
) -> "TemplateBuilder":
"""
Create directory(ies) in the template.
:param path: Directory path(s) to create
:param mode: Directory permissions in octal format (e.g., 0o755)
:param user: User to run the command as
:return: `TemplateBuilder` class
Example
```python
template.make_dir('/app/data', mode=0o755)
template.make_dir(['/app/logs', '/app/cache'])
template.make_dir('/app/data', mode=0o755, user='root')
```
"""
path_list = [path] if isinstance(path, (str, Path)) else path
args = ["mkdir", "-p"]
if mode:
args.append(f"-m {pad_octal(mode)}")
args.extend([shlex.quote(str(p)) for p in path_list])
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user=user)
)
def make_symlink(
self,
src: Union[str, Path],
dest: Union[str, Path],
user: Optional[str] = None,
force: bool = False,
) -> "TemplateBuilder":
"""
Create a symbolic link in the template.
:param src: Source path (target of the symlink)
:param dest: Destination path (location of the symlink)
:param user: User to run the command as
:param force: Force symlink without prompting
:return: `TemplateBuilder` class
Example
```python
template.make_symlink('/usr/bin/python3', '/usr/bin/python')
template.make_symlink('/usr/bin/python3', '/usr/bin/python', user='root')
template.make_symlink('/usr/bin/python3', '/usr/bin/python', force=True)
```
"""
args = ["ln", "-s"]
if force:
args.append("-f")
args.extend([shlex.quote(str(src)), shlex.quote(str(dest))])
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user=user)
)
def run_cmd(
self, command: Union[str, List[str]], user: Optional[str] = None
) -> "TemplateBuilder":
"""
Run a shell command during template build.
:param command: Command string or list of commands to run (joined with &&)
:param user: User to run the command as
:return: `TemplateBuilder` class
Example
```python
template.run_cmd('apt-get update')
template.run_cmd(['pip install numpy', 'pip install pandas'])
template.run_cmd('apt-get install vim', user='root')
```
"""
commands = [command] if isinstance(command, str) else command
args = [" && ".join(commands)]
if user:
args.append(user)
instruction: Instruction = {
"type": InstructionType.RUN,
"args": args,
"force": self._template._force_next_layer,
"forceUpload": None,
}
self._template._instructions.append(instruction)
self._template._collect_stack_trace()
return self
def set_workdir(self, workdir: Union[str, Path]) -> "TemplateBuilder":
"""
Set the working directory for subsequent commands in the template.
:param workdir: Path to set as the working directory
:return: `TemplateBuilder` class
Example
```python
template.set_workdir('/app')
```
"""
instruction: Instruction = {
"type": InstructionType.WORKDIR,
"args": [str(workdir)],
"force": self._template._force_next_layer,
"forceUpload": None,
}
self._template._instructions.append(instruction)
self._template._collect_stack_trace()
return self
def set_user(self, user: str) -> "TemplateBuilder":
"""
Set the user for subsequent commands in the template.
:param user: Username to set
:return: `TemplateBuilder` class
Example
```python
template.set_user('root')
```
"""
instruction: Instruction = {
"type": InstructionType.USER,
"args": [user],
"force": self._template._force_next_layer,
"forceUpload": None,
}
self._template._instructions.append(instruction)
self._template._collect_stack_trace()
return self
def pip_install(
self, packages: Optional[Union[str, List[str]]] = None, g: bool = True
) -> "TemplateBuilder":
"""
Install Python packages using pip.
:param packages: Package name(s) to install. If None, runs 'pip install .' in the current directory
:param g: Install packages globally (default: True). If False, installs for user only
:return: `TemplateBuilder` class
Example
```python
template.pip_install('numpy')
template.pip_install(['pandas', 'scikit-learn'])
template.pip_install('numpy', g=False) # Install for user only
template.pip_install() # Installs from current directory
```
"""
if isinstance(packages, str):
packages = [packages]
args = ["pip", "install"]
if not g:
args.append("--user")
if packages:
args.extend(packages)
else:
args.append(".")
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user="root" if g else None)
)
def npm_install(
self,
packages: Optional[Union[str, List[str]]] = None,
g: Optional[bool] = False,
dev: Optional[bool] = False,
) -> "TemplateBuilder":
"""
Install Node.js packages using npm.
:param packages: Package name(s) to install. If None, installs from package.json
:param g: Install packages globally
:param dev: Install packages as dev dependencies
:return: `TemplateBuilder` class
Example
```python
template.npm_install('express')
template.npm_install(['lodash', 'axios'])
template.npm_install('typescript', g=True)
template.npm_install() # Installs from package.json
```
"""
if isinstance(packages, str):
packages = [packages]
args = ["npm", "install"]
if g:
args.append("-g")
if dev:
args.append("--save-dev")
if packages:
args.extend(packages)
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user="root" if g else None)
)
def bun_install(
self,
packages: Optional[Union[str, List[str]]] = None,
g: Optional[bool] = False,
dev: Optional[bool] = False,
) -> "TemplateBuilder":
"""
Install Bun packages using bun.
:param packages: Package name(s) to install. If None, installs from package.json
:param g: Install packages globally
:param dev: Install packages as dev dependencies
:return: `TemplateBuilder` class
Example
```python
template.bun_install('express')
template.bun_install(['lodash', 'axios'])
template.bun_install('tsx', g=True)
template.bun_install('typescript', dev=True)
template.bun_install() // Installs from package.json
```
"""
if isinstance(packages, str):
packages = [packages]
args = ["bun", "install"]
if g:
args.append("-g")
if dev:
args.append("--dev")
if packages:
args.extend(packages)
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user="root" if g else None)
)
def apt_install(
self,
packages: Union[str, List[str]],
no_install_recommends: bool = False,
fix_missing: bool = False,
) -> "TemplateBuilder":
"""
Install system packages using apt-get.
:param packages: Package name(s) to install
:param no_install_recommends: Whether to install recommended packages
:param fix_missing: Whether to fix missing packages
:return: `TemplateBuilder` class
Example
```python
template.apt_install('vim')
template.apt_install(['git', 'curl', 'wget'])
template.apt_install('vim', fix_missing=True)
```
"""
if isinstance(packages, str):
packages = [packages]
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(
[
"apt-get update",
f"DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get install -y {'--no-install-recommends ' if no_install_recommends else ''}{'--fix-missing ' if fix_missing else ''}{' '.join(packages)}",
],
user="root",
)
)
def add_mcp_server(self, servers: Union[str, List[str]]) -> "TemplateBuilder":
"""
Install MCP servers using mcp-gateway.
Note: Requires a base image with mcp-gateway pre-installed (e.g., mcp-gateway).
:param servers: MCP server name(s)
:return: `TemplateBuilder` class
Example
```python
template.add_mcp_server('exa')
template.add_mcp_server(['brave', 'firecrawl', 'duckduckgo'])
```
"""
if self._template._base_template != "mcp-gateway":
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace = make_traceback(caller_frame)
raise BuildException(
"MCP servers can only be added to mcp-gateway template"
).with_traceback(stack_trace)
if isinstance(servers, str):
servers = [servers]
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(f"mcp-gateway pull {' '.join(servers)}", user="root")
)
def git_clone(
self,
url: str,
path: Optional[Union[str, Path]] = None,
branch: Optional[str] = None,
depth: Optional[int] = None,
user: Optional[str] = None,
) -> "TemplateBuilder":
"""
Clone a git repository into the template.
:param url: Git repository URL
:param path: Destination path for the clone
:param branch: Branch to clone
:param depth: Clone depth for shallow clones
:param user: User to run the command as
:return: `TemplateBuilder` class
Example
```python
template.git_clone('https://github.com/user/repo.git', '/app/repo')
template.git_clone('https://github.com/user/repo.git', branch='main', depth=1)
template.git_clone('https://github.com/user/repo.git', '/app/repo', user='root')
```
"""
args = ["git", "clone", shlex.quote(url)]
if branch:
args.append(f"--branch {shlex.quote(branch)}")
args.append("--single-branch")
if depth:
args.append(f"--depth {depth}")
if path:
args.append(shlex.quote(str(path)))
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(" ".join(args), user=user)
)
def beta_dev_container_prebuild(
self,
devcontainer_directory: Union[str, Path],
) -> "TemplateBuilder":
"""
Prebuild a devcontainer from the specified directory during the build process.
:param devcontainer_directory: Path to the devcontainer directory
:return: `TemplateBuilder` class
Example
```python
template.git_clone('https://myrepo.com/project.git', '/my-devcontainer')
template.beta_dev_container_prebuild('/my-devcontainer')
```
"""
if self._template._base_template != "devcontainer":
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace = make_traceback(caller_frame)
raise BuildException(
"Devcontainers can only used in the devcontainer template"
).with_traceback(stack_trace)
return self._template._run_in_new_stack_trace_context(
lambda: self.run_cmd(
f"devcontainer build --workspace-folder {shlex.quote(str(devcontainer_directory))}",
user="root",
)
)
def beta_set_dev_container_start(
self,
devcontainer_directory: Union[str, Path],
) -> "TemplateFinal":
"""
Start a devcontainer from the specified directory and set it as the start command.
This method returns `TemplateFinal`, which means it must be the last method in the chain.
:param devcontainer_directory: Path to the devcontainer directory
:return: `TemplateFinal` class
Example
```python
# Simple start
template.git_clone('https://myrepo.com/project.git', '/my-devcontainer')
template.beta_set_devcontainer_start('/my-devcontainer')
# With prebuild
template.git_clone('https://myrepo.com/project.git', '/my-devcontainer')
template.beta_dev_container_prebuild('/my-devcontainer')
template.beta_set_dev_container_start('/my-devcontainer')
```
"""
if self._template._base_template != "devcontainer":
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace = make_traceback(caller_frame)
raise BuildException(
"Devcontainers can only used in the devcontainer template"
).with_traceback(stack_trace)
def _set_start():
dir_ = shlex.quote(str(devcontainer_directory))
return self.set_start_cmd(
"sudo devcontainer up --workspace-folder "
+ dir_
+ " && sudo /prepare-exec.sh "
+ dir_
+ " | sudo tee /devcontainer.sh > /dev/null && sudo chmod +x /devcontainer.sh && sudo touch /devcontainer.up",
wait_for_file("/devcontainer.up"),
)
return self._template._run_in_new_stack_trace_context(_set_start)
def set_envs(self, envs: Dict[str, str]) -> "TemplateBuilder":
"""
Set environment variables.
Note: Environment variables defined here are available only during template build.
:param envs: Dictionary of environment variable names and values
:return: `TemplateBuilder` class
Example
```python
template.set_envs({'NODE_ENV': 'production', 'PORT': '8080'})
```
"""
if len(envs) == 0:
return self
instruction: Instruction = {
"type": InstructionType.ENV,
"args": [item for key, value in envs.items() for item in [key, value]],
"force": self._template._force_next_layer,
"forceUpload": None,
}
self._template._instructions.append(instruction)
self._template._collect_stack_trace()
return self
def skip_cache(self) -> "TemplateBuilder":
"""
Skip cache for all subsequent build instructions from this point.
Call this before any instruction to force it and all following layers
to be rebuilt, ignoring any cached layers.
:return: `TemplateBuilder` class
Example
```python
template.skip_cache().run_cmd('apt-get update')
```
"""
self._template._force_next_layer = True
return self
def set_start_cmd(
self, start_cmd: str, ready_cmd: Union[str, ReadyCmd]
) -> "TemplateFinal":
"""
Set the command to start when the sandbox launches and the ready check command.
:param start_cmd: Command to run when the sandbox starts
:param ready_cmd: Command or ReadyCmd to check if the sandbox is ready
:return: `TemplateFinal` class
Example
```python
# Using a string command
template.set_start_cmd(
'python app.py',
'curl http://localhost:8000/health'
)
# Using ReadyCmd helpers
from e2b import wait_for_port, wait_for_url
template.set_start_cmd(
'python -m http.server 8000',
wait_for_port(8000)
)
template.set_start_cmd(
'npm start',
wait_for_url('http://localhost:3000/health', 200)
)
```
"""
self._template._start_cmd = start_cmd
if isinstance(ready_cmd, ReadyCmd):
ready_cmd = ready_cmd.get_cmd()
self._template._ready_cmd = ready_cmd
self._template._collect_stack_trace()
return TemplateFinal(self._template)
def set_ready_cmd(self, ready_cmd: Union[str, ReadyCmd]) -> "TemplateFinal":
"""
Set the command to check if the sandbox is ready.
:param ready_cmd: Command or ReadyCmd to check if the sandbox is ready
:return: `TemplateFinal` class
Example
```python
# Using a string command
template.set_ready_cmd('curl http://localhost:8000/health')
# Using ReadyCmd helpers
from e2b import wait_for_port, wait_for_file, wait_for_process
template.set_ready_cmd(wait_for_port(3000))
template.set_ready_cmd(wait_for_file('/tmp/ready'))
template.set_ready_cmd(wait_for_process('nginx'))
```
"""
if isinstance(ready_cmd, ReadyCmd):
ready_cmd = ready_cmd.get_cmd()
self._template._ready_cmd = ready_cmd
self._template._collect_stack_trace()
return TemplateFinal(self._template)
class TemplateFinal:
"""
Final template state after start/ready commands are set.
"""
def __init__(self, template: "TemplateBase"):
self._template = template
class TemplateBase:
"""
Base class for building E2B sandbox templates.
"""
_logs_refresh_frequency = 0.2
def __init__(
self,
file_context_path: Optional[Union[str, Path]] = None,
file_ignore_patterns: Optional[List[str]] = None,
):
"""
Create a new template builder instance.
:param file_context_path: Base path for resolving relative file paths in copy operations
:param file_ignore_patterns: List of glob patterns to ignore when copying files
"""
self._default_base_image: str = "e2bdev/base"
self._base_image: Optional[str] = self._default_base_image
self._base_template: Optional[str] = None
self._registry_config: Optional[RegistryConfig] = None
self._start_cmd: Optional[str] = None
self._ready_cmd: Optional[str] = None
# Force the whole template to be rebuilt
self._force: bool = False
# Force the next layer to be rebuilt
self._force_next_layer: bool = False
self._instructions: List[Instruction] = []
# If no file_context_path is provided, use the caller's directory
self._file_context_path = (
file_context_path.as_posix()
if isinstance(file_context_path, Path)
else (file_context_path or get_caller_directory(STACK_TRACE_DEPTH) or ".")
)
self._file_ignore_patterns: List[str] = file_ignore_patterns or []
self._stack_traces: List[Union[TracebackType, None]] = []
self._stack_traces_enabled: bool = True
self._stack_traces_override: Optional[Union[TracebackType, None]] = None
def skip_cache(self) -> "TemplateBase":
"""
Skip cache for all subsequent build instructions from this point.
:return: `TemplateBase` class
Example
```python
template.skip_cache().from_python_image('3.11')
```
"""
self._force_next_layer = True
return self
def _collect_stack_trace(
self, stack_traces_depth: int = STACK_TRACE_DEPTH
) -> "TemplateBase":
"""
Collect the current stack trace for debugging purposes.
:param stack_traces_depth: Depth to traverse in the call stack
:return: `TemplateBase` class
"""
if not self._stack_traces_enabled:
return self
# Use the override if set, otherwise get the caller frame
if self._stack_traces_override is not None:
self._stack_traces.append(self._stack_traces_override)
return self
stack = get_caller_frame(stack_traces_depth)
self._stack_traces.append(make_traceback(stack))
return self
def _disable_stack_trace(self) -> "TemplateBase":
"""
Temporarily disable stack trace collection.
:return: `TemplateBase` class
"""
self._stack_traces_enabled = False
return self
def _enable_stack_trace(self) -> "TemplateBase":
"""
Re-enable stack trace collection.
:return: `TemplateBase` class
"""
self._stack_traces_enabled = True
return self
def _run_in_new_stack_trace_context(self, fn):
"""
Execute a function in a clean stack trace context.
:param fn: Function to execute
:return: The result of the function
"""
self._disable_stack_trace()
try:
result = fn()
finally:
self._enable_stack_trace()
self._collect_stack_trace(STACK_TRACE_DEPTH + 1)
return result
def _run_in_stack_trace_override_context(
self, fn, stack_trace_override: Optional[Union[TracebackType, None]]
):
"""
Execute a function with a manual stack trace override.
:param fn: Function to execute
:param stack_trace_override: Stack trace to use instead of auto-collecting
:return: The result of the function
"""
self._stack_traces_override = stack_trace_override
try:
return fn()
finally:
self._stack_traces_override = None
# Built-in image mixins
def from_debian_image(self, variant: str = "stable") -> TemplateBuilder:
"""
Start template from a Debian base image.
:param variant: Debian image variant
:return: `TemplateBuilder` class
Example
```python
Template().from_debian_image('bookworm')
```
"""
return self._run_in_new_stack_trace_context(
lambda: self.from_image(f"debian:{variant}")
)
def from_ubuntu_image(self, variant: str = "latest") -> TemplateBuilder:
"""
Start template from an Ubuntu base image.
:param variant: Ubuntu image variant (default: 'latest')
:return: `TemplateBuilder` class
Example
```python
Template().from_ubuntu_image('24.04')
```
"""
return self._run_in_new_stack_trace_context(
lambda: self.from_image(f"ubuntu:{variant}")
)
def from_python_image(self, version: str = "3") -> TemplateBuilder:
"""
Start template from a Python base image.
:param version: Python version (default: '3')
:return: `TemplateBuilder` class
Example
```python
Template().from_python_image('3')
```
"""
return self._run_in_new_stack_trace_context(
lambda: self.from_image(f"python:{version}")
)
def from_node_image(self, variant: str = "lts") -> TemplateBuilder:
"""
Start template from a Node.js base image.
:param variant: Node.js image variant (default: 'lts')
:return: `TemplateBuilder` class
Example
```python
Template().from_node_image('24')
```
"""
return self._run_in_new_stack_trace_context(
lambda: self.from_image(f"node:{variant}")
)
def from_bun_image(self, variant: str = "latest") -> TemplateBuilder:
"""
Start template from a Bun base image.
:param variant: Bun image variant (default: 'latest')
:return: `TemplateBuilder` class
"""
return self._run_in_new_stack_trace_context(
lambda: self.from_image(f"oven/bun:{variant}")
)
def from_base_image(self) -> TemplateBuilder:
"""
Start template from the E2B base image (e2bdev/base:latest).
:return: `TemplateBuilder` class
Example
```python
Template().from_base_image()
```
"""
return self._run_in_new_stack_trace_context(
lambda: self.from_image(self._default_base_image)
)
def from_image(
self,
image: str,
username: Optional[str] = None,
password: Optional[str] = None,
) -> TemplateBuilder:
"""
Start template from a Docker image.
:param image: Docker image name (e.g., 'ubuntu:24.04')
:param username: Username for private registry authentication
:param password: Password for private registry authentication
:return: `TemplateBuilder` class
Example
```python
Template().from_image('python:3')
# With credentials (optional)
Template().from_image('myregistry.com/myimage:latest', username='user', password='pass')
```
"""
# Validate (and resolve the registry config) before mutating the builder.
if username is not None or password is not None:
if not username or not password:
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace = make_traceback(caller_frame)
raise InvalidArgumentException(
"Both username and password are required when providing registry credentials"
).with_traceback(stack_trace)
self._registry_config = {
"type": "registry",
"username": username,
"password": password,
}
self._base_image = image
self._base_template = None
# If we should force the next layer and it's a FROM command, invalidate whole template
if self._force_next_layer:
self._force = True
self._collect_stack_trace()
return TemplateBuilder(self)
def from_template(self, template: str) -> TemplateBuilder:
"""
Start template from an existing E2B template.
:param template: E2B template ID or alias
:return: `TemplateBuilder` class
Example
```python
Template().from_template('my-base-template')
```
"""
self._base_template = template
self._base_image = None
# If we should force the next layer and it's a FROM command, invalidate whole template
if self._force_next_layer:
self._force = True
self._collect_stack_trace()
return TemplateBuilder(self)
def from_dockerfile(self, dockerfile_content_or_path: str) -> TemplateBuilder:
"""
Parse a Dockerfile and convert it to Template SDK format.
:param dockerfile_content_or_path: Either the Dockerfile content as a string, or a path to a Dockerfile file
:return: `TemplateBuilder` class
Example
```python
Template().from_dockerfile('Dockerfile')
Template().from_dockerfile('FROM python:3\\nRUN pip install numpy')
```
"""
# Create a TemplateBuilder first to use its methods
builder = TemplateBuilder(self)
# Get the caller frame to use for stack trace override
# -1 as we're going up the call stack from the parse_dockerfile function
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
stack_trace_override = make_traceback(caller_frame)
# Parse the dockerfile using the builder as the interface
base_image = self._run_in_stack_trace_override_context(
lambda: parse_dockerfile(dockerfile_content_or_path, builder),
stack_trace_override,
)
self._base_image = base_image
# If we should force the next layer and it's a FROM command, invalidate whole template
if self._force_next_layer:
self._force = True
self._collect_stack_trace()
return builder
def from_aws_registry(
self,
image: str,
access_key_id: str,
secret_access_key: str,
region: str,
) -> TemplateBuilder:
"""
Start template from an AWS ECR registry image.
:param image: Docker image name from AWS ECR
:param access_key_id: AWS access key ID
:param secret_access_key: AWS secret access key
:param region: AWS region
:return: `TemplateBuilder` class
Example
```python
Template().from_aws_registry(
'123456789.dkr.ecr.us-west-2.amazonaws.com/myimage:latest',
access_key_id='AKIA...',
secret_access_key='...',
region='us-west-2'
)
```
"""
self._base_image = image
self._base_template = None
# Set the registry config if provided
self._registry_config = {
"type": "aws",
"awsAccessKeyId": access_key_id,
"awsSecretAccessKey": secret_access_key,
"awsRegion": region,
}
# If we should force the next layer and it's a FROM command, invalidate whole template
if self._force_next_layer:
self._force = True
self._collect_stack_trace()
return TemplateBuilder(self)
def from_gcp_registry(
self, image: str, service_account_json: Union[str, dict]
) -> TemplateBuilder:
"""
Start template from a GCP Artifact Registry or Container Registry image.
:param image: Docker image name from GCP registry
:param service_account_json: Service account JSON string, dict, or path to JSON file
:return: `TemplateBuilder` class
Example
```python
Template().from_gcp_registry(
'gcr.io/myproject/myimage:latest',
service_account_json='path/to/service-account.json'
)
```
"""
self._base_image = image
self._base_template = None
# Set the registry config if provided
self._registry_config = {
"type": "gcp",
"serviceAccountJson": read_gcp_service_account_json(
self._file_context_path, service_account_json
),
}
# If we should force the next layer and it's a FROM command, invalidate whole template
if self._force_next_layer:
self._force = True
self._collect_stack_trace()
return TemplateBuilder(self)
@staticmethod
def to_json(template: "TemplateClass") -> str:
"""
Convert a template to JSON representation.
:param template: The template to convert (TemplateBuilder or TemplateFinal instance)
:return: JSON string representation of the template
Example
```python
template = Template().from_python_image('3').copy('app.py', '/app/')
json_str = TemplateBase.to_json(template)
```
"""
return json.dumps(
template._template._serialize(
template._template._instructions_with_hashes()
),
indent=2,
)
@staticmethod
def to_dockerfile(template: "TemplateClass") -> str:
"""
Convert a template to Dockerfile format.
Note: Templates based on other E2B templates cannot be converted to Dockerfile.
:param template: The template to convert (TemplateBuilder or TemplateFinal instance)
:return: Dockerfile string representation
:raises ValueError: If the template is based on another E2B template or has no base image
Example
```python
template = Template().from_python_image('3').copy('app.py', '/app/')
dockerfile = TemplateBase.to_dockerfile(template)
```
"""
if template._template._base_template is not None:
raise ValueError(
"Cannot convert template built from another template to Dockerfile. "
"Templates based on other templates can only be built using the E2B API."
)
if template._template._base_image is None:
raise ValueError("No base image specified for template")
dockerfile = f"FROM {template._template._base_image}\n"
for instruction in template._template._instructions:
if instruction["type"] == InstructionType.RUN:
dockerfile += f"RUN {instruction['args'][0]}\n"
continue
if instruction["type"] == InstructionType.COPY:
dockerfile += (
f"COPY {instruction['args'][0]} {instruction['args'][1]}\n"
)
continue
if instruction["type"] == InstructionType.ENV:
args = instruction["args"]
values = []
for i in range(0, len(args), 2):
values.append(f"{args[i]}={args[i + 1]}")
dockerfile += f"ENV {' '.join(values)}\n"
continue
dockerfile += (
f"{instruction['type'].value} {' '.join(instruction['args'])}\n"
)
if template._template._start_cmd:
dockerfile += f"ENTRYPOINT {template._template._start_cmd}\n"
return dockerfile
def _instructions_with_hashes(
self,
) -> List[Instruction]:
"""
Add file hashes to COPY instructions for cache invalidation.
:return: Copy of instructions list with filesHash added to COPY instructions
"""
steps: List[Instruction] = []
for index, instruction in enumerate(self._instructions):
step: Instruction = {
"type": instruction["type"],
"args": instruction["args"],
"force": instruction["force"],
"forceUpload": instruction.get("forceUpload"),
"resolveSymlinks": instruction.get("resolveSymlinks"),
"gzip": instruction.get("gzip"),
}
if instruction["type"] == InstructionType.COPY:
stack_trace = None
if index + 1 < len(self._stack_traces):
stack_trace = self._stack_traces[index + 1]
args = instruction.get("args", [])
src = args[0] if len(args) > 0 else None
dest = args[1] if len(args) > 1 else None
if src is None or dest is None:
raise ValueError("Source path and destination path are required")
resolve_symlinks = instruction.get("resolveSymlinks")
step["filesHash"] = calculate_files_hash(
src,
dest,
self._file_context_path,
[
*self._file_ignore_patterns,
*read_dockerignore(self._file_context_path),
],
resolve_symlinks
if resolve_symlinks is not None
else RESOLVE_SYMLINKS,
stack_trace,
)
steps.append(step)
return steps
def _serialize(self, steps: List[Instruction]) -> TemplateType:
"""
Serialize the template to the API request format.
:param steps: List of build instructions with file hashes
:return: Template data formatted for the API
"""
_steps: List[Instruction] = []
for _, instruction in enumerate(steps):
step: Instruction = {
"type": instruction.get("type"),
"args": instruction.get("args"),
"force": instruction.get("force"),
}
files_hash = instruction.get("filesHash")
if files_hash is not None:
step["filesHash"] = files_hash
force_upload = instruction.get("forceUpload")
if force_upload is not None:
step["forceUpload"] = force_upload
_steps.append(step)
template_data: TemplateType = {
"steps": _steps,
"force": self._force,
}
if self._base_image is not None:
template_data["fromImage"] = self._base_image
if self._base_template is not None:
template_data["fromTemplate"] = self._base_template
if self._registry_config is not None:
template_data["fromImageRegistry"] = self._registry_config
if self._start_cmd is not None:
template_data["startCmd"] = self._start_cmd
if self._ready_cmd is not None:
template_data["readyCmd"] = self._ready_cmd
return template_data
TemplateClass = Union[TemplateFinal, TemplateBuilder]