Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

boxd Python SDK

Python SDK for the boxd cloud machine platform. Create machines, run commands in them, move files, and manage everything around them.

Requires Python 3.10+.

Install

pip install boxd

Quick start

from boxd import Boxd

boxd = Boxd(api_key="bxd_...")

machine = boxd.machines.create("my-machine")
boxd.machines.wait_until_ready(machine.id)

result = boxd.machines.exec(machine.id, "uname -a")
print(result.stdout)

boxd.machines.delete(machine.id)
boxd.close()

Everything follows the same shape: boxd.<resource>.<verb>(id, ...). Resources return plain data — a Machine has fields, not methods.

Client

Boxd()                                     # production
Boxd(api_key="bxd_...")
Boxd(base_url="https://boxd.example.com:9443")   # any other cluster

Every argument is keyword-only.

Argument Environment variable Default
api_key BOXD_API_KEY —
token BOXD_TOKEN —
base_url BOXD_BASE_URL (or the deprecated BOXD_API_URL) https://boxd.sh:9443
timeout — 60.0 seconds
max_retries — 2

base_url accepts an optional scheme that controls TLS:

Value Transport
http://host:port plaintext
https://host:port TLS
bare host:port TLS, except localhost / 127.*

boxd.base_url reports the cluster the client settled on.

The client holds a connection, so keep one around rather than making a new one per call. Close it when you're done — or use it as a context manager:

with Boxd(api_key="bxd_...") as boxd:
    ...

Authentication

The first of these that is present wins:

  1. token= — used as given
  2. api_key= — exchanged for a short-lived credential and kept fresh for you
  3. BOXD_TOKEN, then BOXD_API_KEY
  4. running inside a boxd machine — see below
  5. otherwise AuthenticationError

If your key is revoked mid-session, the SDK fails fast with AuthenticationError rather than retrying.

The exchange endpoint is rate-limited per source IP (30 requests / 60s). A session token is valid for one hour, so a fleet of workers behind one NAT should mint one token and share it (Boxd(token=...)) rather than each process exchanging its own key. On a 429 the SDK waits out one Retry-After before raising RateLimitError.

Inside a machine

Inside a boxd machine, Boxd() authenticates automatically — no API key needed, and it talks to that machine's own cluster unless you pass base_url.

from boxd import Boxd

boxd = Boxd()
for machine in boxd.machines.list():
    print(machine.name, machine.status)

One limit: inside a shared machine the automatic credential can manage the organization's shared machines, but cannot read environment variables or secrets, and cannot reach private machines. Pass an API key for those.

Sync and async

Boxd and AsyncBoxd are the same surface — same namespaces, same method names, same arguments, same return types. Switching is await and an import, not a rewrite.

from boxd import AsyncBoxd

boxd = AsyncBoxd(api_key="bxd_...")

machine = await boxd.machines.create("my-machine")
result = await boxd.machines.exec(machine.id, "echo hello")
await boxd.close()

AsyncBoxd is an async context manager too:

async with AsyncBoxd(api_key="bxd_...") as boxd:
    ...

Two methods stay un-awaited, because they hand back something to iterate rather than a result: stream_exec returns the session object directly, and logs is an async generator you drive with async for.

Use AsyncBoxd when you already have an event loop (FastAPI, asyncio scripts, anyio). Use Boxd everywhere else — scripts, notebooks, Django views.

Machines

machine = boxd.machines.create(
    "my-machine",
    vcpu=4,
    memory="16G",
    env={"MODE": "production"},
)
boxd.machines.get("my-machine")     # by name or id
boxd.machines.list()                     # a plain list
boxd.machines.list(org="acme")           # one organization's machines
boxd.machines.list(all_contexts=True)    # every org you belong to
boxd.machines.delete("my-machine")

Only name is positional; everything else is keyword-only. The full set of create options:

