This release is a pre-release and may not be stable for production use.
promigence — Python SDK for Promigence
Verified, forkable sandboxes for agents, evals and RL. One runtime dependency (httpx),
Python ≥ 3.9, sync and async clients, fully typed.
pip install ./promigence-<version>-py3-none-any.whl # the package you were given (not on PyPI)
export PROMIGENCE_API_KEY=... # or `promigence auth login`; see "Credentials" below
Quickstart
from promigence import Sandbox
image = "python:3.12" # the API pins a tag to its digest; NAME@sha256:… works too
# A new image's snapshot build is charged your build price (`promigence pricing`) once it verifies;
# later uses reuse it free.
# allow_build=True agrees to that; without it the create is refused before anything is sent.
with Sandbox.create(image=image, spend_cap=1, allow_build=True) as sb:
result = sb.commands.run("python -c 'print(6 * 7)' > /tmp/answer.txt")
print(result.exit_code) # 0 (non-zero raises CommandExitError)
print(sb.files.read("/tmp/answer.txt")) # "42\n"
# leaving the block kills the sandbox (sb.kill())
Async is the same shape: async with await AsyncSandbox.create(image=image, spend_cap=1, allow_build=True) as sb:
and await sb.commands.run(...).
An image needs a tag or a digest: python alone is refused, because it would pin whatever python:latest
is today (write python:latest to mean that). To pay for a build once and on purpose, build it with
promigence snapshot create --image python:3.12 --alias py312 and create from the name:
Sandbox.create("py312"), which never builds. on_build= receives the build's answer (build_price,
deduped) before the wait; without it a new build's price is logged as a promigence warning.
The two surfaces
Sandbox/AsyncSandbox— a simple class for one sandbox:create,fork,connect,list,commands.run|list|kill|connect,files.read|write|remove|exists|list,pty,ports,kill,pause,resume,probe,set_timeout,is_running,get_info(get_hostraises while preview URLs are not available in the private beta). A live fork works in two calls:client.sandboxes.snapshot(sandbox_id, alias=None, force=None)(it ends background execs; refused withsecrets_in_snapshotunlessforce=True), thenSandbox.fork(snapshot_id, count=N).snapshots.bundle()is not supported from Python (export with the CLI).sandboxes.exec(..., terminal=True)does not give you a TTY (usesandboxes.pty): it makes the command own the sandbox — when it exits the sandbox is closed on its verdict,task_failedwith a 7-day capture on a non-zero exit. Leave it False to keep using the sandbox.Promigence/AsyncPromigence— the native client. Responses are the wire JSON (snake_case dicts, typed inpromigence.types):
from promigence import Promigence
with Promigence() as client:
build = client.snapshots.create(image="ghcr.io/org/env@sha256:...", verify="pytest -q", tier="small")
done = client.snapshots.wait_build(build["build_id"]) # long-polled, bounded, never kills
run = client.runs.start(done["build"]["snapshot_id"], count=50, spend_cap=5)
client.runs.wait_done(run["run_id"])
report = client.runs.report(run["run_id"]) # env vs task failures, cost
Keys, organizations and projects: client.keys.list|create|revoke, client.orgs.list|set_default|create_key| members|invites|leave, client.orgs.projects.list|create|rename, and client.credits.offer(). Inviting
people, roles, removals, deleting an organization or project, a project's cap, moving a project and the
audit log need a signed-in person, so they are CLI commands (promigence org …, promigence project …).
runs.list_page, snapshots.builds_page and billing.ledger_page return {items, next_cursor}: pass
next_cursor back as cursor until it is None.
snapshots.create takes exactly one input form: image= (a tag such as python:3.12, which the API
pins to its digest, or name@sha256:<digest>),
dockerfile={"context": ..., "path": ...}, or repo= + a full 40-character commit= (with
verify=, required for that form).
Give it a Dockerfile and we build it for you — a directory here, or a repository pinned to a full commit. Both refuse anything that moves, because a snapshot that cannot be rebuilt is not reproducible:
from promigence import pack_context
client.snapshots.create(dockerfile={"context": pack_context("./app"), "path": "ci/Dockerfile"},
setup=["pip install -e ."], verify="pytest -q")
client.snapshots.create(dockerfile={"context": "https://github.com/org/app@<40-char sha>"})
Live sessions
Background processes, directory listing and interactive terminals:
with Sandbox.create(image=image, spend_cap=1) as sb:
# Background: returns a CommandHandle at once. `pid` is the exec_id (a string).
server = sb.commands.run("python -m http.server 3000", background=True,
on_stdout=print, on_stderr=print)
print([p.pid for p in sb.commands.list()])
server.kill() # or sb.commands.kill(server.pid)
print([e.name for e in sb.files.list("/work")]) # EntryInfo: type, size, permissions, …
# Terminal over a WebSocket (stdlib client, Authorization header — never a query string).
term = sb.pty.create(120, 40, on_data=lambda b: print(b.decode(errors="replace"), end=""))
term.send_input("echo hi\n")
term.resize(100, 30)
term.eof() # or term.kill() (SIGKILL), term.disconnect() (SIGHUP)
print(term.wait().exit_code) # PtyExit: exit_code, signal, wall_ms, error
handle.wait()long-polls, returns aCommandResult, and raisesCommandExitErroron a non-zero exit — or an unknown one: an exit with no code is128 + signal, elseNone, never 0.- A command killed at its own
timeout_msraisesCommandTimeoutError, aCommandExitErrorwithcode="task_timeout",timed_out=Trueand a message naming the bound (exit_codestays 137). - Ctrl-C (or a cancelled task) during
Sandbox.createends whatever the create had started, also while itsPOST /runsis still unanswered, then the interruption propagates.handle.disconnect()stops reading; the process keeps running andcommands.connect(pid)re-attaches with its output replayed from the first byte. - A background command cannot take
secrets(its output is served by byte range and is not redacted); the SDK refuses it before sending, with the API's reason. AsyncSandboxis the same surface withawait; callbacks run on the event loop (CommandHandlepolls on a daemon thread,PtySessionreads on one).- Native client:
sandboxes.exec_background,sandboxes.execs.list|get|read|signal|forget,sandboxes.ports.expose|list|revoke,sandboxes.files.list,sandboxes.pty.
Units
Every duration is milliseconds and says so (timeout_ms, wait_timeout_ms), matching the
wire, on purpose: the unit is in the name.
Credentials
Resolved in this order: explicit api_key= / api_url= → PROMIGENCE_API_KEY / PROMIGENCE_API_URL
→ the key promigence login stored (~/.config/promigence/credentials when it is there, else the
system keychain; honours XDG_CONFIG_HOME; shared with the CLI). The stored key is read only when no
key was given: with api_key= or PROMIGENCE_API_KEY the client never opens the credentials file or
asks the system keychain, and that key goes to api_url=, else PROMIGENCE_API_URL, else the default
API. No other environment variable is read for a key.
A key and its URL come from the same place: the stored key goes only to the api_url stored with
it (the default when there is none), and PROMIGENCE_API_KEY not to an api_url= passed
without api_key=. A URL from a higher place than the key's leaves the client without a key, and
every authenticated call raises InvalidArgumentError naming the fix (client.credentials_problem
says why). A credentials file, or its directory, owned by another user or writable by others is
ignored with a warning on the promigence logger.
Every run is capped: spend_cap= → PROMIGENCE_SPEND_CAP → your org's default_spend_cap.
Errors
Everything raised on purpose is a PromigenceError with code, message, fix, docs,
status, request_id and the CLI exit_code: AuthError, NotFoundError,
RateLimitError (retry_after_ms), SpendCapError, PromigenceTimeoutError, UnreachableError,
InvalidArgumentError, EnvFailedError (ours; credited back when our side shows the cause), NotVerifiedError,
NotReleasedError, CommandExitError (exit_code is the command's; CommandTimeoutError when the
command ran past its own timeout_ms), ApiError. An API refusal of your arguments keeps the API's
code (usage) as the TypeScript SDK does.
Only 429 and 503 are retried (at most 3 times, jittered, honouring Retry-After up to 10 s).
Changes
0.1.0b8
- A key you pass (
api_key=) or set (PROMIGENCE_API_KEY) is used as is: the client no longer opens the stored credentials file or asks the system keychain, and that key goes toapi_url=, elsePROMIGENCE_API_URL, else the default API. A stored login'sapi_urlgoes only with the stored key. - The client reads no other environment variable for a key; the compatibility variables are read only by the drop-in package.
- The version follows the npm packages (
0.1.0-beta.8there).
0.1.0b7
- A sandbox's record names the snapshot it started from (
snapshot_id) and when it was created (created_at), and a reattached sandbox carries both. - A paused sandbox's record says when it will be deleted if it is not resumed (
pause_expires_at), when a deletion date applies. snapshots.create()no longer takescpu_class: passing it raisesTypeErrorbefore any request, as a removed option does.snapshots.bundle()no longer calls a route the API does not serve: it raisesUnsupportedErrornamingpromigence snapshot export, which builds the bundle (a missing snapshot is stillNotFoundError).- The result of storing a secret (
SecretStored) includesscan_responses, as the API returns it. - The docstrings and types name only what the API returns: a sandbox's stdin errors are a 400
usage, and the response types drop fields the API never sends. - The version follows the npm packages (
0.1.0-beta.7there).
0.1.0b6
DEFAULT_SNAPSHOT's description names what the default sandbox carries. No behaviour changed.- The version follows the npm packages (
0.1.0-beta.6there).
0.1.0b5
- Packing a directory for a Dockerfile build applies its
.dockerignorewith the build's own rules, and always sends the Dockerfile and its ignore file..gitis not sent unless the ignore file says!.git. Files that look like credentials are refused unlessallow_secrets=True. A snapshot built from a packed directory gets a new id once, because what is sent changed. - A file write keeps the existing file's mode; a new file is
0644unless a mode is given. - The version follows the npm packages (
0.1.0-beta.5there).
0.1.0b4
runs.start()drops a deprecated option that never had an effect and was no longer sent. Passing it now raisesTypeError, which names it, before any request: remove it from the call. (injectstill warns and is ignored.)- A sandbox's handle no longer says where it runs: that attribute of
Sandboxis alwaysNone, and the constructor still accepts it and ignores it. The typed responses drop the fields the API never sends to a customer key. - The version follows the npm packages (
0.1.0-beta.4there).
Development
cd sdk-py && python3 -m venv .venv && .venv/bin/pip install -e '.[dev]' && .venv/bin/pytest -q
Metadata
Release files for promigence 0.1.0b8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| promigence-0.1.0b8.tar.gz | 158.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| promigence-0.1.0b8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 327.6 kB
Release files / promigence-0.1.0b8.tar.gz
| Download URL | promigence-0.1.0b8.tar.gz |
|---|---|
| Size | 158.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e9f0b9e5798a0e777dbc6563a3689dda3c5cee1a0a178a7554e7eab055b037ea
|
|
BLAKE2b-256 checksum How to use checksums |
8ec3aba759f356d2c8aa42516b663bcc2d563c917cb7865666857aee01fe6ce1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / promigence-0.1.0b8-py3-none-any.whl
| Download URL | promigence-0.1.0b8-py3-none-any.whl |
|---|---|
| Size | 169.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b9fd209af7eff1e1b29520afc8a0a610d395271fde2e77a5bec909cd050fa2b5
|
|
BLAKE2b-256 checksum How to use checksums |
31956cc91ef958308287e7e90dd94188400edabbaf5cd774a20b47b36b61231c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|