PandaStack

Python SDK

pandastack — the official Python SDK. Full reference.

The pandastack package is the recommended way to use PandaStack from Python. It targets 3.9+, has no required dependencies beyond requests, and mirrors the REST API 1:1.

Install

pip install pandastack

Configure

Two env vars:

export PANDASTACK_API_KEY=pds_abc123def456...
export PANDASTACK_API=https://api.pandastack.ai   # optional, this is the default

The SDK also reads ~/.config/pandastack/config.json (token / apiKey / api_key) if no env vars are set — useful for shared dev workstations.

Imports

from pandastack import (
    Client, Sandbox, ExecResult,
    Template, TemplateBuild,
    Token,
    PandastackError, AuthError, NotFoundError,
    BadRequestError, ServerError, APIConnectionError,
)

The most common surface is Sandbox — a class with classmethod constructors and rich instance methods. Under the hood every call goes through a singleton Client, but you rarely instantiate one yourself.

Sandboxes

Sandbox.create(**opts) -> Sandbox

ArgTypeDefaultNotes
templatestr"ubuntu-24.04"Any official template or your own. CPU and memory are baked into the template.
from_snapshotstr?Boot from this snapshot ID instead of the template default.
ttl_secondsint?server default (3600)Wall-clock lifetime.
persistentbool?FalseIf True, ignores TTL but is hibernated when idle.
metadatadict[str,str]?Free-form tags for filtering / auditing.
volumeslist[dict]?[{"name": "data", "read_only": True}]

cpu and memory_mb keyword arguments are accepted for backwards compatibility but deprecated and ignored — the server always uses the template's baked size. Passing them emits a DeprecationWarning. To run at a different size, bake a custom template with --cpu / --memory-mb.

Returns a Sandbox instance with .id, .template, .status, .boot_ms, .boot_mode, .guest_ip, .metadata, .created_at.

Sandbox.list() -> list[Sandbox]

for s in Sandbox.list():
    print(s.id, s.status, s.template)

Sandbox.get(id) -> Sandbox

s = Sandbox.get("278a4f42-3467-4424-98e6-a547646dd0fd")

Sandbox instance methods

Exec & code

result = sandbox.exec("python3 -c 'print(2+2)'", timeout_seconds=10)
# ExecResult(stdout='4\n', stderr='', exit_code=0, duration_ms=23)

shell  = sandbox.run_code("ls -la", language="shell")
python = sandbox.run_code("import sys; print(sys.version)", language="python")

cmd is a string, not a list — it runs under sh -c. See Exec for the full guide.

Streaming logs

for line in sandbox.logs(stream="both", follow=True):
    print(line)

stream"stdout"|"stderr"|"both". follow=True opens an SSE stream until you break or the sandbox exits.

Filesystem

sandbox.filesystem.write("/workspace/in.txt", "hi\n")
data = sandbox.filesystem.read("/workspace/in.txt")     # bytes
sandbox.filesystem.upload("./local.csv", "/workspace/data.csv")
sandbox.filesystem.download("/workspace/out.csv", "./out.csv")

Full guide: Filesystem.

Lifecycle

sandbox.pause()
sandbox.resume()

snap = sandbox.snapshot()           # snapshot ID string
fork = sandbox.fork(metadata={"branch": "exp"})
many = sandbox.fork_tree(count=8, metadata={"batch": "search"})
fork.promote()                      # detach from parent

sandbox.hibernate()
sandbox.wake()

sandbox.set_ttl(7200)
sandbox.set_persistent(True)
sandbox.lifecycle()                 # {"ttl_seconds":7200,"persistent":True,"idle_seconds":12}

sandbox.kill()

Preview URLs

sandbox.preview_url(3000)                              # public URL for a port
sandbox.preview_urls()                                 # {3000: "https://3000-…"}

Full guide: Preview URLs.

Context manager

with Sandbox.create(template="code-interpreter") as sb:
    sb.exec("python3 train.py")
# sb.kill() called automatically

Templates

from pandastack import Client
client = Client()

client.templates.list()
client.templates.get("code-interpreter")
client.templates.delete("my-template")

build = client.templates.build(
    name="my-template",
    rootfs=open("Dockerfile.tar", "rb"),
    size_mb=2048,
)
print(build.id, build.status)

client.templates.builds()
client.templates.get_build(build.id)

Template has .name, .rootfs_path, .size_bytes, .meta. TemplateBuild has .id, .name, .status, .error, .started_at, .ended_at, .size_mb, .bytes.

Databases

Managed PostgreSQL 16 in dedicated Firecracker microVMs (Beta). See Databases.

from pandastack import Client
client = Client()

# Create — blocks until Postgres is accepting connections (~30–90s)
db = client.databases.create(label="my-app-db")   # size is fixed by the postgres-16 template
print(db["connection_url"])                         # postgres://...

client.databases.list()                             # metadata only (no credentials)
client.databases.get(db["id"])                      # full record incl. connection_url
client.databases.connection(db["id"])               # {connection_url, broker_url, broker_token}
client.databases.delete(db["id"])                   # irreversible

