Metadata-Version: 2.4
Name: extro-sim
Version: 0.4.0
Summary: Python client for the Extropic thermodynamic compute cloud
License: Apache-2.0
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Requires-Dist: cloudpickle>=2
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# extro-sim — Python client

## Install

```sh
# hosted archive (the console serves the packaged client at /package.zip)
pip install https://extropic.dev/package.zip
# or from a checkout of the repo
pip install ./clients/python
```

Not on PyPI yet — the hosted zip is the install path until `pip install
extro-sim` lands there (the distribution name; it imports as `extro_sim`).

> **Do NOT `pip install extropic`** — the PyPI package by that name is an
> unrelated squatted package (no client API, no console script). Always install
> from the repo as above. Post-install smoke check:
> `python -c "import extro_sim; print(extro_sim.DEFAULT_API_URL)"`

Defaults are baked for the production deployment (`api.extropic.dev`); no
configuration needed. Auth is `extro-sim auth` (browser login, stored session) —
and a job submitted from an interactive terminal without a session triggers the
same flow automatically. Set `STAGING=1` to target the staging deployment
(`api.staging.extropic.dev`) instead.

A first-class Python interface to the Extropic thermodynamic compute cloud.
Decorate a function, then run it on the cloud with `.remote()` / `.submit()`.

```python
import extro_sim as xtr

xtr.configure(api_url="https://api.extropic.dev", token="<supabase-jwt>")

@xtr.job(tier="cpu", timeout_s=120)
def add(a, b):
    return {"sum": a + b}

add(2, 3)            # {"sum": 5}  — runs locally, like any function
add.local(2, 3)      # {"sum": 5}  — explicit local run
add.remote(2, 3)     # {"sum": 5}  — runs on Extropic, blocks for the result

job = add.submit(2, 3)   # fire-and-forget, returns a handle
job.state                # "running" | "succeeded" | ...
job.result()             # blocks, returns {"sum": 5}
job.cancel()
```

## Configuration

`xtr.configure(...)` or environment variables:

| | env | notes |
|---|---|---|
| `api_url` | `EXTROPIC_API_URL` | default `https://api.extropic.dev` |
| — | `STAGING` | `STAGING=1` targets staging (`api.staging.extropic.dev`) as a set |
| `token` | `EXTROPIC_TOKEN` | Supabase JWT — overrides the `extro-sim auth` session (CI/staging) |
| `user_id` | `EXTROPIC_USER_ID` | dev identity (local, `X-User-Id`) |

## Hardware

Pass `tier=` to `@xtr.job` — labels state the hardware explicitly:

`cpu` (1 vCPU · 4 GB) · `l4` (4 vCPU · 16 GB · 1×L4) · `a100` (8 vCPU · 64 GB · 1×A100 80GB) · `h100` (8 vCPU · 64 GB · 1×H100)

### GPU tiers: JAX must be the CUDA build

A plain `pip install thrml` (or `jax`) pulls **CPU-only** jaxlib — on the GPU
tiers the GPU sits idle and JAX silently runs on `cpu:0`. Install the CUDA 13
extra explicitly:

```sh
pip install "jax[cuda13]" thrml
```

Avoid `jax[cuda12]`: on newer GPUs it runs, but cuBLAS < 13.2 has a known
concurrent-kernel TMEM bug (silent data corruption warning at import). Sanity
check inside the job before doing real work:

```python
import jax
assert jax.default_backend() == "gpu", jax.devices()
```

## API contract notes

- **Rate limits**: submit bursts can 429 — raised as typed `RateLimited` with
  `.retry_after` (seconds). Submits are never retried automatically (a blind
  retry could double-submit); honor `retry_after` when fanning out.
- **Auth expiry**: if the stored session can't be refreshed the client raises
  `extro_sim.auth.AuthExpired` *before* any request — run `extro-sim auth login`.
- **`GET /jobs/{id}/result`** currently returns the full job record (not a
  narrow result view); `Job.result()` handles either shape.
- **`DELETE /jobs/{id}`** means *cancellation requested* — the response may
  still show `pending`/`running`; poll until terminal (`cancelled`).
- **`/healthz` requires auth** by design; there is no anonymous health probe.
- **output & tracebacks**: `stdout`, `stderr` and `result` are artifacts —
  durable objects in the store, synced while the job runs and always flushed
  before the runner exits (a failed flush errors the job rather than losing
  bytes). `Job.follow()` subscribes to all three from the store (chunks a few
  seconds behind live; ends with the result / raises the terminal error);
  `Job.stream_logs(stream=...)` follows one stream and uses the live per-job
  tunnel (`runner_url` on the view) when present for lower latency, artifact
  polling otherwise. On failure the full Python `traceback` is an artifact
  and rides in the `JobFailed` message (`e.traceback`).
- **status polling never false-fails**: `Job.result()`/`.wait()` treat a 429
  during polling as retry-after-and-continue, never as a job failure — a
  rate-limited poll of a healthy running job must not report it dead.
- **You are never billed before compute starts**: a job stays `pending`
  (unbilled, hold only) until its sandbox is actually allocated. If the
  backend can't provide capacity within the start deadline the job ends
  `at_capacity` — retryable, $0 charged. `timeout_s` counts from when your
  code's sandbox exists, not from submit.

## Errors

`.result()` raises a typed error for non-success states: `JobFailed`,
`OutOfCredits`, `AtCapacity` (retry later), `JobCancelled` — all subclasses of
`ExtropicError`. A 503 on submit also raises `AtCapacity`.

## CLI

Installing the client (from the repo — see Install above) also installs the
`extro-sim` command — **submit · supervise · collect**:

> `submit`/`run` ship the file's **source** into the sandbox — keep the file
> plain (no top-level `import extro_sim`, no decorator; the base image doesn't
> have the package). The decorated `@xtr.job` + `.remote()` form is the SDK
> path and never goes through `extro-sim run`.

```bash
extro-sim auth                            # log in via the browser (reuses your console session)
extro-sim submit job.py --entrypoint main --tier cpu --arg 2 --arg 3 --wait
extro-sim run job.py --arg 2 --arg 3     # submit + supervise + collect in one shot
extro-sim jobs                            # list your jobs
extro-sim supervise <id>                  # stream state/progress until it finishes
extro-sim collect <id>                    # wait for + print the result
extro-sim logs <id> -f                    # follow a job's stdout live
extro-sim cancel <id>
extro-sim balance                         # credit balance + holds
extro-sim recharge 25                     # start a top-up (finish the card in the web console)
```

`extro-sim auth` opens the web console, which signs you in with Supabase (instantly
if you're already logged in there) and hands the session back to the CLI; it's
stored under `~/.config/extropic/credentials.json` and auto-refreshed. `auth status`
/ `auth logout` manage it. Point the CLI at your console + API with
`EXTROPIC_FRONTEND_URL` and `EXTROPIC_API_URL` (or `--api-url`); the edge/WAF
secret must be supplied at runtime as `EXTROPIC_EDGE_SECRET` and is never baked
into the package. `--arg`/`--kwarg` values parse as JSON when possible, else as
strings.

## Constraints (v1)

Functions ship via **cloudpickle**: closures, objects, and non-JSON args/results
all work. The pickle carries interpreter bytecode, so the sandbox runs a matching
Python/cloudpickle (the SDK ships both versions in the job envelope); the
function's imports must exist in the sandbox image for its tier.
