diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9f35b..e4fee58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 8389c8b..9987178 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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_...") @@ -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), diff --git a/deepinfra/_version.py b/deepinfra/_version.py index 3ced358..493f741 100644 --- a/deepinfra/_version.py +++ b/deepinfra/_version.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.3.0" diff --git a/deepinfra/sandbox_api.py b/deepinfra/sandbox_api.py index 89a3bdc..461f5ce 100644 --- a/deepinfra/sandbox_api.py +++ b/deepinfra/sandbox_api.py @@ -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, ...). """ @@ -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, @@ -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) @@ -113,7 +119,6 @@ def create( async def acreate( cls, *, - image: str = "", plan: str = "", timeout: Duration | None = None, tags: Mapping[str, str] | None = None, @@ -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) @@ -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) @@ -369,7 +374,6 @@ def _exec_spec( @staticmethod def _create_spec( - image: str, plan: str, timeout: Duration | None, tags: Mapping[str, str] | None, @@ -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, diff --git a/examples/sandbox_quickstart.py b/examples/sandbox_quickstart.py index 6237b3f..1ea88b5 100644 --- a/examples/sandbox_quickstart.py +++ b/examples/sandbox_quickstart.py @@ -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__": diff --git a/tests/test_sandbox.py b/tests/test_sandbox.py index 471cd3d..511febe 100644 --- a/tests/test_sandbox.py +++ b/tests/test_sandbox.py @@ -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,