Connect with any driver using connection_url (TLS required):

import psycopg
with psycopg.connect(db["connection_url"] + "?sslmode=require") as conn:
    print(conn.execute("SELECT version()").fetchone())

Volumes

Persistent named volumes — ext4 block devices that survive sandbox deletion and attach at create time. See Volumes.

from pandastack import Client, Sandbox
client = Client()

vol = client.volumes.create(name="models", size_mb=4096)
client.volumes.list()              # [{"name": "models", "size_mb": 4096, ...}]
client.volumes.get("models")
client.volumes.delete("models")    # refused (409) while attached to a running sandbox

# Attach at create — appears as /dev/vdb (then vdc, ...) inside the guest
sb = Sandbox.create(
    template="code-interpreter",
    volumes=[{"name": "models", "read_only": True}, {"name": "scratch"}],
)
sb.exec("mkdir -p /mnt/models && mount -o ro /dev/vdb /mnt/models")

Quotas are per plan (Free 1 × 1 GiB · Pro 10 × 10 GiB · Team 50 × 50 GiB · Enterprise unlimited; 64 GiB hard per-volume ceiling). Exceeding the quota returns a 429 (BadRequestError); storage above your plan's included total bills at $0.15 per provisioned GiB-month. Live rates and per-tier limits: GET /v1/pricing.

Apps (git-driven hosting)

Connect a GitHub repo and serve it behind a stable per-app URL with blue-green deploys. See Apps overview.

from pandastack import Client
client = Client()

# Create — only name + git_url are required; framework is auto-detected when omitted
app = client.apps.create(
    name="my-site",
    git_url="https://github.com/acme/my-site",
    git_branch="main",            # optional (default: main)
    framework="nextjs",           # optional: nextjs | vite | node | static (auto-detected if omitted)
    install_command=None,         # optional overrides
    build_command=None,
    start_command=None,
    root_directory=None,          # optional sub-dir to build from
    port=3000,                    # optional (default: 3000)
    env={"NODE_ENV": "production"},
)
print(app["id"], app.get("url"))

client.apps.list()                 # all apps in the workspace
client.apps.get(app["id"])         # includes the stable `url` when running
client.apps.update(app["id"], build_command="npm run build:prod")
client.apps.delete(app["id"])      # also tears down the runtime sandbox

Deploys are blue-green: a fresh sandbox builds the new commit, health-checks, then the app atomically flips to it.

dep = client.apps.deploy(app["id"], git_ref="v1.2.0")   # git_ref optional (defaults to git_branch)
client.apps.deployments(app["id"])                       # newest first
client.apps.deployment(app["id"], dep["id"])             # includes full build log

for line in client.apps.deploy_logs(app["id"], dep["id"]):  # SSE, line by line
    print(line)

client.apps.rollback(app["id"])    # rebuild + flip back to the previous deployment

Apps run on the base template snapshot, so CPU/RAM are governed by that template's baked size (2 GiB), not the per-app cpu/memory_mb — those are accepted for forward-compat but the agent overrides them to the snapshot size.

GitHub integration

Private repos and push-triggered auto-deploys use a GitHub App installation. The connect flow is browser-based (GET /v1/github/connect returns the install URL); once installed, list repos and installations via the REST API. A push to a connected branch with auto_deploy=true triggers a deploy pinned to the exact commit.

Tokens

client.tokens.list()
new = client.tokens.create("ci-pipeline", expires_in_days=30)
print(new.token)            # only visible once
client.tokens.revoke(new.prefix)

Errors

All SDK exceptions inherit from PandastackError:

ClassWhen
AuthError401 / 403 — missing or invalid token.
NotFoundError404 — sandbox / template / snapshot doesn't exist.
BadRequestError4xx other — bad payload, quota exceeded, validation failure.
ServerError5xx — orchestrator hiccup, transient — usually safe to retry.
APIConnectionErrorUnderlying requests connection problem.

All carry .status_code and .response for diagnostics.

try:
    sandbox = Sandbox.get(some_id)
except NotFoundError:
    sandbox = Sandbox.create(template="code-interpreter")

Custom client

Most apps work with the implicit singleton client. Override when you need a non-default base URL or token:

from pandastack import Client

client = Client(api_url="https://api.staging.pandastack.ai", token="pds_test_…")

sandbox = client.sandboxes.create(template="code-interpreter")
client.sandboxes.list()
client.sandboxes.get(sandbox.id)
client.sandboxes.fork(sandbox.id, metadata={"x": "y"})
client.sandboxes.fork_tree(sandbox.id, count=4)
client.sandboxes.promote(sandbox.id)
client.sandboxes.delete(sandbox.id)

client.me()                  # whoami — returns the org / user / scopes

Sandbox.create(...) is sugar for Client._global().sandboxes.create(...).

Async support

The current SDK is synchronous (built on requests). For high-concurrency workloads, run it in a thread pool (asyncio.to_thread) or use the REST API directly with httpx. An async SDK is on the roadmap — track GitHub issues.

On this page