boxd.machines.create(
    "builder",
    image="ubuntu:24.04",
    org="acme",                   # create inside an organization
    shared=True,                  # and make it visible to every member
    env={"API_URL": "https://example.com"},
    cmd=["/usr/local/bin/start"],
    restart_policy="always",      # "always" | "never"
    vcpu=2,
    memory="8G",                  # sizes: 1 vCPU/4G, 2/8G, 4/16G
    disk="100G",
    auto_suspend_timeout=300,     # seconds; 0 disables
    auto_destroy_timeout=0,
    ssh=True,                     # give the machine an SSH port
    proxies=[ProxyEntry(name="api", port=3000)],
    volumes=[VolumeMount(disk_id="d_...", mount_path="/data", read_only=False)],
)

boxd.machines.create()            # every option is optional — cluster default image

ProxyEntry and VolumeMount are importable from boxd. A ProxyEntry with port=0 has its port detected inside the machine.

State:

boxd.machines.start(id)
boxd.machines.stop(id)
boxd.machines.reboot(id)
boxd.machines.pause(id)        # suspend to RAM — fast to resume; PauseResult(suspend_us)
boxd.machines.resume(id)       # ResumeResult(resume_us)
boxd.machines.hibernate(id)    # suspend to disk — cheaper, slower to wake
boxd.machines.wake(id)

Everything else:

boxd.machines.fork("my-machine", "my-copy")     # live clone
boxd.machines.fork(id, shared=True)             # forks keep the source machine's size
boxd.machines.rename(id, "new-name")            # returns the new name; reboots the machine
boxd.machines.share(id)                         # visible to your whole org
boxd.machines.unshare(id)
boxd.machines.set_auto_suspend_timeout(id, 300) # seconds idle; 0 disables
boxd.machines.set_auto_hibernate_timeout(id, 0)
boxd.machines.wait_until_ready(id)
boxd.machines.wait_until_ready(id, timeout=180.0, poll_interval=1.0)   # seconds
boxd.machines.suggest_name()

A fork inherits the source's sizing for anything you leave unset, and is private to you unless you pass shared=True.

create and fork return once the machine is scheduled, not once it is usable. Call wait_until_ready before doing anything that depends on it running — especially before forking it again.

Egress allowlist

A machine can be limited to what it may reach on the internet:

boxd.machines.set_egress_allow("alpha", ["api.stripe.com", "*.github.com", "203.0.113.0/24"])
boxd.machines.get("alpha").egress_allow
boxd.machines.set_egress_allow("alpha", [])   # clear: unrestricted again

Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs. Web requests are admitted by hostname, everything else by address, with the addresses of allowlisted names learned as the machine resolves them. Nothing that is not named is reachable. The hosts of any bound secret the machine holds are always admitted. Takes effect within a second and is not visible or changeable from inside the machine. A fork inherits its source's allowlist; a machine restored from a snapshot starts unrestricted.

Sizing

There are three sizes: 1 vCPU/4G, 2/8G, 4/16G. Name either dimension and the other follows, so vcpu=2 and memory="8G" are the same request. Anything above your org's quota is refused, never quietly clamped.

A machine keeps the size it was created with, through stop/start, wake, and migration alike. Changing it is an explicit call:

result = boxd.machines.resize(id, memory="8G")   # or vcpu=2
result.vcpu, result.memory_bytes                 # the size now persisted
result.rebooted                                  # it was running, so it was rebooted

A running machine is rebooted to apply the new size: a cold boot, so the disk survives and anything in RAM does not. A stopped machine takes it at its next start. A suspended or hibernated machine is refused, because its saved memory image is the old size — resume or wake it first.

A create that names no size uses the org default (see Organizations), and a fork always inherits its source's size.

The Machine record

machine.id, machine.name, machine.image_ref
machine.status                  # "pending" | "starting" | "running" | "suspended" |
                                # "hibernated" | "stopped" | "failed" | "destroyed" |
                                # "migrating"
