platinum-sdk (Python)
Python client for Platinum — hardware-isolated sandbox microVMs (one Cloud Hypervisor VM per sandbox, sub-second boots via a warm pool).
pip install platinum-sdk # import name: platinum
Python ≥ 3.9, typed (PEP 561 — ships py.typed; mypy --strict clean). Sync and async clients. Server-side use only — never ship an API key into client-side code.
Quickstart
from platinum import Platinum
dn = Platinum(token="pt_live_...", api_url="https://api.platinum.dev")
# or: PT_TOKEN / PT_API_URL env vars
# Pick a template that exists on YOUR deployment first:
print(dn.templates.list())
sbx = dn.sandboxes.create(template="pt-base", wait_for_running=True)
r = sbx.exec(["uname", "-a"]).check() # .check() raises on non-zero exit
print(r.stdout)
sbx.delete()
Async — a full 1:1 mirror of the sync surface:
from platinum import AsyncPlatinum
async with AsyncPlatinum(token="pt_live_...", api_url="https://api.platinum.dev") as dn:
sbx = await dn.sandboxes.create(template="pt-base", wait_for_running=True)
r = await sbx.exec("uname -a") # argv list or plain string
print(r.stdout)
await sbx.delete()
Expose a port at create time (URL comes back in the same response):
sbx = dn.sandboxes.create(
template="pt-base",
expose=[{"port": 8080, "public": True}],
wait_for_running=True,
)
print(sbx.exposed_url(8080))
Build a custom image inline (cache-hit on repeat, built on first use):
from platinum import Platinum, Template
dn = Platinum()
image = (Template.from_python_image("3.12-slim")
.pip_install(["fastapi", "uvicorn"])
.workdir("/app"))
sbx = dn.sandboxes.create(image=image, wait_for_running=True, wait_timeout_ms=600_000)
Configuration
| Arg / env | Default | Meaning |
|---|---|---|
token / PT_TOKEN |
— (required) | API key pt_live_…, org-scoped bearer token |
api_url / PT_API_URL |
http://127.0.0.1:3000 |
Control-plane URL |
timeout |
60.0 |
Per-request timeout in seconds (file transfer calls override per call) |
transport |
— | httpx transport override (e.g. httpx.MockTransport in tests) |
Errors
Everything the SDK raises extends PlatinumError (.status, .body, .code — the
API's machine-readable error code, also folded into str(err)):
from platinum import NotFoundError, ConflictError, RateLimitError
try:
sbx.exec("true")
except NotFoundError: # sandbox gone
...
except ConflictError as e: # e.g. e.code == "sandbox_not_running"
...
except RateLimitError as e: # back off e.retry_after_seconds — the SDK never retries
...
Subclasses: ValidationError (400) · AuthenticationError (401) · ForbiddenError (403)
· NotFoundError (404) · ConflictError (409) · RateLimitError (429) · ServerError
(5xx) · PlatinumTimeoutError / PlatinumConnectionError (client-side, status == 0).
No automatic retries, ever — a 429 or a failed create is surfaced, never silently retried.
Surface (at parity with the TypeScript SDK)
The two SDKs expose the same operations — enforced in CI by verify/sdk-parity.sh
(name-level) and the offline unit suites (behaviour). The async client mirrors the sync
surface 1:1 (enforced by tests/test_unit.py::test_async_mirrors_sync_surface).
| Area | Methods |
|---|---|
| Sandboxes | sandboxes.create(...) · get · connect · list · iter (auto-pagination) · rename · delete |
| Lifecycle | stop/start (optional server-side wait) · pause/resume · kill · resize · fork · clone · snapshot · list_snapshots · delete_snapshot · restore · restore_from_backup · archive · backup · wait_running · wait_state · refresh |
| Run | exec(argv | str) · sh(script) · run_code(code, lang=None) — defaults to the sandbox's create-time language |
| Processes | process.start(cmd, cwd=, env=, pty=, cols=, rows=, timeout_ms=) · process.list() · process.get(handle) — the handle has logs · stream · stdin/close_stdin · signal · kill · resize · wait · refresh |
| Files (vsock) | files.read/write/delete/list/stat/mkdir/exists · find (glob) · grep (content) · replace (bulk sed) · watch (generator of change events) |
| Networking | expose(port, public=, ttl_seconds=) · unexpose · exposed_url · revoke_expose_token · set_egress_policy · add_ssh_keys |
| Observability | metrics() (live cpu/mem/disk) · usage() (billed usage) |
| Platform | templates.list/get/delete · Template builder · webhooks.* · volumes.* · regions.list() · me() · health.check() |
| Results | ExecResult(stdout, stderr, exit_code, duration_ms, lang) with .check() |
JuiceFS Shared Volumes
With the independent JuiceFS feature enabled:
# Returns the volume named "data", creating it (and waiting for it) if absent.
vol = dn.volumes.get_or_create("data", type="shared", driver="juicefs", size_gib=20, inode_quota=100_000)
dn.volumes.attach(vol["id"], sbx.id, "/mnt/data")
# Choose the key yourself when a retry must be safe across process restarts.
dn.volumes.resize(vol["id"], 40, idempotency_key=f"resize-{vol['id']}-40")
Every lifecycle write (sync and async) sends an Idempotency-Key. The SDK
generates one per call when you pass none and never retries on its own. If a
call fails with an unknown outcome (lost response, timeout, 5xx), the key it
sent is on the error as err.idempotency_key: resend it with
idempotency_key=err.idempotency_key to recover the same operation instead of
starting a second one. Pass your own idempotency_key when a retry must survive
a process restart or comes from your own retry loop. volumes.barrier(id)
returns the key it used as idempotency_key on the receipt.
get_operation and wait_operation only read the durable operation. Restore is fork-only. See the
JuiceFS client parity guide for the
cross-client contract. When juicefsGitWorkspacesEnabled is discovered,
volumes.git / async_volumes.git expose public project/workspace lifecycle
and project usage reconciliation. Git reads return either content or
{"pending": True, "error": ...} and never a host command id, path, remote,
gitdir, or credential.
Run something that outlives the call — exec kills its process group the moment it
returns, process.start does not:
p = sbx.process.start(["python", "-m", "http.server", "8000"], cwd="/workspace")
print(p.handle, p.pid)
for chunk in p.stream(): # resumes at the last byte if the socket drops
if chunk.type in ("stdout", "stderr"):
if chunk.dropped: # the ring buffer evicted bytes — a HOLE, not a stat
print(f"[{chunk.dropped} bytes lost]")
print(chunk.data, end="") # str, decoded here — a split emoji arrives whole
elif chunk.type == "reconnect": # incl. the server's 30-min cap: it keeps going
print(f"[resumed at {chunk.stdout_offset}: {chunk.reason}]")
elif chunk.type == "exit": # the only terminal event — the loop ends here
print("exit", chunk.exit_code)
p.signal("SIGTERM") # or p.kill() — the record (logs + exit code) is kept
Reconnect to it later from anywhere — reads are by absolute byte offset, so nothing is replayed and nothing is lost:
p = sbx.process.get(handle)
out = p.logs(stdout_offset=my_last_offset)
print(out.stdout, out.stdout_offset, out.running, out.exit_code)
raw = p.logs(encoding="base64").stdout # bytes, not str — binary-safe
Interactive PTY (stdin also delivers control characters), and a server-side wait:
sh = sbx.process.start("bash -i", pty=True, cols=120, rows=40)
sh.stdin("ls -la\n")
sh.resize(100, 30)
sh.stdin(b"\x03") # ^C
done = sbx.process.start(["make", "build"]).wait(timeout_ms=600_000)
print(done.exit_code)
Async is the same surface with await / async for (await sbx.process.start(...),
async for chunk in p.stream()). Managed processes need an in-VM agent that implements
them; an older guest answers 409 managed_procs_unsupported and exec still works.
Watch for file changes:
for ev in sbx.files.watch("/workspace", max_seconds=60):
if ev["event"] == "change":
print(ev["data"]["type"], ev["data"]["path"])
Not wrapped yet: interactive terminal (WebSocket), cross-host migrate (admin-only),
API-key management, audit-log queries, billing.
Limits worth knowing
files.writebodies must stay ≤ 16 MiB — larger writes currently truncate and fail through the vsock path. Split big files into ≤ 16 MiB chunks, or fetch them directly inside the sandbox (execwithwget).exec/run_codeare buffered, not streaming — output arrives after the command exits (default timeout 30 s viatimeout_ms).run_codesource is capped at 1 MiB.- Template names and minimums are per-deployment.
pt-base(busybox — no git/pip/node/httpdinside;nc/wgetavailable) exists on default installs; hosted deployments may only offer other templates with higher cpu/ram minimums. Calltemplates.list()first; a below-minimum create fails with a clear 400. - Background processes are reaped when their
execcall returns. Usesbx.process.start(...)for anything that has to keep running — it is owned by the guest agent, not by the request, and its output and exit code stay readable. The old workaround (setsid sh -c '<server loop>' >/dev/null 2>&1 < /dev/null &, seeexamples/02_expose_service.py) still works but loses both.
Testing
pip install -e '.[test]' && pytest runs the offline unit suite (httpx.MockTransport,
no network). The live e2e is opt-in: PT_E2E=1 PT_API_URL=… PT_TOKEN=… pytest test_e2e.py.
Examples
Runnable scripts in examples/:
pip install platinum-sdk
PT_API_URL=… PT_TOKEN=… python packages/sdk-py/examples/01_create_exec_delete.py
License
MIT
Release files for platinum-sdk 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| platinum_sdk-0.4.0.tar.gz | 102.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| platinum_sdk-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 174.3 kB
Release files / platinum_sdk-0.4.0.tar.gz
| Download URL | platinum_sdk-0.4.0.tar.gz |
|---|---|
| Size | 102.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4fd577c69f328330e6fc16b7bdc321900f7b13e05fb5d12d018a7c8ad37286e2
|
|
BLAKE2b-256 checksum How to use checksums |
aca241dd165b32a2c93f2c596618d0760198152cdcac9232d1399a88fd7cf2ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / platinum_sdk-0.4.0-py3-none-any.whl
| Download URL | platinum_sdk-0.4.0-py3-none-any.whl |
|---|---|
| Size | 71.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f7b893960d1dc44feb161b8ffaddc01c30ce2b27976856e7aa141a26dca13ff8
|
|
BLAKE2b-256 checksum How to use checksums |
90f3c35deadf562c21fc05dc7766016636e89f28b73b85e191f99390cc6d1b9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log