chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
import asyncio
|
||||
import os
|
||||
from types import TracebackType
|
||||
from typing import Callable, Optional, List, Union
|
||||
|
||||
import httpx
|
||||
|
||||
from e2b.api import handle_api_exception
|
||||
from e2b.io_utils import aiter_io_chunks
|
||||
from e2b.api.client.api.templates import (
|
||||
post_v3_templates,
|
||||
get_templates_template_id_files_hash,
|
||||
post_v_2_templates_template_id_builds_build_id,
|
||||
get_templates_template_id_builds_build_id_status,
|
||||
get_templates_aliases_alias,
|
||||
)
|
||||
from e2b.api.client.api.tags import (
|
||||
post_templates_tags,
|
||||
delete_templates_tags,
|
||||
get_templates_template_id_tags,
|
||||
)
|
||||
from e2b.api.client.client import AuthenticatedClient
|
||||
from e2b.api.client.models import (
|
||||
TemplateBuildRequestV3,
|
||||
TemplateBuildStartV2,
|
||||
TemplateBuildFileUpload,
|
||||
Error,
|
||||
AssignTemplateTagsRequest,
|
||||
DeleteTemplateTagsRequest,
|
||||
)
|
||||
from e2b.api.client.types import UNSET, Unset
|
||||
from e2b.exceptions import BuildException, FileUploadException, TemplateException
|
||||
from e2b.template.logger import LogEntry
|
||||
from e2b.template.types import (
|
||||
TemplateType,
|
||||
BuildStatusReason,
|
||||
TemplateBuildStatus,
|
||||
TemplateBuildStatusResponse,
|
||||
TemplateTag,
|
||||
TemplateTagInfo,
|
||||
)
|
||||
from e2b.template.consts import FILE_UPLOAD_TIMEOUT_SECONDS
|
||||
from e2b.template.utils import get_build_step_index, tar_file_stream
|
||||
|
||||
|
||||
async def request_build(
|
||||
client: AuthenticatedClient,
|
||||
name: str,
|
||||
tags: Optional[List[str]],
|
||||
cpu_count: int,
|
||||
memory_mb: int,
|
||||
):
|
||||
res = await post_v3_templates.asyncio_detailed(
|
||||
client=client,
|
||||
body=TemplateBuildRequestV3(
|
||||
name=name,
|
||||
tags=tags if tags else UNSET,
|
||||
cpu_count=cpu_count,
|
||||
memory_mb=memory_mb,
|
||||
),
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, BuildException)
|
||||
|
||||
if isinstance(res.parsed, Error):
|
||||
raise BuildException(f"API error: {res.parsed.message}")
|
||||
|
||||
if res.parsed is None:
|
||||
raise BuildException("Failed to request build")
|
||||
|
||||
return res.parsed
|
||||
|
||||
|
||||
async def get_file_upload_link(
|
||||
client: AuthenticatedClient,
|
||||
template_id: str,
|
||||
files_hash: str,
|
||||
stack_trace: Optional[TracebackType] = None,
|
||||
) -> TemplateBuildFileUpload:
|
||||
res = await get_templates_template_id_files_hash.asyncio_detailed(
|
||||
template_id=template_id,
|
||||
hash_=files_hash,
|
||||
client=client,
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, FileUploadException, stack_trace)
|
||||
|
||||
if isinstance(res.parsed, Error):
|
||||
raise FileUploadException(f"API error: {res.parsed.message}").with_traceback(
|
||||
stack_trace
|
||||
)
|
||||
|
||||
if res.parsed is None:
|
||||
raise FileUploadException("Failed to get file upload link").with_traceback(
|
||||
stack_trace
|
||||
)
|
||||
|
||||
return res.parsed
|
||||
|
||||
|
||||
async def upload_file(
|
||||
api_client: AuthenticatedClient,
|
||||
file_name: str,
|
||||
context_path: str,
|
||||
url: str,
|
||||
ignore_patterns: List[str],
|
||||
resolve_symlinks: bool,
|
||||
gzip: bool,
|
||||
stack_trace: Optional[TracebackType],
|
||||
request_timeout: Optional[float] = None,
|
||||
):
|
||||
# Uploading a large build-context archive can take far longer than the 60s
|
||||
# general API timeout, so default to a 1-hour upload timeout unless the
|
||||
# caller set an explicit request_timeout. Matches the JS SDK
|
||||
# (FILE_UPLOAD_TIMEOUT_MS).
|
||||
upload_timeout = (
|
||||
request_timeout if request_timeout is not None else FILE_UPLOAD_TIMEOUT_SECONDS
|
||||
)
|
||||
try:
|
||||
tar_file = tar_file_stream(
|
||||
file_name, context_path, ignore_patterns, resolve_symlinks, gzip
|
||||
)
|
||||
try:
|
||||
size = os.fstat(tar_file.fileno()).st_size
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(upload_timeout),
|
||||
verify=api_client._verify_ssl,
|
||||
follow_redirects=api_client._follow_redirects,
|
||||
proxy=getattr(api_client, "_proxy", None),
|
||||
http2=False,
|
||||
) as client:
|
||||
# Stream the archive from disk via an async iterator. The
|
||||
# explicit Content-Length suppresses chunked transfer
|
||||
# encoding, which S3 presigned URLs reject.
|
||||
response = await client.put(
|
||||
url,
|
||||
content=aiter_io_chunks(tar_file),
|
||||
headers={"Content-Length": str(size)},
|
||||
)
|
||||
response.raise_for_status()
|
||||
finally:
|
||||
# Closing the spooled temp file is best-effort: a failure here
|
||||
# must not mask a successful upload as a FileUploadException,
|
||||
# nor overwrite a real upload error.
|
||||
try:
|
||||
tar_file.close()
|
||||
except Exception:
|
||||
pass
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
|
||||
stack_trace
|
||||
)
|
||||
except Exception as e:
|
||||
raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
|
||||
stack_trace
|
||||
)
|
||||
|
||||
|
||||
async def trigger_build(
|
||||
client: AuthenticatedClient,
|
||||
template_id: str,
|
||||
build_id: str,
|
||||
template: TemplateType,
|
||||
) -> None:
|
||||
# Convert template dict to TemplateBuildStartV2 model using from_dict
|
||||
template_data = TemplateBuildStartV2.from_dict(template)
|
||||
|
||||
res = await post_v_2_templates_template_id_builds_build_id.asyncio_detailed(
|
||||
template_id=template_id,
|
||||
build_id=build_id,
|
||||
client=client,
|
||||
body=template_data,
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, BuildException)
|
||||
|
||||
|
||||
def _map_log_entry(entry) -> LogEntry:
|
||||
"""Map API log entry to LogEntry type."""
|
||||
return LogEntry(
|
||||
timestamp=entry.timestamp,
|
||||
level=entry.level.value,
|
||||
message=entry.message,
|
||||
)
|
||||
|
||||
|
||||
def _map_build_status_reason(reason) -> Optional[BuildStatusReason]:
|
||||
"""Map API build status reason to custom BuildStatusReason type."""
|
||||
if reason is None or isinstance(reason, Unset):
|
||||
return None
|
||||
return BuildStatusReason(
|
||||
message=reason.message,
|
||||
step=reason.step if not isinstance(reason.step, Unset) else None,
|
||||
log_entries=[
|
||||
_map_log_entry(e)
|
||||
for e in (
|
||||
reason.log_entries
|
||||
if not isinstance(reason.log_entries, Unset) and reason.log_entries
|
||||
else []
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def get_build_status(
|
||||
client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int
|
||||
) -> TemplateBuildStatusResponse:
|
||||
res = await get_templates_template_id_builds_build_id_status.asyncio_detailed(
|
||||
template_id=template_id,
|
||||
build_id=build_id,
|
||||
client=client,
|
||||
logs_offset=logs_offset,
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, BuildException)
|
||||
|
||||
if isinstance(res.parsed, Error):
|
||||
raise BuildException(f"API error: {res.parsed.message}")
|
||||
|
||||
if res.parsed is None:
|
||||
raise BuildException("Failed to get build status")
|
||||
|
||||
return TemplateBuildStatusResponse(
|
||||
build_id=res.parsed.build_id,
|
||||
template_id=res.parsed.template_id,
|
||||
status=TemplateBuildStatus(res.parsed.status.value),
|
||||
log_entries=[_map_log_entry(e) for e in res.parsed.log_entries],
|
||||
logs=res.parsed.logs,
|
||||
reason=_map_build_status_reason(res.parsed.reason),
|
||||
)
|
||||
|
||||
|
||||
async def wait_for_build_finish(
|
||||
client: AuthenticatedClient,
|
||||
template_id: str,
|
||||
build_id: str,
|
||||
on_build_logs: Optional[Callable[[LogEntry], None]] = None,
|
||||
logs_refresh_frequency: float = 0.2,
|
||||
stack_traces: List[Union[TracebackType, None]] = [],
|
||||
):
|
||||
logs_offset = 0
|
||||
status = TemplateBuildStatus.BUILDING
|
||||
|
||||
async def poll_status() -> TemplateBuildStatusResponse:
|
||||
nonlocal logs_offset
|
||||
build_status = await get_build_status(
|
||||
client, template_id, build_id, logs_offset
|
||||
)
|
||||
|
||||
logs_offset += len(build_status.log_entries)
|
||||
|
||||
for log_entry in build_status.log_entries:
|
||||
if on_build_logs:
|
||||
on_build_logs(log_entry)
|
||||
|
||||
return build_status
|
||||
|
||||
while status in [TemplateBuildStatus.BUILDING, TemplateBuildStatus.WAITING]:
|
||||
build_status = await poll_status()
|
||||
|
||||
status = build_status.status
|
||||
|
||||
if status in [TemplateBuildStatus.READY, TemplateBuildStatus.ERROR]:
|
||||
# The status endpoint returns at most 100 log entries per call, so
|
||||
# the terminal response may not include the last logs - keep
|
||||
# fetching until they are drained.
|
||||
tail_status = build_status
|
||||
while len(tail_status.log_entries) > 0:
|
||||
tail_status = await poll_status()
|
||||
|
||||
if status == TemplateBuildStatus.READY:
|
||||
return
|
||||
|
||||
traceback = None
|
||||
if build_status.reason and build_status.reason.step:
|
||||
# Find the corresponding stack trace for the failed step
|
||||
step_index = get_build_step_index(
|
||||
build_status.reason.step, len(stack_traces)
|
||||
)
|
||||
if step_index < len(stack_traces):
|
||||
traceback = stack_traces[step_index]
|
||||
|
||||
raise BuildException(
|
||||
build_status.reason.message if build_status.reason else "Build failed"
|
||||
).with_traceback(traceback)
|
||||
|
||||
# Wait for a short period before checking the status again
|
||||
await asyncio.sleep(logs_refresh_frequency)
|
||||
|
||||
raise BuildException("Unknown build error occurred.")
|
||||
|
||||
|
||||
async def check_alias_exists(client: AuthenticatedClient, alias: str) -> bool:
|
||||
"""
|
||||
Check if a template with the given alias exists.
|
||||
|
||||
Args:
|
||||
client: Authenticated API client
|
||||
alias: Template alias to check
|
||||
|
||||
Returns:
|
||||
True if the alias exists, False otherwise
|
||||
"""
|
||||
res = await get_templates_aliases_alias.asyncio_detailed(
|
||||
alias=alias,
|
||||
client=client,
|
||||
)
|
||||
|
||||
# If we get a NotFound, the alias doesn't exist
|
||||
if res.status_code == 404:
|
||||
return False
|
||||
|
||||
# If we get a Forbidden, alias exists, but you are not owner
|
||||
if res.status_code == 403:
|
||||
return True
|
||||
|
||||
# Handle other errors
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, TemplateException)
|
||||
|
||||
# If we get Ok with data, you are owner and the alias exists
|
||||
return res.parsed is not None
|
||||
|
||||
|
||||
async def assign_tags(
|
||||
client: AuthenticatedClient, target_name: str, tags: List[str]
|
||||
) -> TemplateTagInfo:
|
||||
"""
|
||||
Assign tag(s) to an existing template build.
|
||||
|
||||
Args:
|
||||
client: Authenticated API client
|
||||
target_name: Template name in 'name:tag' format (the source build to tag from)
|
||||
tags: Tags to assign
|
||||
|
||||
Returns:
|
||||
TemplateTagInfo with build_id and assigned tags
|
||||
"""
|
||||
res = await post_templates_tags.asyncio_detailed(
|
||||
client=client,
|
||||
body=AssignTemplateTagsRequest(
|
||||
target=target_name,
|
||||
tags=tags,
|
||||
),
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, TemplateException)
|
||||
|
||||
if isinstance(res.parsed, Error):
|
||||
raise TemplateException(f"API error: {res.parsed.message}")
|
||||
|
||||
if res.parsed is None:
|
||||
raise TemplateException("Failed to assign tags")
|
||||
|
||||
return TemplateTagInfo(
|
||||
build_id=str(res.parsed.build_id),
|
||||
tags=res.parsed.tags,
|
||||
)
|
||||
|
||||
|
||||
async def remove_tags(client: AuthenticatedClient, name: str, tags: List[str]) -> None:
|
||||
"""
|
||||
Remove tag(s) from a template.
|
||||
|
||||
Args:
|
||||
client: Authenticated API client
|
||||
name: Template name
|
||||
tags: List of tags to remove
|
||||
"""
|
||||
res = await delete_templates_tags.asyncio_detailed(
|
||||
client=client,
|
||||
body=DeleteTemplateTagsRequest(
|
||||
name=name,
|
||||
tags=tags,
|
||||
),
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, TemplateException)
|
||||
|
||||
|
||||
async def get_template_tags(
|
||||
client: AuthenticatedClient, template_id: str
|
||||
) -> List[TemplateTag]:
|
||||
"""
|
||||
Get all tags for a template.
|
||||
|
||||
Args:
|
||||
client: Authenticated API client
|
||||
template_id: Template ID or name
|
||||
"""
|
||||
res = await get_templates_template_id_tags.asyncio_detailed(
|
||||
template_id=template_id,
|
||||
client=client,
|
||||
)
|
||||
|
||||
if res.status_code >= 300:
|
||||
raise handle_api_exception(res, TemplateException)
|
||||
|
||||
if isinstance(res.parsed, Error):
|
||||
raise TemplateException(f"API error: {res.parsed.message}")
|
||||
|
||||
if res.parsed is None:
|
||||
raise TemplateException("Failed to get template tags")
|
||||
|
||||
return [
|
||||
TemplateTag(
|
||||
tag=item.tag,
|
||||
build_id=str(item.build_id),
|
||||
created_at=item.created_at,
|
||||
)
|
||||
for item in res.parsed
|
||||
]
|
||||
@@ -0,0 +1,528 @@
|
||||
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.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.utils import normalize_build_arguments, read_dockerignore
|
||||
|
||||
from .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.api.client_async import get_api_client
|
||||
|
||||
|
||||
class AsyncTemplate(TemplateBase):
|
||||
"""
|
||||
Asynchronous template builder for E2B sandboxes.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async 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 = await 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 = await 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
|
||||
):
|
||||
await 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...",
|
||||
)
|
||||
)
|
||||
|
||||
await 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
|
||||
async 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 AsyncTemplate
|
||||
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_python_image('3')
|
||||
.copy('requirements.txt', '/home/user/')
|
||||
.run_cmd('pip install -r /home/user/requirements.txt')
|
||||
)
|
||||
|
||||
# Build with single tag
|
||||
await AsyncTemplate.build(template, 'my-python-env:v1.0')
|
||||
|
||||
# Build with multiple tags
|
||||
await AsyncTemplate.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 = await AsyncTemplate._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...",
|
||||
)
|
||||
)
|
||||
|
||||
await 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
|
||||
async 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 AsyncTemplate
|
||||
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_python_image('3')
|
||||
.run_cmd('echo "test"')
|
||||
.set_start_cmd('echo "Hello"', 'sleep 1')
|
||||
)
|
||||
|
||||
# Build with single tag
|
||||
build_info = await AsyncTemplate.build_in_background(template, 'my-python-env:v1.0')
|
||||
|
||||
# Build with multiple tags
|
||||
build_info = await AsyncTemplate.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 await AsyncTemplate._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
|
||||
async 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 AsyncTemplate
|
||||
|
||||
build_info = await AsyncTemplate.build_in_background(template, alias='my-template')
|
||||
status = await AsyncTemplate.get_build_status(build_info, logs_offset=0)
|
||||
```
|
||||
"""
|
||||
config = ConnectionConfig(**opts)
|
||||
api_client = get_api_client(
|
||||
config,
|
||||
)
|
||||
return await get_build_status(
|
||||
api_client,
|
||||
build_info.template_id,
|
||||
build_info.build_id,
|
||||
logs_offset,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async 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 AsyncTemplate
|
||||
|
||||
exists = await AsyncTemplate.exists('my-python-env')
|
||||
if exists:
|
||||
print('Template exists!')
|
||||
```
|
||||
"""
|
||||
|
||||
return await AsyncTemplate.alias_exists(name, **opts)
|
||||
|
||||
@staticmethod
|
||||
async 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 AsyncTemplate
|
||||
|
||||
exists = await AsyncTemplate.alias_exists('my-python-env')
|
||||
if exists:
|
||||
print('Template exists!')
|
||||
```
|
||||
"""
|
||||
config = ConnectionConfig(**opts)
|
||||
api_client = get_api_client(
|
||||
config,
|
||||
)
|
||||
|
||||
return await check_alias_exists(api_client, alias)
|
||||
|
||||
@staticmethod
|
||||
async 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 AsyncTemplate
|
||||
|
||||
# Assign a single tag
|
||||
result = await AsyncTemplate.assign_tags('my-template:v1.0', 'production')
|
||||
|
||||
# Assign multiple tags
|
||||
result = await AsyncTemplate.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 await assign_tags(api_client, target_name, normalized_tags)
|
||||
|
||||
@staticmethod
|
||||
async 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 AsyncTemplate
|
||||
|
||||
# Remove a single tag
|
||||
await AsyncTemplate.remove_tags('my-template', 'production')
|
||||
|
||||
# Remove multiple tags
|
||||
await AsyncTemplate.remove_tags('my-template', ['production', 'stable'])
|
||||
```
|
||||
"""
|
||||
config = ConnectionConfig(**opts)
|
||||
api_client = get_api_client(
|
||||
config,
|
||||
)
|
||||
|
||||
normalized_tags = [tags] if isinstance(tags, str) else tags
|
||||
await remove_tags(api_client, name, normalized_tags)
|
||||
|
||||
@staticmethod
|
||||
async 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 AsyncTemplate
|
||||
|
||||
tags = await AsyncTemplate.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 await get_template_tags(api_client, template_id)
|
||||
Reference in New Issue
Block a user