machine.restart_policy          # str | None
machine.created_at              # datetime | None — None when none is on record

machine.resources.vcpu          # what the machine actually got, not what you
machine.resources.memory_bytes  # asked for — always concrete
machine.resources.disk_bytes

machine.org                     # OrgRef(id, name) | None — None = personal quota
machine.shared                  # shared with that org, or private to you

machine.access.ssh_port         # int | None — None until allocated
machine.access.domain
machine.access.url              # https://<name>.<domain>

machine.idle.suspend_after      # seconds; 0 = that timer is disabled
machine.idle.hibernate_after
machine.idle.destroy_after

machine.source                  # MachineSource | None — None = booted from an image
machine.source.kind             # "fork" | "snapshot"
machine.source.name             # source machine, or snapshot name
machine.source.version          # int | None — snapshots only; a fork has none
machine.source.id               # str | None — provenance; may not resolve

machine.hibernated_at           # datetime | None — None = not hibernated
machine.last_connected_at       # datetime | None — None = never connected
machine.boot_time_ms            # int | None — last boot; None = never booted

None always means "not set": a port that was never allocated, a boot that never happened, an org you do not have. Where 0 is a real answer — a disabled idle timer — it stays 0.

org is the org the machine belongs to and is billed to; shared says whether your teammates can see it. A private machine can still be org-billed, so org set with shared=False is normal, not a contradiction.

source.id points at the machine or snapshot this one came from. It is a record of where the machine came from, not a live link — it may not resolve, and a lookup that finds nothing is normal.

MachineStatus is importable from boxd when you want the literal type; a status a newer server introduces is passed through as a plain string.

Creating from a snapshot

boxd.snapshots.create(machine_id, "golden")
machine = boxd.machines.create("from-golden", from_snapshot="golden")

Restoring a snapshot replays the machine as it was captured, so from_snapshot goes with name, org and the sizing options. Combining it with image, env, cmd, restart_policy or shared raises ValueError.

Exec

result = boxd.machines.exec(id, "cargo build")
result.stdout      # str
result.stderr      # str — populated for non-PTY execs
result.exit_code   # int
result.success     # bool

boxd.machines.exec(id, ["echo", "a b"])              # a list is quoted for you
boxd.machines.exec(id, "env", env={"FOO": "bar"})
boxd.machines.exec(id, "make", cwd="/srv/app")       # run from a working directory
boxd.machines.exec(id, "cargo build", timeout=30)    # seconds

# Under a PTY, stderr merges into stdout and `stderr` comes back empty.
boxd.machines.exec(id, "top -b -n1", tty=True, cols=120, rows=40)

command takes a list of argv — shell-quoted for you — or a ready-made command line as a string. cwd runs the command from that directory; a cwd that doesn't exist fails fast instead of silently running in the default directory. timeout gives up on the call; whatever it started inside the machine may well still be running.

result.stdout/result.stderr are UTF-8-decoded with errors="replace": any byte sequence that isn't valid UTF-8 is silently replaced with U+FFFD, changing the length with no error raised. Fine for text output; corrupts binary output. For binary or byte-exact output, read from stream_exec instead — its chunks are raw bytes, never decoded:

with boxd.machines.stream_exec(id, command="cat file.bin") as stream:
    data = b"".join(stream)   # byte-exact

For anything interactive, stream_exec gives you a live session — the one handle in the SDK, because a bidirectional stream really is stateful:

with boxd.machines.stream_exec(id, command="bash", tty=True) as stream:
    stream.write(b"ls\n")       # bytes or str
    stream.write_eof()          # half-close stdin; the process sees EOF
    for chunk in stream:        # bytes — merged output, what a terminal would show
        print(chunk.decode(errors="replace"), end="")
    print("exited", stream.exit_code)

stream_exec takes command and the rest as keywords, and hands back the session without a round trip. exit_code is None until the stream is exhausted. Leaving the with block — or calling close() — ends the session.

