Files
2026-07-13 12:47:58 +08:00

530 lines
16 KiB
Python

from datetime import datetime
from typing import Callable, List, Optional, Union
from typing_extensions import Unpack
from e2b.api.client.client import AuthenticatedClient
from e2b.connection_config import ApiParams, ConnectionConfig
from e2b.api.client_sync import get_api_client
from e2b.template.consts import GZIP, RESOLVE_SYMLINKS
from e2b.template.logger import LogEntry, LogEntryEnd, LogEntryStart
from e2b.template.main import TemplateBase, TemplateClass
from e2b.template.types import BuildInfo, InstructionType, TemplateTag, TemplateTagInfo
from e2b.template_sync.build_api import (
assign_tags,
check_alias_exists,
get_template_tags,
remove_tags,
get_build_status,
get_file_upload_link,
request_build,
trigger_build,
upload_file,
wait_for_build_finish,
)
from e2b.template.utils import normalize_build_arguments, read_dockerignore
class Template(TemplateBase):
"""
Synchronous template builder for E2B sandboxes.
"""
@staticmethod
def _build(
api_client: AuthenticatedClient,
template: TemplateClass,
name: str,
tags: Optional[List[str]] = None,
cpu_count: int = 2,
memory_mb: int = 1024,
skip_cache: bool = False,
on_build_logs: Optional[Callable[[LogEntry], None]] = None,
request_timeout: Optional[float] = None,
) -> BuildInfo:
"""
Internal implementation of the template build process
:param api_client: Authenticated API client
:param template: The template to build
:param name: Name for the template
:param tags: Optional tags for the template
:param cpu_count: Number of CPUs allocated to the sandbox
:param memory_mb: Amount of memory in MB allocated to the sandbox
:param skip_cache: If True, forces a complete rebuild ignoring cache
:param on_build_logs: Callback function to receive build logs during the build process
"""
if skip_cache:
template._template._force = True
# Create template
if on_build_logs:
tags_msg = f" with tags: {', '.join(tags)}" if tags else ""
on_build_logs(
LogEntry(
timestamp=datetime.now(),
level="info",
message=f"Requesting build for template: {name}{tags_msg}",
)
)
response = request_build(
api_client,
name=name,
cpu_count=cpu_count,
memory_mb=memory_mb,
tags=tags,
)
template_id = response.template_id
build_id = response.build_id
response_tags = response.tags
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}",
)
)
instructions_with_hashes = template._template._instructions_with_hashes()
# Upload files
for index, file_upload in enumerate(instructions_with_hashes):
if file_upload["type"] != InstructionType.COPY:
continue
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")
if resolve_symlinks is None:
resolve_symlinks = RESOLVE_SYMLINKS
gzip = file_upload.get("gzip")
if gzip is None:
gzip = GZIP
if src is None or files_hash is None:
raise ValueError("Source path and files hash are required")
stack_trace = None
if index + 1 < len(template._template._stack_traces):
stack_trace = template._template._stack_traces[index + 1]
file_info = get_file_upload_link(
api_client, template_id, files_hash, stack_trace
)
if (force_upload and file_info.url) or (
file_info.present is False and file_info.url
):
upload_file(
api_client,
src,
template._template._file_context_path,
file_info.url,
[
*template._template._file_ignore_patterns,
*read_dockerignore(template._template._file_context_path),
],
resolve_symlinks,
gzip,
stack_trace,
request_timeout=request_timeout,
)
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",
)
)
# 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),
)
return BuildInfo(
template_id=template_id,
build_id=build_id,
alias=name,
name=name,
tags=response_tags,
)
@staticmethod
def build(
template: TemplateClass,
name: Optional[str] = None,
*,
alias: Optional[str] = None,
tags: Optional[List[str]] = None,
cpu_count: int = 2,
memory_mb: int = 1024,
skip_cache: bool = False,
on_build_logs: Optional[Callable[[LogEntry], None]] = None,
**opts: Unpack[ApiParams],
) -> BuildInfo:
"""
Build and deploy a template to E2B infrastructure.
:param template: The template to build
:param name: Template name in 'name' or 'name:tag' format
:param alias: (Deprecated) Alias name for the template. Use name instead.
:param tags: Optional additional tags to assign to the template
:param cpu_count: Number of CPUs allocated to the sandbox
:param memory_mb: Amount of memory in MB allocated to the sandbox
:param skip_cache: If True, forces a complete rebuild ignoring cache
:param on_build_logs: Callback function to receive build logs during the build process
Example
```python
from e2b import Template
template = (
Template()
.from_python_image('3')
.copy('requirements.txt', '/home/user/')
.run_cmd('pip install -r /home/user/requirements.txt')
)
# Build with single tag
Template.build(template, 'my-python-env:v1.0')
# Build with multiple tags
Template.build(template, 'my-python-env', tags=['v1.1.0', 'stable'])
```
"""
name = normalize_build_arguments(name, alias)
try:
if on_build_logs:
on_build_logs(
LogEntryStart(
timestamp=datetime.now(),
message="Build started",
)
)
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
data = Template._build(
api_client,
template,
name,
tags=tags,
cpu_count=cpu_count,
memory_mb=memory_mb,
skip_cache=skip_cache,
on_build_logs=on_build_logs,
# Only honor an explicitly set request_timeout for uploads;
# otherwise upload_file applies its 1-hour default.
request_timeout=opts.get("request_timeout"),
)
if on_build_logs:
on_build_logs(
LogEntry(
timestamp=datetime.now(),
level="info",
message="Waiting for logs...",
)
)
wait_for_build_finish(
api_client,
data.template_id,
data.build_id,
on_build_logs,
logs_refresh_frequency=TemplateBase._logs_refresh_frequency,
stack_traces=template._template._stack_traces,
)
return data
finally:
if on_build_logs:
on_build_logs(
LogEntryEnd(
timestamp=datetime.now(),
message="Build finished",
)
)
@staticmethod
def build_in_background(
template: TemplateClass,
name: Optional[str] = None,
*,
alias: Optional[str] = None,
tags: Optional[List[str]] = None,
cpu_count: int = 2,
memory_mb: int = 1024,
skip_cache: bool = False,
on_build_logs: Optional[Callable[[LogEntry], None]] = None,
**opts: Unpack[ApiParams],
) -> BuildInfo:
"""
Build and deploy a template to E2B infrastructure without waiting for completion.
:param template: The template to build
:param name: Template name in 'name' or 'name:tag' format
:param alias: (Deprecated) Alias name for the template. Use name instead.
:param tags: Optional additional tags to assign to the template
:param cpu_count: Number of CPUs allocated to the sandbox
:param memory_mb: Amount of memory in MB allocated to the sandbox
:param skip_cache: If True, forces a complete rebuild ignoring cache
:return: BuildInfo containing the template ID and build ID
Example
```python
from e2b import Template
template = (
Template()
.from_python_image('3')
.run_cmd('echo "test"')
.set_start_cmd('echo "Hello"', 'sleep 1')
)
# Build with single tag
build_info = Template.build_in_background(template, 'my-python-env:v1.0')
# Build with multiple tags
build_info = Template.build_in_background(template, 'my-python-env', tags=['v1.1.0', 'stable'])
```
"""
name = normalize_build_arguments(name, alias)
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
return Template._build(
api_client,
template,
name,
tags=tags,
cpu_count=cpu_count,
memory_mb=memory_mb,
skip_cache=skip_cache,
on_build_logs=on_build_logs,
# Only honor an explicitly set request_timeout for uploads;
# otherwise upload_file applies its 1-hour default.
request_timeout=opts.get("request_timeout"),
)
@staticmethod
def get_build_status(
build_info: BuildInfo,
logs_offset: int = 0,
**opts: Unpack[ApiParams],
):
"""
Get the status of a build.
:param build_info: Build identifiers returned from build_in_background
:param logs_offset: Offset for fetching logs
:return: TemplateBuild containing the build status and logs
Example
```python
from e2b import Template
build_info = Template.build_in_background(template, alias='my-template')
status = Template.get_build_status(build_info, logs_offset=0)
```
"""
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
return get_build_status(
api_client,
build_info.template_id,
build_info.build_id,
logs_offset,
)
@staticmethod
def exists(
name: str,
**opts: Unpack[ApiParams],
) -> bool:
"""
Check if a template with the given name exists.
:param name: Template name to check
:return: True if the name exists, False otherwise
Example
```python
from e2b import Template
exists = Template.exists('my-python-env')
if exists:
print('Template exists!')
```
"""
return Template.alias_exists(name, **opts)
@staticmethod
def alias_exists(
alias: str,
**opts: Unpack[ApiParams],
) -> bool:
"""
Check if a template with the given alias exists.
Deprecated Use `exists` instead.
:param alias: Template alias to check
:return: True if the alias exists, False otherwise
Example
```python
from e2b import Template
exists = Template.alias_exists('my-python-env')
if exists:
print('Template exists!')
```
"""
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
return check_alias_exists(api_client, alias)
@staticmethod
def assign_tags(
target_name: str,
tags: Union[str, List[str]],
**opts: Unpack[ApiParams],
) -> TemplateTagInfo:
"""
Assign tag(s) to an existing template build.
:param target_name: Template name in 'name:tag' format (the source build to tag from)
:param tags: Tag or tags to assign
:return: TemplateTagInfo with build_id and assigned tags
Example
```python
from e2b import Template
# Assign a single tag
result = Template.assign_tags('my-template:v1.0', 'production')
# Assign multiple tags
result = Template.assign_tags('my-template:v1.0', ['production', 'stable'])
```
"""
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
normalized_tags = [tags] if isinstance(tags, str) else tags
return assign_tags(api_client, target_name, normalized_tags)
@staticmethod
def remove_tags(
name: str,
tags: Union[str, List[str]],
**opts: Unpack[ApiParams],
) -> None:
"""
Remove tag(s) from a template.
:param name: Template name
:param tags: Tag or tags to remove
Example
```python
from e2b import Template
# Remove a single tag
Template.remove_tags('my-template', 'production')
# Remove multiple tags
Template.remove_tags('my-template', ['production', 'stable'])
```
"""
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
normalized_tags = [tags] if isinstance(tags, str) else tags
remove_tags(api_client, name, normalized_tags)
@staticmethod
def get_tags(
template_id: str,
**opts: Unpack[ApiParams],
) -> List[TemplateTag]:
"""
Get all tags for a template.
:param template_id: Template ID or name
:return: List of TemplateTag with tag name, build_id, and created_at
Example
```python
from e2b import Template
tags = Template.get_tags('my-template')
for tag in tags:
print(f"Tag: {tag.tag}, Build: {tag.build_id}, Created: {tag.created_at}")
```
"""
config = ConnectionConfig(**opts)
api_client = get_api_client(
config,
)
return get_template_tags(api_client, template_id)