> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Generated models, bearer-key authentication, and a typed error taxonomy verified against real HTTP responses.

<Info>**\[AVAILABLE]**, `python/`, v1.0.5. 64 passing tests, 0 placeholders. Three gaps found in a
full SDK audit (no bearer-key auth; two real error-taxonomy bugs; a `PolicyApi.validate()` bug)
were closed and re-verified in that pass, including against a real running local server. A later
pass added `test_quickstart_example.py`, proving the documented quickstart script itself runs
(see [Changelog](/changelog)). See the fix history at the bottom of this page.</Info>

## Install

```bash theme={null}
pip install -e ./python
```

`py.typed` is shipped, this is a PEP 561 typed distribution, confirmed by installing into
a clean venv and checking `parmana.__file__`'s directory for the marker.

## Bearer-key authentication

`ParmanaClient(api_key=...)` is sent as `Authorization: Bearer <api_key>` on every request, set
once on the underlying `requests.Session()`:

```python theme={null}
from parmana import ParmanaClient

client = ParmanaClient(
    endpoint="http://localhost:3000",
    api_key="my-secret-api-key",
)

health = client.execution.health()  # GET /health, no key required
```

`api_key` is optional, for the same reason it's optional server-side: local development against a
server started with `PARMANA_AUTH_DISABLED=true` needs no key. Every real deployment requires
one — an omitted or wrong key gets a real `401`, raised as `AuthenticationError` (see below), not
a silent failure. See [Authentication](/api-reference/authentication) for how keys are minted.

## Models are generated, not hand-maintained

Every model in `python/parmana/models/*.py` is generated directly from the TypeScript AST
of `packages/shared/src/domain/*.ts` (and `CryptoAlgorithms.ts`) by
`python/scripts/generate_models.ts`, not hand-aligned copies. A drift guard
(`npm run check:python-models`, wired into CI) regenerates into memory and fails the build
if the committed output would change. Spot-checked field-for-field against the real JSON
schemas this pass (`Authority.display_name` optional, `Authorization.expires_at` optional,
`BusinessTransactionMetadata` correctly optional-except-`business_transaction_id`,
`ExecutionTrustRecord.settlement_confirmations` optional) — all accurate, no drift found.

Enums are real Python `str, Enum` classes (e.g. `SignatureAlgorithm`,
`VerificationStatus`), not bare strings.

## Errors, correctly mapped to real conditions

```python theme={null}
from parmana import ConflictError, NotFoundError, ValidationError

try:
    client.execution.execute(transaction)
except ConflictError as exc:          # HTTP 409
    print(exc.status_code, exc)
```

| Status                                                           | Exception                |
| ---------------------------------------------------------------- | ------------------------ |
| 400                                                              | `ValidationError`        |
| 401                                                              | `AuthenticationError`    |
| 403, code `POLICY_DENIED`, message starting `Execution rejected` | `ExecutionRejectedError` |
| 403 (no `code`)                                                  | `AuthorizationError`     |
| 404                                                              | `NotFoundError`          |
| 409                                                              | `ConflictError`          |
| any other 5xx                                                    | `ServerError`            |
| connection failure                                               | `NetworkError`           |

All inherit `ParmanaHttpError` → `ApiError`. Proven against the real `HttpTransport` (not a
test double) using `responses`-mocked HTTP built from real, verified response shapes
(`python/tests/test_http_transport.py`), and against an actual running server in
`python/tests/test_live_server_integration.py`.