iter_chunks() yields OutputChunk(data, is_stderr) when you need the two streams apart. Under tty=True the terminal merges them, so everything arrives as stdout — set tty=False if you need the split.

For a headless one-shot that reads stdin (jq, cat, claude -p), pass close_stdin=True so it sees end-of-input immediately instead of hanging. Combining it with tty=True raises ValueError — a shell needs stdin open.

Set the terminal size with cols/rows, and call stream.resize(cols, rows) when the local terminal changes size:

import shutil, signal

cols, rows = shutil.get_terminal_size()
stream = boxd.machines.stream_exec(id, command="htop", tty=True, cols=cols, rows=rows)
signal.signal(signal.SIGWINCH, lambda *_: stream.resize(*shutil.get_terminal_size()))

Logs

for chunk in boxd.machines.logs(id):
    print(chunk.decode(errors="replace"), end="")

for chunk in boxd.machines.logs(id, follow=True):   # stays open
    ...

Files

from pathlib import Path

written = boxd.machines.files.upload(id, "/app/config.json", '{"debug": true}')
boxd.machines.files.upload(id, "/app/data.bin", Path("local.bin").read_bytes())
boxd.machines.files.upload(id, "notes.txt", "relative to /home/boxd")
data = boxd.machines.files.download(id, "/app/output.json")   # bytes

upload takes str or bytes, streams it in chunks so large files are fine, and returns the number of bytes the machine confirmed it wrote. download streams too and returns the whole file as bytes — there is no size cap, but it is all held in memory. Paths are either absolute (/app/config.json) or relative to the VM user's home directory (/home/boxd) — a bare filename like "notes.txt" resolves to /home/boxd/notes.txt, same as "./notes.txt" or "sub/notes.txt" would.

Ports and proxies

fwd = boxd.machines.ports.expose(id, 8080)            # public TCP forward
boxd.machines.ports.expose(id, 5353, protocol="udp")  # "tcp" | "udp" | "both"
fwd.dns, fwd.public_port, fwd.machine_port, fwd.protocol
fwd.machine_id, fwd.machine_name
boxd.machines.ports.unexpose(id, 8080)                # echoes back what it removed
boxd.machines.ports.list(id)                          # one machine's forwards
boxd.machines.ports.list()                            # every forward you own

Connect on dns:public_port. Max 3 forwards per machine. Re-exposing a machine port keeps its public port and just updates the protocol set; "both" shares one public port across TCP and UDP.

ports.list() is account-wide — pass a machine to narrow it, or filter on .machine_id / .machine_name.

route = boxd.machines.proxies.create("my-machine", "api", 3001)  # api.<machine>...
route.name, route.port
routes = boxd.machines.proxies.list("my-machine")
routes[0].name          # str | None — None on the machine's default route
routes[0].domain        # the hostname this route answers on
routes[0].port          # int — where traffic actually goes
routes[0].port_mode     # "locked" (you pinned it) | "auto" (detected for you)
routes[0].is_default
routes[0].machine_id, routes[0].machine_name
boxd.machines.proxies.set_port("my-machine", 3000, name="api")
boxd.machines.proxies.set_port("my-machine", "auto")  # default route, auto-detected
boxd.machines.proxies.delete("my-machine", "api")

These take an id or a name, like everything else on machines. name is a subdomain label: lowercase letters, digits and hyphens, not starting or ending with one. create answers as soon as the route is accepted, so it confirms the subdomain and the port it was pointed at; list() reports the full domain and the resolved port.

port_mode="auto" detection runs continuously for the whole life of the machine, not just once at boot: whenever the app binds one of a curated list of common ports (80, 443, 8080, 8000, 3000, 5000, 5173), that becomes a candidate, and it's adopted once roughly 750ms have passed with no different candidate port seen (an already-adopted port won't be replaced again more than once every 5s). So a redeploy or restart that moves your app onto a new port is picked up on its own within a few seconds — no action needed. If your app doesn't listen on one of those ports, or you don't want to wait out the detection window, call set_port with the port explicitly instead of "auto".

