Metadata-Version: 2.5
Name: 3torus
Version: 0.1.3
Summary: Typed Python client for the 3Torus GPU compute marketplace API
Project-URL: Homepage, https://3torus.com
Project-URL: Documentation, https://docs.3torus.com
Author: 3Torus
License: Proprietary
Keywords: 3torus,compute,gpu,grpc,marketplace
Requires-Python: >=3.11
Requires-Dist: googleapis-common-protos>=1.66.0
Requires-Dist: grpcio>=1.69.0
Requires-Dist: protobuf>=5.29.3
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: mypy>=1.14.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Requires-Dist: ruff>=0.9.0; extra == 'dev'
Description-Content-Type: text/markdown

# 3torus — Python SDK

Typed Python client for the **3Torus GPU compute marketplace** API. The
Python sibling of the [`3torus` CLI](../../cmd/3torus): the same gRPC
core-services surface, exposed as a typed client with [Pydantic](https://docs.pydantic.dev)
models.

> **Status (CLAUDE.md Rule 39):** first leg, **Done@L1** — typed client +
> tests against an in-process fake server. Live-API use (**L2**) needs the
> public API gateway active or a `kubectl port-forward`; the gateway
> activation is operator-gated (m3-plan Phase C — `M3-GRPC-GATEWAY-REST`).
> Several RPCs are contract-only / `Unimplemented` server-side until each
> handler ships under `M3-CORE-SERVICES-SERVER-IMPL`.

## Install

```bash
pip install 3torus          # dist name
```

```python
import threetorus           # import name
```

## Quickstart — submit a GPU job in 5 lines

```python
from threetorus import Client

with Client("api.dev.3torus.com:443", api_key="3t_...", secure=True) as c:
    job = c.jobs.submit(image="nvidia/cuda-samples:vectoradd", gpu_count=1)
    print(job.id, job.status.name)          # gpu-vectoradd-019e943b SUBMITTED
    result = c.jobs.get(job.id)             # poll until terminal
    print(result.status.name, result.result and result.result.output_uri)
```

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 (CLAUDE.md Rule 10). Prefer loading it from
an environment variable or secret store; never hard-code it.

> This is the seed for the GitBook **SDK Quickstart**
> (`M3-GITBOOK-SDK-QUICKSTART`) — keep the 5-line example in sync.

## Verb surface

The client mirrors the CLI's verb groups one-to-one. Each sub-client
fronts one gRPC core service:

| Sub-client          | Service            | Methods                                            |
| ------------------- | ------------------ | -------------------------------------------------- |
| `client.clusters`   | `ComputeService`   | `list`, `get`, `list_nodes`, `capacity`            |
| `client.jobs`       | `JobsService`      | `submit`, `get`, `list`, `cancel`                  |
| `client.billing`    | `BillingService`   | `usage`, `quota`, `invoices`, `balance`            |
| `client.providers`  | `ProvidersService` | `list`, `get`, `gpu_skus`                           |
| `client.health`     | `HealthService`    | `check`                                            |
| `client.auth`       | `AuthService`      | `validate`, `whoami`                               |

```python
clusters, page = c.clusters.list(role=ClusterRole.COMPUTE)
cap = c.clusters.capacity("gpu-dev-pool-4")
usage = c.billing.usage("client-acme")
skus, _ = c.providers.gpu_skus(tier="datacenter")
status = c.health.check()
who = c.auth.validate("3t_...")
```

## Typed models

Responses are [Pydantic](https://docs.pydantic.dev) models with native
Python types — `datetime` for timestamps, `Decimal` for money, `IntEnum`
for enums — instead of raw protobuf accessors:

```python
from threetorus import models as m

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` + the server's message verbatim:

```python
from threetorus import NotFoundError, UnimplementedError, UnavailableError

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))`.

## Roadmap (later legs)

- **L2 live-API verification** against the public gateway / port-forward.
- **PyPI publish** of the `3torus` distribution — automated by
  `release-sdk.yml` (see *Releasing to PyPI* below); the first published
  version waits on the operator's trusted-publisher registration.
- **Async client** (`grpcio` aio) for high-concurrency callers.

See the platform backlog row `M3-PYTHON-SDK`. Sibling rows:
`M3-CLI-3TORUS` (the Go CLI), `M3-GITBOOK-SDK-QUICKSTART` (docs).

## 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
anywhere — not in a repo secret, not in 1Password. The workflow is
[`.github/workflows/release-sdk.yml`](../../.github/workflows/release-sdk.yml)
(`M4-RELEASE-AUTOMATION-SDK-CLI` batch 1).

1. Bump `[project].version` in `pyproject.toml` on a branch, merge to `main`.
2. Tag that commit on `main` and push the tag:

   ```bash
   git tag v0.1.1            # MUST equal pyproject.toml [project].version
   git push origin v0.1.1
   ```

3. The workflow asserts the tag equals the declared version
   (`scripts/sdk-version-from-tag.sh` — a mismatch fails the run, nothing is
   stamped), asserts the tagged commit is reachable from `main`, builds the
   wheel with the canonical `gen/core` stubs inside, runs `twine check`, and
   publishes the **wheel** to <https://pypi.org/project/3torus/>.
4. Verify from a clean environment (the row's L2 probe):

   ```bash
   pip download 3torus==0.1.1 --no-deps -d ./dl
   ```

**Dry run** (build + `twine check` + version assert, no publish):
`gh workflow run release-sdk.yml --ref main` — `dry_run` defaults to `true`.

**Before the first release** the operator registers the workflow as a
trusted publisher on PyPI (operator-actions-register row 29): project
`3torus`, owner `alexperritaz`, repository `3-torus`, workflow
`release-sdk.yml`, environment `pypi`. Until then the publish step fails with
PyPI's "trusted publishing not configured" error — that is the expected
pre-registration state, not a workflow defect.

**Wheel only.** The sdist hatchling emits omits `gen/core` (the force-include
is wheel-scoped), so it cannot be built into a working install; publishing
it would be a lie on the index. Tracked as `M4-SDK-SDIST-OMITS-GEN-CORE`.

## Development

```bash
pip install -e ".[dev]"     # from sdk/python/
ruff check src tests
pytest
```

The generated protobuf stubs are **not** vendored here — the wheel
force-includes the repo's canonical `gen/core` tree (produced by
`buf generate`) as the `core` package, so the SDK never drifts from the
proto contracts (`scripts/check-proto-codegen-sync.sh`).
