Metadata-Version: 2.4
Name: pauli-sdk
Version: 0.1.0
Summary: Official Python client for the Pauli quantum compute gateway: OpenQASM 3 in, canonical counts out
Project-URL: Homepage, https://pauli.xyz
Project-URL: Documentation, https://pauli.xyz/docs
Requires-Python: >=3.10
Requires-Dist: anyio>=4
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Description-Content-Type: text/markdown

# pauli-sdk

Official Python client for [Pauli](https://pauli.xyz), the API gateway for
quantum compute: OpenQASM 3 in, canonical counts out, priced on every
eligible device before anything runs.

```sh
pip install pauli-sdk
```

```python
from pauli_sdk import Pauli

client = Pauli()  # reads PAULI_API_KEY
job = client.jobs.run(GHZ_QASM3, shots=1000, policy="cheapest", max_spend_usd="1.50")
res = job.results()
print(res.counts)             # {'000': 489, '111': 511} — cbit0_left on every backend
print(res.billing.price_usd)  # Decimal('0.685750'): the exact wallet debit
```

`Pauli` and `AsyncPauli` expose the same surface (the sync client is
generated from the async source). Get a key at [pauli.xyz](https://pauli.xyz);
docs live at [pauli.xyz/docs](https://pauli.xyz/docs), the full endpoint
reference at [pauli.xyz/reference](https://pauli.xyz/reference).

## Why this API is different, in SDK terms

- **Price before you run.** `client.jobs.estimate(qasm)` prices the circuit
  on every eligible device (cheapest first, with reasoned exclusions);
  `client.jobs.dry_run(qasm, ...)` previews the exact submit — routing
  decision, exact wallet debit, admission check — for free. The `202` from
  `submit()` already carries the routing decision and cost estimate.
- **Money is exact.** Every USD figure is a `decimal.Decimal` parsed from
  the API's decimal strings. Passing a `float` where money is expected
  raises: use `"1.50"` or `Decimal("1.50")`.
- **One bit ordering.** Every backend's counts normalize to `cbit0_left`
  (leftmost character = classical bit 0 of the first-declared register,
  registers in declaration order). `res.counts` is a `Counts` with
  `probabilities()`, `marginal("c")`, `top(n)`, and `to_qiskit()` — the one
  framework whose convention reverses ours.
- **Safe retries.** `submit()` auto-generates an `Idempotency-Key` and
  reuses it across network retries, so a retried submit converges on one
  job; `job.replayed` tells you when the server answered from an earlier
  identical submit. Transient failures retry with jittered backoff,
  honoring `Retry-After`.
- **Typed failure modes.** Errors are keyed on the API's stable machine
  codes (`GET /public/errors` is the registry; `exc.code` is always set).
  The fundable 402 is first-class:

  ```python
  import pauli_sdk

  try:
      job = client.jobs.submit(qasm, shots=100_000)
  except pauli_sdk.InsufficientCreditsError as e:
      print(e.shortfall_usd)       # Decimal
      print(e.challenge.checkout)  # fund it, then resubmit with the same idempotency key
  ```

- **Webhooks verified in one call.** `pauli_sdk.webhooks.unwrap(body,
  headers, secret=...)` implements the documented `X-Pauli-Signature`
  recipe (HMAC-SHA256, 5-minute replay tolerance, rotation overlap) and
  hands back the parsed event; dedupe on `event.event_id`.
- **BYOK.** Register your own provider credentials
  (`client.credentials.put("ionq", api_key=..., accept_provider_terms=True)`)
  and hardware runs under them automatically — the provider bills you
  directly and the wallet pays only the platform fee. Secrets are
  write-only; Braket uses `client.credentials.braket_external_id()` first.
- **Keyless public surface.** `Pauli()` with no key still serves
  `client.public.*`: the live device registry, anonymous fleet estimates,
  the error registry, published benchmarks.

## Waiting on jobs

`job.wait()` polls with jittered exponential backoff (0.5 s → 15 s);
`client.jobs.run(...)` is submit + wait, raising `JobFailedError` /
`JobCancelledError` on the other terminal states. For long queues prefer
webhooks (`webhook_url=` per job, or workspace endpoints via
`client.webhooks.create_endpoint(...)`).

```python
job = client.jobs.submit(qasm, policy="highest_fidelity")
print(job.routing_decision.selected_device, job.cost_estimate.amount)
status = job.wait(timeout=3600)
```

Agents: Pauli also serves a remote MCP endpoint at `https://pauli.xyz/mcp`
(same bearer key). This SDK and MCP are two veneers over the same REST API —
same auth, limits, and billing rails.

## Versioning

The SDK is semver, currently 0.x; the API is `/v1` and additive. Response
models tolerate unknown fields, so an older SDK keeps working against a
newer server.