Checkpoints

Per-machine captures, restored in place. They are deleted with the machine.

cp = boxd.machines.checkpoints.create(id, "before-upgrade")
cp.id, cp.name, cp.status
saved = boxd.machines.checkpoints.list(id)
saved[0].size_bytes
saved[0].created_at     # datetime
saved[0].created_by     # str | None
saved[0].available      # restorable right now — mirrors `status == "ready"`
boxd.machines.checkpoints.restore(id, "before-upgrade")
boxd.machines.checkpoints.delete(id, "before-upgrade")

The machine must be running to take a checkpoint. status is "pending" until the artifact lands, then "ready" (or "failed") — poll status for "ready" to know when restore will succeed. available is a convenience mirror of that same check (False for "pending"/"failed", and also False if the machine has since migrated off the checkpoint's worker even though it's "ready"); it is never a separate readiness signal from status.

Environment variables and secrets

Two namespaces with identical methods. The difference is that a secret's value is write-only — the server never returns it, and the Secret model has no value field at all.

boxd.env.set("MODE", "production", scope="all")
boxd.env.list()                    # EnvVar(name, scope, value)
boxd.env.list(org="acme")
boxd.env.delete("MODE", scope="all")

boxd.secrets.set("API_TOKEN", "s3cr3t", scope="shared")
boxd.secrets.list()                # Secret(name, scope, domains) — no value
boxd.secrets.delete("API_TOKEN", scope="shared")

A secret can be bound to the hosts it may be sent to. The machine then only ever holds a placeholder, and the real value is substituted into requests to those hosts on the way out:

boxd.secrets.set("STRIPE_KEY", "sk_live_...", scope="all", domains=["api.stripe.com"])

Inside the machine, STRIPE_KEY is an opaque bxds_… string. A request to api.stripe.com carrying it, in a header, the query, the body or Basic auth, arrives at Stripe with the real key; a request anywhere else carries the useless placeholder. Nothing to configure in the machine, and the placeholder does not change when the value is rotated. domains is the full desired set: setting the secret again without it makes it a plain secret. Wildcards such as *.stripe.com are accepted; provider-wide ones such as *.amazonaws.com are refused.

scope defaults to "shared" on set and delete; list takes org only and reports every scope. Pass org="acme" to any of these to work in an organization instead of your personal scope. With an API key minted in an organization your personal scope already lives there, so org is only needed to reach the organization's shared/all vars.

set, delete and move each return the server's human-readable confirmation of what it did.

Scope decides which machines a name applies to:

Scope Applies to
private only your machines in that organization
shared the organization's shared machines
all every machine in the organization

Scope is part of a name's identity — the same name can exist in several scopes at once — so changing it is a move between two addresses, and both ends are required:

boxd.secrets.move("API_TOKEN", from_scope="private", to_scope="shared")

Calling it twice fails the second time. Environment variables and secrets share one namespace within a scope, so an environment variable can block a secret of the same name moving in, and vice versa.

Snapshots and disks

snap = boxd.snapshots.create(machine_id, "golden")   # re-saving bumps the version
snap.id, snap.name, snap.version, snap.status
boxd.snapshots.get("golden")                         # by name or id
boxd.snapshots.list()
boxd.snapshots.delete("golden")
boxd.snapshots.list(org="acme")      # `org` works on get/list/delete too

disk = boxd.disks.create("data", "10G")   # bytes or a human string
disk.id, disk.name, disk.size_bytes, disk.status
boxd.disks.attach(disk.id, machine_id, "/mnt/data")
boxd.disks.attach(disk.id, machine_id, "/mnt/data", read_only=True)
boxd.disks.detach(disk.id, machine_id)
boxd.disks.list()
boxd.disks.delete(disk.id)

