Python client SDK for Boxer - a sandboxed container execution service backed by gVisor.
pip install boxer-sdkRequires Python 3.9+ and a running Boxer server.
from boxer import BoxerClient
with BoxerClient("http://localhost:8080") as client:
result = client.run(
image="python:3.12-slim",
cmd=["python3", "-c", "print('hello world')"],
)
print(result.stdout) # hello world
print(result.exit_code) # 0
print(result.wall_ms) # e.g. 312result = client.run(
image="python:3.12-slim",
cmd=["python3", "-c", "print('hello world')"],
)
print(result.stdout) # hello worldresult = client.run(
image="node:20-slim",
cmd=["node", "-e", "console.log('hello world')"],
)
print(result.stdout) # hello worldresult = client.run(
image="perl:5.38-slim",
cmd=["perl", "-e", "print 'hello world\n'"],
)
print(result.stdout) # hello worldUpload a file to the Boxer file store, then reference it by path in run.
The file is bind-mounted read-only at /<remote_path> inside the container.
# Python
with open("script.py", "rb") as f:
client.upload_file("script.py", f)
result = client.run(
image="python:3.12-slim",
cmd=["python3", "/script.py"],
files=["script.py"],
)
# Node.js
with open("app.js", "rb") as f:
client.upload_file("app.js", f)
result = client.run(
image="node:20-slim",
cmd=["node", "/app.js"],
files=["app.js"],
)
# Perl
with open("hello.pl", "rb") as f:
client.upload_file("hello.pl", f)
result = client.run(
image="perl:5.38-slim",
cmd=["perl", "/hello.pl"],
files=["hello.pl"],
)Any file the container writes to /output/ is automatically captured and
retrievable via download_file after the run completes.
# Script that writes a file to /output/
code = """
import os, json
os.makedirs('/output', exist_ok=True)
with open('/output/result.json', 'w') as f:
json.dump({'message': 'hello world', 'value': 42}, f)
"""
with open("compute.py", "rb") as f:
client.upload_file("compute.py", f)
result = client.run(
image="python:3.12-slim",
cmd=["python3", "/compute.py"],
files=["compute.py"],
)
# Download the file the container wrote
data = client.download_file(f"output/{result.exec_id}/result.json")
print(data) # b'{"message": "hello world", "value": 42}'The output path pattern is always output/<exec_id>/<filename>.
from boxer import ResourceLimits
limits = ResourceLimits(
cpu_cores=0.5,
memory_mb=128,
wall_clock_secs=10,
)
result = client.run(
image="python:3.12-slim",
cmd=["python3", "-c", "print('done')"],
limits=limits,
)Every method on BoxerClient has an await-able equivalent on AsyncBoxerClient:
import asyncio
from boxer import AsyncBoxerClient
async def main():
async with AsyncBoxerClient("http://localhost:8080") as client:
result = await client.run(
image="python:3.12-slim",
cmd=["python3", "-c", "print('hello world')"],
)
print(result.stdout)
asyncio.run(main())from boxer import BoxerAPIError, BoxerTimeoutError, BoxerOutputLimitError
try:
result = client.run(
image="python:3.12-slim",
cmd=["python3", "-c", "while True: pass"],
limits=ResourceLimits(wall_clock_secs=5),
)
except BoxerTimeoutError:
print("execution timed out")
except BoxerOutputLimitError:
print("output exceeded size limit")
except BoxerAPIError as e:
print(f"API error {e.status_code}: {e}")Tests require a live Boxer server:
pip install -e ".[dev]"
BOXER_URL=http://localhost:8080 pytest tests/ -vWithout BOXER_URL all tests are skipped automatically.