- Rename Capsule.kill/AsyncCapsule.kill to destroy for frontend consistency - Add Sandbox deprecation alias to wrenn.code_interpreter module - run_code text falls back to stripped stdout when no expression result - Strip quotes from string expression results (matching e2b behavior) - _ensure_kernel reuses existing Jupyter kernels before creating new ones - Rewrite README with complete examples for capsules and code interpreter - Remove stale AGENTS.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrenn Python SDK
Python client for the Wrenn microVM platform. Create isolated capsules, execute commands, manage files, run interactive terminals, and execute persistent code -- all from Python.
Designed as a drop-in replacement for e2b. If you're migrating, just swap your imports.
Installation
pip install wrenn
Requires Python 3.13+.
Authentication
Set the WRENN_API_KEY environment variable:
export WRENN_API_KEY="wrn_your_api_key_here"
Optionally override the API base URL:
export WRENN_BASE_URL="https://app.wrenn.dev/api" # default
You can also pass credentials directly:
from wrenn import Capsule
capsule = Capsule(api_key="wrn_...", base_url="https://...")
Wrenn Capsules
Quick Start
from wrenn import Capsule
# Create a capsule (reads WRENN_API_KEY from env)
with Capsule(template="minimal") as capsule:
result = capsule.commands.run("echo hello")
print(result.stdout) # "hello\n"
Creating Capsules
from wrenn import Capsule
# Direct construction (creates immediately)
capsule = Capsule()
capsule = Capsule(template="base-python", vcpus=2, memory_mb=1024, timeout=300)
# With auto-wait (blocks until capsule is running)
capsule = Capsule(template="minimal", wait=True)
# Via factory classmethod
capsule = Capsule.create(template="minimal", wait=True)
Context Manager
Use capsules as context managers for automatic cleanup (destroys capsule on exit):
with Capsule(template="minimal", wait=True) as capsule:
capsule.commands.run("echo hello")
# capsule is automatically destroyed
Connecting to Existing Capsules
Attach to a running capsule by ID. If it's paused, it will be resumed automatically:
capsule = Capsule.connect("cl-abc123")
result = capsule.commands.run("echo still running")
For code interpreter capsules:
from wrenn.code_interpreter import Capsule as CodeCapsule
capsule = CodeCapsule.connect("cl-abc123")
result = capsule.run_code("print('reconnected')")
Lifecycle Management
# Instance methods
capsule.pause()
capsule.resume()
capsule.destroy()
capsule.ping() # reset inactivity timer
capsule.wait_ready() # block until running
info = capsule.get_info()
print(info.status) # "running"
print(capsule.is_running()) # True
# Static methods (no instance needed)
Capsule.destroy("cl-abc123", api_key="wrn_...")
Capsule.pause("cl-abc123")
Capsule.resume("cl-abc123")
info = Capsule.get_info("cl-abc123")
# List all capsules
capsules = Capsule.list()
Command Execution
Commands are accessed via capsule.commands:
# Foreground (blocks until complete)
result = capsule.commands.run("python -c 'print(42)'")
print(result.stdout) # "42\n"
print(result.stderr) # ""
print(result.exit_code) # 0
print(result.duration_ms) # 35
# With options
result = capsule.commands.run(
"python train.py",
timeout=120,
envs={"CUDA_VISIBLE_DEVICES": "0"},
cwd="/app",
)
# Background process
handle = capsule.commands.run("python server.py", background=True)
print(handle.pid) # 1234
print(handle.tag) # "exec-abc123"
Streaming Output
import sys
# Stream a new command
for event in capsule.commands.stream("python", args=["-u", "train.py"]):
match event.type:
case "stdout":
print(event.data, end="")
case "stderr":
print(event.data, end="", file=sys.stderr)
case "exit":
print(f"\nExited with code {event.exit_code}")
# Connect to a running background process
for event in capsule.commands.connect(handle.pid):
if event.type == "stdout":
print(event.data, end="")
Process Management
# List running processes
for proc in capsule.commands.list():
print(proc.pid, proc.cmd, proc.tag)
# Kill a process
capsule.commands.kill(pid=1234)
Filesystem
Files are accessed via capsule.files:
# Write and read files
capsule.files.write("/app/main.py", "print('hello')")
content = capsule.files.read("/app/main.py") # str
raw = capsule.files.read_bytes("/app/main.py") # bytes
# Check existence
capsule.files.exists("/app/main.py") # True
# List directory
entries = capsule.files.list("/home/user", depth=1)
for entry in entries:
print(entry.name, entry.type, entry.size)
# Create directory
capsule.files.make_dir("/app/data")
# Remove file or directory
capsule.files.remove("/app/old_data")
Streaming (Large Files)
# Streaming upload
def chunks():
yield b"chunk1"
yield b"chunk2"
capsule.files.upload_stream("/data/large.bin", chunks())
# Streaming download
for chunk in capsule.files.download_stream("/data/large.bin"):
process(chunk)
Interactive Terminal (PTY)
import sys
with capsule.pty(cmd="/bin/bash", cols=120, rows=40, cwd="/home/user") as term:
term.write(b"ls -la\n")
for event in term:
if event.type == "output":
sys.stdout.buffer.write(event.data)
elif event.type == "exit":
break
# Reconnect to an existing session
with capsule.pty_connect(term.tag) as term:
term.write(b"echo reconnected\n")
PtySession methods:
| Method | Description |
|---|---|
write(data: bytes) |
Send raw bytes to stdin |
resize(cols, rows) |
Resize the terminal |
kill() |
Send SIGKILL to the process |
tag |
Session tag (after started event) |
pid |
Process PID (after started event) |
Proxy URL
Access services running inside a capsule:
url = capsule.get_url(8080)
# "wss://8080-cl-abc123.app.wrenn.dev"
Snapshots
Create reusable templates from running capsules:
template = capsule.create_snapshot(name="my-template", overwrite=True)
Code Interpreter
The wrenn.code_interpreter module provides a specialized capsule for stateful code execution via a persistent Jupyter kernel.
Quick Start
from wrenn.code_interpreter import Capsule
with Capsule(wait=True) as capsule:
result = capsule.run_code("print('hello')")
print(result.text) # "hello"
Stateful Execution
Variables, imports, and function definitions persist across run_code calls:
from wrenn.code_interpreter import Capsule
with Capsule(wait=True) as capsule:
capsule.run_code("x = 42")
result = capsule.run_code("x * 2")
print(result.text) # "84"
capsule.run_code("import math")
result = capsule.run_code("math.pi")
print(result.text) # "3.141592653589793"
capsule.run_code("def greet(name): return f'hello {name}'")
result = capsule.run_code("greet('world')")
print(result.text) # "hello world"
The text field returns the expression result when available. For print() calls (which produce no expression result), it falls back to the stripped stdout output.
Error Handling in Code
result = capsule.run_code("1 / 0")
print(result.error) # "ZeroDivisionError: division by zero\n..."
Rich Output
result = capsule.run_code("""
import matplotlib.pyplot as plt
plt.plot([1, 2, 3])
plt.savefig('/tmp/plot.png')
plt.show()
""")
print(result.data) # {"image/png": "base64...", "text/plain": "..."}
Custom Templates
By default, code-runner-beta template is used. You can specify a custom template:
capsule = Capsule(template="my-custom-jupyter-template", wait=True)
result = capsule.run_code("print('running on custom template')")
CodeResult Fields
| Field | Type | Description |
|---|---|---|
text |
str | None |
Expression result, or stripped stdout if no expression result |
data |
dict | None |
Rich MIME bundle (e.g. {"image/png": "..."}) |
stdout |
str |
Raw accumulated stdout output |
stderr |
str |
Raw accumulated stderr output |
error |
str | None |
Error traceback string |
String expression results have quotes stripped automatically (e.g. 'hello' becomes hello).
Code Interpreter + Commands/Files
The code interpreter capsule inherits all standard capsule features:
from wrenn.code_interpreter import Capsule
with Capsule(wait=True) as capsule:
# Use run_code for Jupyter execution
capsule.run_code("import pandas as pd; df = pd.DataFrame({'a': [1,2,3]})")
capsule.run_code("df.to_csv('/tmp/data.csv', index=False)")
# Use standard file operations
content = capsule.files.read("/tmp/data.csv")
print(content)
# Use standard command execution
result = capsule.commands.run("wc -l /tmp/data.csv")
print(result.stdout)
Async Support
All operations have async variants via AsyncCapsule:
Async Capsule
from wrenn import AsyncCapsule
async with await AsyncCapsule.create(template="minimal", wait=True) as capsule:
result = await capsule.commands.run("echo hello")
print(result.stdout)
await capsule.files.write("/app/file.txt", "data")
entries = await capsule.files.list("/app")
await capsule.pause()
await capsule.resume()
Async Code Interpreter
from wrenn.code_interpreter import AsyncCapsule
async with await AsyncCapsule.create(wait=True) as capsule:
result = await capsule.run_code("2 + 2")
print(result.text) # "4"
Async PTY
async with capsule.pty(cmd="/bin/bash") as term:
await term.write(b"ls -la\n")
async for event in term:
if event.type == "output":
sys.stdout.buffer.write(event.data)
Error Handling
The SDK maps server error codes to typed exceptions:
from wrenn import (
WrennError,
WrennValidationError, # 400
WrennAuthenticationError, # 401
WrennForbiddenError, # 403
WrennNotFoundError, # 404
WrennConflictError, # 409
WrennHostHasCapsulesError, # 409 (host has running capsules)
WrennAgentError, # 502
WrennInternalError, # 500
WrennHostUnavailableError, # 503
)
try:
Capsule.get_info("nonexistent")
except WrennNotFoundError as e:
print(e.code) # "not_found"
print(e.message) # "capsule not found"
print(e.status_code) # 404
All exceptions inherit from WrennError and expose .code, .message, and .status_code.
Migrating from e2b
Replace your imports:
# Before
from e2b import Sandbox
sandbox = Sandbox()
# After
from wrenn import Capsule
capsule = Capsule()
For code interpreter:
# Before
from e2b_code_interpreter import Sandbox
sandbox = Sandbox()
result = sandbox.run_code("print('hello')")
# After
from wrenn.code_interpreter import Capsule
capsule = Capsule()
result = capsule.run_code("print('hello')")
The Sandbox name is available as a deprecated alias in both modules:
from wrenn import Sandbox # works, emits FutureWarning
from wrenn.code_interpreter import Sandbox # works, emits FutureWarning
Low-Level Client
For direct API access, use WrennClient / AsyncWrennClient:
from wrenn import WrennClient
with WrennClient(api_key="wrn_...") as client:
capsule = client.capsules.create(template="minimal")
client.capsules.pause(capsule.id)
client.capsules.resume(capsule.id)
client.capsules.ping(capsule.id)
client.capsules.destroy(capsule.id)
# Snapshots
template = client.snapshots.create(capsule_id="cl-abc", name="my-snap")
templates = client.snapshots.list()
client.snapshots.delete("my-snap")
Development
This project uses uv for dependency management.
# Install dependencies
uv sync
# Run linting
make lint
# Run unit tests
make test
# Run all tests (including integration)
make test-integration
Running Integration Tests
Integration tests require a live Wrenn server:
export WRENN_API_KEY="wrn_..."
export WRENN_BASE_URL="http://localhost:8080" # optional
make test-integration
License
MIT