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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.3.0 (2026-08-12)

- Breaking: `Sandbox.create()` / `acreate()` no longer take `image`. The server
stopped accepting a caller-set image: sandboxes always run the managed base
image, which now ships curl, wget, git, jq, build-essential, node and npm.
`Sandbox.image` still reports which image a sandbox is running.
- Docs: examples used `/work/...`, which the server rejects — only `/workspace`
is readable/writable through `fs`, and it is the only path that survives
`stop()`/`start()` (or the idle auto-stop). Corrected throughout, with the
persistence caveat spelled out in the README.

## 0.2.1 (2026-08-05)

- New: `Sandbox.catalog()` / `Sandbox.acatalog()` list the available sandbox
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ print(r.stdout, r.stderr, r.returncode)
out = sb.run_python("print(21 * 2)").check() # .check() raises on non-zero exit
print(out.stdout) # "42"

sb.fs.write("/work/in.csv", b"a,b\n1,2\n")
data = sb.fs.read("/work/in.csv")
sb.fs.write("/workspace/in.csv", b"a,b\n1,2\n")
data = sb.fs.read("/workspace/in.csv")

sb.stop() # frees compute, keeps disk; blocks until stopped
sb.start() # resumes on the same disk; blocks until running
sb.terminate() # deletes the sandbox (stays fetchable by id as "deleted" briefly)
```

`/workspace` is the only location `fs` accepts, and the only one that survives
`stop()`/`start()` — the rest of the filesystem comes back from the base image, so
packages installed at runtime are gone after a restart. Sandboxes also auto-stop
once idle for `timeout` (1 hour by default), which has the same effect.

Every network method has an async twin prefixed with `a`:

```python
Expand All @@ -61,8 +66,8 @@ Useful patterns:
```python
# Auto-terminate with a context manager
with Sandbox.create(plan="small") as sb:
sb.run_python("open('/work/out.txt', 'w').write('hi')")
print(sb.fs.read("/work/out.txt"))
sb.run_python("open('/workspace/out.txt', 'w').write('hi')")
print(sb.fs.read("/workspace/out.txt"))

# Find existing sandboxes
sb = Sandbox.from_id("sb_...")
Expand All @@ -73,8 +78,8 @@ for plan in Sandbox.catalog():
print(plan.id, plan.vcpu, plan.ram_gb, plan.price_per_hour)

# Large scripts: upload, then run
sb.fs.write("/work/script.py", open("script.py").read())
sb.exec("python3", "/work/script.py", timeout="30m")
sb.fs.write("/workspace/script.py", open("script.py").read())
sb.exec("python3", "/workspace/script.py", timeout="30m")
```

Errors are typed: `AuthenticationError` (401), `NotFoundError` (404),
Expand Down
2 changes: 1 addition & 1 deletion deepinfra/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.1"
__version__ = "0.3.0"
23 changes: 13 additions & 10 deletions deepinfra/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

from deepinfra import Sandbox

for plan in Sandbox.catalog():
print(plan.id, plan.vcpu, plan.ram_gb, plan.price_per_hour)

sb = Sandbox.create(plan="medium", timeout="10m")
r = sb.exec("bash", "-c", "pip install pandas && python -c 'import pandas'")
print(r.stdout, r.returncode)
sb.fs.write("/work/in.csv", b"a,b\n1,2\n")
data = sb.fs.read("/work/in.csv")
sb.fs.write("/workspace/in.csv", b"a,b\n1,2\n")
data = sb.fs.read("/workspace/in.csv")
sb.terminate()

Only paths under /workspace are readable/writable through fs, and /workspace is
the only directory that survives stop/start -- everything else reverts to the
base image.

Every network method has an async twin prefixed with "a" (exec/aexec,
create/acreate, ...).
"""
Expand Down Expand Up @@ -91,7 +98,6 @@ def __repr__(self) -> str:
def create(
cls,
*,
image: str = "",
plan: str = "",
timeout: Duration | None = None,
tags: Mapping[str, str] | None = None,
Expand All @@ -101,7 +107,7 @@ def create(
) -> Sandbox:
"""Create a sandbox; by default block until it is running."""
client = client or default_client()
reply = client.request(cls._create_spec(image, plan, timeout, tags)).json()
reply = client.request(cls._create_spec(plan, timeout, tags)).json()
sandbox = cls(SandboxInfo(sandbox_id=reply["sandbox_id"]), client=client)
if wait:
sandbox.wait_until_running(timeout=wait_timeout)
Expand All @@ -113,7 +119,6 @@ def create(
async def acreate(
cls,
*,
image: str = "",
plan: str = "",
timeout: Duration | None = None,
tags: Mapping[str, str] | None = None,
Expand All @@ -122,7 +127,7 @@ async def acreate(
client: DeepInfraClient | None = None,
) -> Sandbox:
client = client or default_client()
reply = (await client.arequest(cls._create_spec(image, plan, timeout, tags))).json()
reply = (await client.arequest(cls._create_spec(plan, timeout, tags))).json()
sandbox = cls(SandboxInfo(sandbox_id=reply["sandbox_id"]), client=client)
if wait:
await sandbox.await_until_running(timeout=wait_timeout)
Expand Down Expand Up @@ -279,8 +284,8 @@ async def aexec(
def run_python(self, code: str, *, timeout: Duration | None = None) -> ExecResult:
"""Run a Python snippet (python3 -c).

For large scripts prefer fs.write("/work/script.py", code) +
exec("python3", "/work/script.py").
For large scripts prefer fs.write("/workspace/script.py", code) +
exec("python3", "/workspace/script.py").
"""
return self.exec("python3", "-c", code, timeout=timeout)

Expand Down Expand Up @@ -369,7 +374,6 @@ def _exec_spec(

@staticmethod
def _create_spec(
image: str,
plan: str,
timeout: Duration | None,
tags: Mapping[str, str] | None,
Expand All @@ -378,7 +382,6 @@ def _create_spec(
"POST",
_SANDBOXES,
json={
"image": image,
"plan": plan,
"tags": dict(tags or {}),
"timeout_seconds": parse_duration(timeout) if timeout is not None else 0,
Expand Down
4 changes: 2 additions & 2 deletions examples/sandbox_quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ def main() -> None:
r = sb.run_python("print(sum(range(101)))").check()
print("sum 0..100 =", r.stdout.strip())

sb.fs.write("/work/hello.txt", "hello from the host\n")
print("read back:", sb.fs.read("/work/hello.txt").decode().strip())
sb.fs.write("/workspace/hello.txt", "hello from the host\n")
print("read back:", sb.fs.read("/workspace/hello.txt").decode().strip())


if __name__ == "__main__":
Expand Down
1 change: 0 additions & 1 deletion tests/test_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def test_create_waits_until_running(client):
assert get.call_count == 3
body = json.loads(create.calls.last.request.content)
assert body == {
"image": "",
"plan": "medium",
"tags": {"job": "t"},
"timeout_seconds": 600,
Expand Down
Loading