The machine must be running to snapshot it, and create answers before the artifact lands — status is "pending" until it does. Snapshots stay inside one organization.

A disk is always created writable; read-only is chosen per attachment. A disk can be attached to only one machine at a time.

create confirms only what the server can answer immediately; the full records come back from get and list:

snapshot = boxd.snapshots.get("golden")
snapshot.id, snapshot.name
snapshot.version        # int | None — latest ready version; None = nothing captured yet
snapshot.status         # "pending" | "ready" | "failed"
snapshot.size_bytes
snapshot.created_at     # datetime | None — the first capture; it stays put
snapshot.updated_at     # datetime | None — the most recent capture
snapshot.vcpu           # the sizing the machine was captured at
snapshot.memory_bytes
snapshot.use_count      # machines restored from it so far

volume = boxd.disks.list()[0]
volume.id, volume.name, volume.size_bytes
volume.status           # "creating" | "ready" | "destroyed" — attach once "ready"
volume.created_at       # datetime | None
volume.attachments      # [DiskAttachment(machine_id, machine_name, mount_path, mount_mode)]
                        # mount_mode is "ro" or "rw"

Custom domains

machine = boxd.machines.get(machine_id)
print(boxd.domains.dns_instructions(
    "app.example.com", machine_name=machine.name, machine_ip="<machine's public IP>",
    zone=boxd.account.config().zone,
))
# Point an A record at the machine's public IP, and a wildcard CNAME
# (*.app.example.com) at <machine>.<zone> — only once those are actually in
# place:
domain = boxd.domains.create("app.example.com", machine_id)   # starts "pending"
domain.status           # "pending" | "active"
domain.last_error       # empty once active
boxd.domains.list()
boxd.domains.delete("app.example.com")

create binds the domain and starts verification immediately — call it only once DNS is actually set. Checking before the records exist risks a public DNS resolver caching the "no records" answer, which would delay verification after you do set them. dns_instructions is a local helper (no network call) for rendering the records to show a user first — it doesn't touch the API. A background check verifies DNS and issues certs automatically, then flips status to "active".

Organizations, credentials, account

orgs = boxd.orgs.list()              # a plain list
orgs[0].id
orgs[0].name                         # display label — it can repeat across organizations
orgs[0].slug                         # the organization's unique key
orgs[0].is_admin                     # you administer it
orgs[0].is_default                   # where your personal machines are billed

Anywhere a call takes org, it accepts an organization's name or id.

An org can also have a wildcard domain (e.g. preview.mysaas.com, covering <machine>.preview.mysaas.com for every machine in the org). Setting or clearing it requires org admin; reading it doesn't.

# Delegate the apex's NS records to boxd's cluster nameservers at your
# registrar — only once that's actually done:
d = boxd.orgs.set_domain("acme", "preview.mysaas.com")   # starts "pending"
d.status, d.last_error
boxd.orgs.get_domain("acme")     # None if unset
boxd.orgs.clear_domain("acme")

set_domain starts verification immediately — call it only once NS delegation is actually live, for the same reason as per-machine domains above. A background check then verifies delegation and issues certs automatically, then flips status to "active".

An org also has a default machine size: what a create that names no size gets. It is a preference below the quota, not the quota itself — the quota stays the operator-owned ceiling and the per-request maximum. Setting or clearing it requires org admin; reading it doesn't.

boxd.orgs.set_machine_defaults("acme", vcpu=2)     # or memory="8G"
d = boxd.orgs.get_machine_defaults("acme")
d.is_set                                           # False = no default; the values
d.vcpu, d.memory_bytes                             # below are just the quota
d.ceiling_vcpu, d.ceiling_memory_bytes             # the org quota
boxd.orgs.clear_machine_defaults("acme")           # back to the quota

Only machines created afterwards are affected — every existing machine keeps the size it was created with. Omit the org to target your home org.

