Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/python-sdk/src/bashlet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
TimeoutError,
)
from .types import (
BackendType,
BashletOptions,
CommandResult,
CreateSessionOptions,
DockerOptions,
EnvVar,
ExecOptions,
Mount,
Expand All @@ -52,8 +54,10 @@
"Bashlet",
"AsyncBashlet",
# Types
"BackendType",
"BashletOptions",
"CreateSessionOptions",
"DockerOptions",
"ExecOptions",
"CommandResult",
"Session",
Expand Down
27 changes: 27 additions & 0 deletions packages/python-sdk/src/bashlet/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
TimeoutError,
)
from .types import (
BackendType,
BashletOptions,
CommandResult,
CreateSessionOptions,
Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(
workdir: str | None = None,
timeout: int = 300,
config_path: str | None = None,
backend: BackendType | str | None = None,
) -> None:
"""
Initialize an AsyncBashlet client.
Expand All @@ -71,9 +73,18 @@ def __init__(
workdir: Default working directory inside sandbox
timeout: Command timeout in seconds (default: 300)
config_path: Path to config file
backend: Sandbox backend to use (wasmer, firecracker, docker, auto)
"""
from .types import EnvVar

# Convert string backend to BackendType enum
backend_type: BackendType | None = None
if backend is not None:
if isinstance(backend, str):
backend_type = BackendType(backend)
else:
backend_type = backend

self._options = BashletOptions(
binary_path=binary_path,
preset=preset,
Expand All @@ -84,6 +95,7 @@ def __init__(
workdir=workdir,
timeout=timeout,
config_path=config_path,
backend=backend_type,
)

# =========================================================================
Expand All @@ -99,6 +111,7 @@ async def exec(
env_vars: list[tuple[str, str]] | None = None,
workdir: str | None = None,
timeout: int | None = None,
backend: BackendType | str | None = None,
) -> CommandResult:
"""
Execute a one-shot command in an isolated sandbox.
Expand All @@ -110,6 +123,7 @@ async def exec(
env_vars: Environment variables as (key, value) tuples
workdir: Working directory inside sandbox
timeout: Command timeout in seconds
backend: Sandbox backend to use (wasmer, firecracker, docker, auto)

Returns:
CommandResult with stdout, stderr, and exit code
Expand All @@ -118,6 +132,14 @@ async def exec(
>>> result = await bashlet.exec('echo "Hello World"')
>>> print(result.stdout) # "Hello World\\n"
"""
# Convert string backend to BackendType enum
backend_type: BackendType | None = None
if backend is not None:
if isinstance(backend, str):
backend_type = BackendType(backend)
else:
backend_type = backend

options = self._merge_options(
ExecOptions(
preset=preset,
Expand All @@ -127,6 +149,7 @@ async def exec(
env_vars=[],
workdir=workdir,
timeout=timeout,
backend=backend_type,
),
env_vars,
)
Expand Down Expand Up @@ -455,6 +478,7 @@ def _merge_options(
],
workdir=options.workdir or self._options.workdir,
timeout=options.timeout or self._options.timeout,
backend=options.backend or self._options.backend,
)

async def _run_command(
Expand Down Expand Up @@ -527,6 +551,9 @@ def _build_exec_args(self, command: str, options: ExecOptions) -> list[str]:
if options.preset:
args.extend(["--preset", options.preset])

if options.backend:
args.extend(["--backend", options.backend.value])

for mount in options.mounts:
args.extend(["--mount", mount.to_cli_arg()])

Expand Down
27 changes: 27 additions & 0 deletions packages/python-sdk/src/bashlet/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
TimeoutError,
)
from .types import (
BackendType,
BashletOptions,
CommandResult,
CreateSessionOptions,
Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(
workdir: str | None = None,
timeout: int = 300,
config_path: str | None = None,
backend: BackendType | str | None = None,
) -> None:
"""
Initialize a Bashlet client.
Expand All @@ -71,9 +73,18 @@ def __init__(
workdir: Default working directory inside sandbox
timeout: Command timeout in seconds (default: 300)
config_path: Path to config file
backend: Sandbox backend to use (wasmer, firecracker, docker, auto)
"""
from .types import EnvVar

# Convert string backend to BackendType enum
backend_type: BackendType | None = None
if backend is not None:
if isinstance(backend, str):
backend_type = BackendType(backend)
else:
backend_type = backend

self._options = BashletOptions(
binary_path=binary_path,
preset=preset,
Expand All @@ -84,6 +95,7 @@ def __init__(
workdir=workdir,
timeout=timeout,
config_path=config_path,
backend=backend_type,
)

# =========================================================================
Expand All @@ -99,6 +111,7 @@ def exec(
env_vars: list[tuple[str, str]] | None = None,
workdir: str | None = None,
timeout: int | None = None,
backend: BackendType | str | None = None,
) -> CommandResult:
"""
Execute a one-shot command in an isolated sandbox.
Expand All @@ -110,6 +123,7 @@ def exec(
env_vars: Environment variables as (key, value) tuples
workdir: Working directory inside sandbox
timeout: Command timeout in seconds
backend: Sandbox backend to use (wasmer, firecracker, docker, auto)

Returns:
CommandResult with stdout, stderr, and exit code
Expand All @@ -118,6 +132,14 @@ def exec(
>>> result = bashlet.exec('echo "Hello World"')
>>> print(result.stdout) # "Hello World\\n"
"""
# Convert string backend to BackendType enum
backend_type: BackendType | None = None
if backend is not None:
if isinstance(backend, str):
backend_type = BackendType(backend)
else:
backend_type = backend

options = self._merge_options(
ExecOptions(
preset=preset,
Expand All @@ -127,6 +149,7 @@ def exec(
env_vars=[],
workdir=workdir,
timeout=timeout,
backend=backend_type,
),
env_vars,
)
Expand Down Expand Up @@ -485,6 +508,7 @@ def _merge_options(
],
workdir=options.workdir or self._options.workdir,
timeout=options.timeout or self._options.timeout,
backend=options.backend or self._options.backend,
)

def _run_command(
Expand Down Expand Up @@ -542,6 +566,9 @@ def _build_exec_args(self, command: str, options: ExecOptions) -> list[str]:
if options.preset:
args.extend(["--preset", options.preset])

if options.backend:
args.extend(["--backend", options.backend.value])

for mount in options.mounts:
args.extend(["--mount", mount.to_cli_arg()])

Expand Down
30 changes: 30 additions & 0 deletions packages/python-sdk/src/bashlet/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,33 @@
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import TypedDict


class BackendType(str, Enum):
"""Sandbox backend type."""

WASMER = "wasmer"
FIRECRACKER = "firecracker"
DOCKER = "docker"
AUTO = "auto"


@dataclass
class DockerOptions:
"""Docker-specific configuration options."""

image: str | None = None
"""Custom Docker image name (default: bashlet-sandbox:latest)."""

enable_networking: bool = False
"""Enable networking in the container (default: false)."""

session_mode: bool = False
"""Enable session mode for persistent container (default: false)."""


class MountDict(TypedDict, total=False):
"""Mount configuration as a dictionary."""

Expand Down Expand Up @@ -83,6 +107,9 @@ class BashletOptions:
config_path: str | None = None
"""Path to config file."""

backend: BackendType | None = None
"""Sandbox backend to use (wasmer, firecracker, docker, auto)."""


@dataclass
class CreateSessionOptions:
Expand Down Expand Up @@ -126,6 +153,9 @@ class ExecOptions:
timeout: int | None = None
"""Command timeout in seconds."""

backend: BackendType | None = None
"""Sandbox backend to use (wasmer, firecracker, docker, auto)."""


@dataclass
class CommandResult:
Expand Down
46 changes: 45 additions & 1 deletion packages/python-sdk/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
CommandExecutionError,
TimeoutError,
)
from bashlet.types import Mount
from bashlet.types import BackendType, Mount


class TestAsyncBashletInit:
Expand Down Expand Up @@ -45,6 +45,14 @@ def test_with_all_options(self) -> None:
assert bashlet._options.binary_path == "/custom/bashlet"
assert bashlet._options.preset == "default"

def test_with_backend_string(self) -> None:
bashlet = AsyncBashlet(backend="docker")
assert bashlet._options.backend == BackendType.DOCKER

def test_with_backend_enum(self) -> None:
bashlet = AsyncBashlet(backend=BackendType.DOCKER)
assert bashlet._options.backend == BackendType.DOCKER


class TestAsyncBashletExec:
"""Tests for AsyncBashlet.exec method."""
Expand Down Expand Up @@ -104,6 +112,42 @@ async def test_exec_with_mounts(self) -> None:
assert "--mount" in call_args
assert "/host:/guest:ro" in call_args

@pytest.mark.asyncio
async def test_exec_with_docker_backend(self) -> None:
with patch("asyncio.create_subprocess_exec") as mock_create:
mock_process = AsyncMock()
mock_process.communicate.return_value = (
json.dumps({"stdout": "", "stderr": "", "exit_code": 0}).encode(),
b"",
)
mock_process.returncode = 0
mock_create.return_value = mock_process

bashlet = AsyncBashlet()
await bashlet.exec("ls", backend="docker")

call_args = mock_create.call_args[0]
assert "--backend" in call_args
assert "docker" in call_args

@pytest.mark.asyncio
async def test_exec_with_default_backend(self) -> None:
with patch("asyncio.create_subprocess_exec") as mock_create:
mock_process = AsyncMock()
mock_process.communicate.return_value = (
json.dumps({"stdout": "", "stderr": "", "exit_code": 0}).encode(),
b"",
)
mock_process.returncode = 0
mock_create.return_value = mock_process

bashlet = AsyncBashlet(backend="docker")
await bashlet.exec("ls")

call_args = mock_create.call_args[0]
assert "--backend" in call_args
assert "docker" in call_args

@pytest.mark.asyncio
async def test_exec_timeout(self) -> None:
with patch("asyncio.create_subprocess_exec") as mock_create:
Expand Down
Loading
Loading