<Warning>
  **Two real bugs found and fixed this pass, previously live and untested against real
  conditions:**

  1. **`403` was mapped to `ExecutionRejectedError`.** The real `403` is the caller-principal-scoping
     check (`isPrincipalAllowed`, "Caller is not permitted to assert this authority.principalId.") —
     nothing to do with policy rejection. Now a dedicated `AuthorizationError`.
  2. **Real policy rejection reached the caller as `500`, code `RUNTIME_ERROR`, message starting
     `"Execution rejected:"`** — there was no dedicated status code for it at the time.
     `build_http_error` previously classified purely by status code, so this real condition raised
     generic `ServerError`, never the SDK's own purpose-built `ExecutionRejectedError`.

  The previous test for the `403` case (`test_403_raises_execution_rejected_error`) encoded this
  same wrong assumption with a fabricated response body, not a real captured one — fixed alongside
  the source.

  **Since superseded again, current behavior is the table above.** A later server-side fix (see
  [Error catalog](/api-reference/error-catalog)) gave policy rejection its own dedicated `403`
  with `code: "POLICY_DENIED"`, replacing the `500`/`RUNTIME_ERROR` shape bug #2 describes.
  `build_http_error` checks for `code == "POLICY_DENIED"` ahead of the generic `403` branch, so it
  still doesn't collide with bug #1's caller-identity `403` (no `code`). A real risk-rejected
  `test:fixture-execute` execution raises `ExecutionRejectedError` with `status_code == 403` today,
  not `500`.
</Warning>

The client also reuses a `requests.Session()` (connection pooling) and retries idempotent
GETs with backoff on 502/503/504, POSTs are never retried.

## Every endpoint the API exposes has a method

```python theme={null}
client.execution.execute(transaction)          # POST /execute
client.execution.health()                       # GET /health
client.execution.version()                      # GET /version
client.verification.verify(id)                  # POST /verify        (fresh)
client.verification.get_latest(id)              # GET /verification/:id (cached)
client.receipt.generate(id)                     # POST /receipt
client.receipt.get_latest(id)                   # GET /receipt/latest/:id
client.replay.replay(id)                        # POST /replay
client.transactions.create(transaction)         # POST /transactions  (added this pass)
client.transactions.get(id) / .list()           # GET /transactions[/:id]
client.trust_records.get(id)                    # GET /trust-records/:id
client.policy.validate(policy_id, policy_version) # POST /policies/validate
```

`client.transactions.create()` was missing entirely before this pass — a real capability
(`POST /transactions`, a second independent entry point into the identical execution pipeline as
`execute()`, differing only in its `201` status code) with zero SDK coverage. `policy.validate`
takes `(policy_id, policy_version)`, not a policy document, matching what the route actually
reads.

<Note>
  **`policy.validate()` bug fixed this pass.** `POST /policies/validate` never uses the shared
  `{error, code?}` envelope at `400`/`404`: every status it returns (`200`, `400`, `404`) is
  `{valid, errors}`, the caller's answer, not an SDK-level failure. Before this pass, an unknown
  policy raised `NotFoundError` instead of returning `{"valid": False, "errors": [...]}` as
  documented. `401` is not exempted: it's generated by caller-auth middleware before this route's
  own handler runs, using the shared envelope like every other route. See
  [Error handling](/api-reference/error-handling).
</Note>

## Test suite

Previously 26 tests, real but incomplete (no auth coverage, wrong assumption baked into the `403`
test, no real-server integration test). Now 64, `pytest`, confirmed against a real run this pass:
unit tests for every error-mapping case including the two fixed bugs, bearer-key header
attachment tests, and two real-server integration suites. `test_live_server_integration.py`
spawns the actual `@parmana/api` process (`npx tsx packages/api/src/server.ts`, the same entry
point `npm run dev` runs) on a real OS-assigned TCP port and drives it with the real
`ParmanaClient` over real HTTP: a real `401`, a real `403`, a real `400`, a real policy
rejection, a real `404`, a real `409` duplicate, and the `policy.validate()` `404` case, each
asserted against the exact typed result and exact message the server returned.
`test_quickstart_example.py` does the same for `run_quickstart()`, the quickstart example script
itself, additionally asserting its documented printed output still matches what it actually
prints (see [Changelog](/changelog)).

<Note>
  `ruff`/`black`/`mypy` were not reverified in this pass — the dev-tooling extras were not
  installed in this environment. `pytest` (48/48) and live-server verification were run directly;
  the type/lint claim from an earlier pass is not repeated here as current.
</Note>

## Next

<CardGroup cols={2}>
  <Card title="TypeScript SDK" icon="js" href="/sdks/typescript">
    The other maintained SDK — same bearer-key model, same error taxonomy shape.
  </Card>

  <Card title="Error catalog" icon="list" href="/api-reference/error-catalog">
    Every error the real API returns, independent of any SDK.
  </Card>
</CardGroup>