key = boxd.api_keys.create(
    "ci",
    org="acme",                      # the organization the key is fenced to
    kind="member",                   # "member" (default) acts as you within that org;
                                     # "org" is a userless service credential, limited
                                     # to the org's shared fleet, org admin only
    expires_in=60 * 60 * 24 * 30,    # seconds; 0 for no expiry
)
key.id
key.api_key                          # the raw key — shown once, store it now
key.expires_at                       # datetime | None

keys = boxd.api_keys.list()
keys[0].name, keys[0].key_prefix, keys[0].created_at
keys[0].last_used_at                 # datetime | None — None = never used
keys[0].expires_at                   # datetime | None — None = no expiry
keys[0].org, keys[0].kind            # "member" | "org"
boxd.api_keys.delete(key.id)

Every key is fenced to exactly one organization. Deleting one takes effect immediately.

from pathlib import Path

me = boxd.account.get()
me.user_id
me.display_name                      # str | None — falls back to `user_id`
me.pubkey_fingerprints               # list[str]
me.billing.subscription_status       # "active", "trialing", … | None
me.billing.past_due_since            # datetime | None
me.billing.max_vms                   # effective quota
me.billing.vcpu, me.billing.memory_bytes

pubkey = (Path.home() / ".ssh/id_ed25519.pub").read_text()
boxd.account.link_ssh_key(pubkey)
boxd.account.link_ssh_key(
    pubkey,
    device_id="laptop",              # one key kept per device — re-linking replaces it
    label="MacBook Pro",             # shown wherever the device is listed
)

cfg = boxd.account.config()
cfg.default_image, cfg.zone

Errors

from boxd import (
    BoxdError,               # base class — catch this to catch everything
    AuthenticationError,     # no usable credential, or it was rejected
    PermissionDeniedError,   # authenticated, but not allowed
    NotFoundError,
    ConflictError,           # already exists, or fights the current state
    RateLimitError,          # rate limit or quota
    APIStatusError,          # any other error from the server
    APIConnectionError,      # could not reach the server
)

try:
    boxd.machines.get("nope")
except NotFoundError:
    ...

Every error carries .message, .code (the canonical status name, e.g. "not_found") and .grpc_code, the numeric status code.

Connection failures are retried with exponential backoff, max_retries times. Timeouts are never retried — the server may already have applied the request — and neither is AuthenticationError.

Update notices

The SDK prints a one-time note to stderr if the server reports a newer release:

A new version of boxd is available (v0.2.0, you have v0.1.9). Update with:
  pip install --upgrade boxd

It fires at most once per process and never causes a request to fail.

The installed version is available as boxd.__version__.

Metadata

Release files for boxd 0.2.10.dev617

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for boxd 0.2.10.dev617
File Size Uploaded
boxd-0.2.10.dev617.tar.gz 111.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for boxd 0.2.10.dev617
File Interpreter ABI Platform
boxd-0.2.10.dev617-py3-none-any.whl Python 3 none any Details

Total release size: 195.8 kB

Release files / boxd-0.2.10.dev617.tar.gz

Download URL boxd-0.2.10.dev617.tar.gz
Size 111.4 kB
Tags Source
SHA-256 checksum
How to use checksums
efd418ef19f8234ff733d02f1bc29325788c958e25d1fbbc57b50119ab936eef
BLAKE2b-256 checksum
How to use checksums
03b11138f264ee89f44c72e847f66b09fe64f57fa24caff0f91872564df38eaf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / boxd-0.2.10.dev617-py3-none-any.whl

Download URL boxd-0.2.10.dev617-py3-none-any.whl
Size 84.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e2bbb80ec6a135cd5436908d96e2d2444300ddea7712ed71a083abfffadb0688
BLAKE2b-256 checksum
How to use checksums
99a26e4289775f446109d79572b39b1501e93549579c1c610e16a7b2cdd31a16
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

0.2.13

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.10

2 release files

This release

0.2.10.dev617 This release

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page