3torus — Python SDK
Typed Python client for the 3Torus GPU compute marketplace API — the
Python sibling of the 3torus CLI. It exposes the platform's public gRPC
surface as a typed client with Pydantic models,
a typed exception hierarchy and built-in retry.
Install
pip install 3torus # distribution name
import threetorus # import name
Python ≥ 3.11.
Quickstart — submit a GPU job in 5 lines
from threetorus import Client
with Client(api_key="3t_...") as c: # public gateway, TLS — the default
job = c.jobs.submit(image="busybox:1.36", gpu_count=1, command=["sh", "-c"], args=["echo hello"])
print(job.id, job.status.name) # gpu-job-1a2b3c4d5e6f SUBMITTED
result = c.jobs.get(job.id) # poll until terminal
print(result.status.name, result.result and result.result.exit_code)
Since 0.3.1 Client() defaults to the public gateway, api.dev.3torus.com:443,
over TLS, so the address is optional. Callers inside the platform pass their
own address; plaintext is then chosen automatically for local and in-cluster
hosts, and secure=True / secure=False override the choice. (On 0.3.0, pass
"api.dev.3torus.com:443" and secure=True explicitly.)
The API key rides on every RPC as an authorization: Bearer <key> gRPC
metadata header — it is never logged, never placed in the address, and
never echoed in an exception. The SDK reads no environment variable for it:
load it yourself from an environment variable or secret store and pass it
explicitly; never hard-code it. Each method also needs a scope on the key —
a key without it gets PermissionDeniedError.
The GitBook SDK Quickstart is the published copy of this page — keep the two in sync.
Verb surface
Each sub-client fronts one gRPC service:
| Sub-client | Service | Methods |
|---|---|---|
client.clusters |
user.v1 ComputeService (your own clusters) |
list, get |
client.jobs |
user.v1 JobsService |
submit, get, result, list, cancel |
client.billing |
user.v1 BillingService |
usage, quota, invoices, balance |
client.providers |
core.v1 ProvidersService |
list, get, gpu_skus |
client.health |
core.v1 HealthService |
check |
client.auth |
core.v1 AuthService (validate), user.v1 IdentityService (whoami → Identity) |
validate, whoami |
from threetorus import models as m
clusters, page = c.clusters.list(phase=m.ClusterPhase.READY) # your tenant only
usage = c.billing.usage() # the caller's current period
quota = c.billing.quota() # limits + consumption
skus, _ = c.providers.gpu_skus(tier="datacenter")
status = c.health.check()
me = c.auth.whoami()
jobs.submit parameters
Keyword-only: image (required), gpu_count (0 means 1; the upper bound is
the largest GPU node in the fleet), name, command / args (at most 64
entries together), env (at most 64 entries, non-empty keys), gpu_model
and priority (not supported yet — a non-empty / non-zero value is rejected
with UnimplementedError). submit and cancel are never retried
automatically.
Typed models
Responses are Pydantic models with native
Python types — datetime for timestamps, Decimal for money, IntEnum
for enums — instead of raw protobuf accessors:
job: m.Job = c.jobs.get("job-id")
job.status # m.JobStatus.COMPLETED (IntEnum, .name == "COMPLETED")
job.completed_at # datetime | None (UTC; None when unset)
usage.estimated_cost # m.Money(amount=Decimal("4.20"), currency="USD")
Errors
Every gRPC failure is mapped to a typed exception carrying the original
grpc.StatusCode and the server's message verbatim:
from threetorus import NotFoundError
try:
c.clusters.get("does-not-exist")
except NotFoundError as e:
print(e.code, e.message) # StatusCode.NOT_FOUND ...
UNAVAILABLE / DEADLINE_EXCEEDED are retried automatically with capped
exponential backoff before surfacing; permanent codes are never retried.
Override the policy with Client(..., retry=RetryPolicy(max_attempts=5)).
Known limits
- Resubmitting an identical job returns the existing one. The SDK sends no
idempotency key, so a job is identified by its spec (image, name, GPU count,
command, args, env); submitting the same spec again returns the job you
already have. Change
nameto run it again. jobs.submit()does not yet expose a result output path, a maximum run time, or a start tier (the CLI'sjobs submitdoes:--output,--max-run-seconds,--start-tier).providers.gpu_skus(provider=…)is rejected withUnimplementedError; filter bytier=instead.- Paging is offset-based (
page=/page_size=); cursor pagination is not supported. - With a machine key,
auth.whoami()returns an emptyuser_idand may return an emptytenant_id.
Version 0.3.0 was checked against the live gateway after release:
health.check() returned SERVING, and a clusters.list() without a key was
refused with UnauthenticatedError.
Changelog
Pre-1.0: a minor bump may break the API; a patch bump never does. The
version lives in TWO places that must match — pyproject.toml
[project].version and threetorus.__version__ (the latter rides every RPC
in the x-3torus-client header) — and the release tag must equal them.
0.3.1
Changed:
Client()now defaults to the public gateway,api.dev.3torus.com:443, over TLS. The previous default address was reachable only from inside the platform.securenow defaults toNone= choose from the address, with the same rule as the CLI: TLS for port 443 or any non-local host; plaintext forlocalhost, loopback IPs, Kubernetes service names (*.svc,*.svc.cluster.local) and bare host names. An explicitsecure=True/secure=Falseis honoured as before.
Not breaking for callers that pass their address: those hosts keep the transport they had. A caller that relied on the old default address now reaches the public gateway.
Docs: JobArtifact.download_url is signed for the platform's public
object-store endpoint, so it can be fetched from outside the platform until
it expires (the docstring used to say otherwise).
0.3.0 (2026-10-11)
Breaking — client.clusters now speaks the tenant-scoped
torus.user.v1.ComputeService:
clusters.list()returns only YOUR tenant's clusters;clusters.get()of another tenant's cluster raisesNotFoundError.clusters.list(role=…)removed — passing it raisesTypeError.clusters.list_nodes()andclusters.capacity()removed (never implemented server-side; no user-plane equivalent).models.Node,models.Capacity,models.ClusterRoleremoved with them.models.Clusterdropsrole, gainsgpu_model.
Fixed: jobs.list(page=…/page_size=…) and billing.invoices(page=…/ page_size=…) raised TypeError before sending anything.
0.2.0 (never released)
Jobs, billing and auth.whoami() moved to torus.user.v1. Superseded by
0.3.0 before any release.
Development (contributors)
pip install -e ".[dev]" # from sdk/python/ in a checkout of the platform repo
ruff check src tests
pytest
The generated protobuf stubs are not vendored here — the wheel
force-includes the platform repo's canonical gen/core and gen/user trees
(produced by buf generate), so the SDK never drifts from the proto
contracts.
Releasing to PyPI
Releases are tag-driven and publish through PyPI trusted publishing
(GitHub OIDC → short-lived upload token); there is no long-lived PyPI token.
The workflow is .github/workflows/release-sdk.yml.
- Bump
[project].versioninpyproject.tomlandthreetorus.__version__on a branch, merge tomain. - Tag that commit on
mainand push the tag (git tag v0.3.1 && git push origin v0.3.1— the tag MUST equal the declared version). - The workflow asserts the tag equals the declared version, asserts the
tagged commit is reachable from
main, builds the wheel with the stubs inside, runstwine check, and publishes the wheel to https://pypi.org/project/3torus/. - Verify from a clean environment:
pip install 3torus==<version>.
Dry run (build + twine check + version assert, no publish):
gh workflow run release-sdk.yml --ref main — dry_run defaults to true.
Wheel only. The sdist omits the generated stubs (the force-include is wheel-scoped), so it cannot be built into a working install and is not published.
Metadata
Release files for 3torus 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 3torus-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Release files / 3torus-0.3.1-py3-none-any.whl
| Download URL | 3torus-0.3.1-py3-none-any.whl |
|---|---|
| Size | 305.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6f2be58175cb22491e4d4a82fef21eaa4d2a0f74a654595526dd565e748197d
|
|
BLAKE2b-256 checksum How to use checksums |
9b6331b5f52759aff8780d7a5e57f6a928645da3bfac7ccb3e04ddfd9cba12e6
|
| 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 Oct 12, 2026.
Transparency log