Skip to main content
[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). See the fix history at the bottom of this page.

Install

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():
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 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

All inherit ParmanaHttpErrorApiError. 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.
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) 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.
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

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

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

Next

TypeScript SDK

The other maintained SDK — same bearer-key model, same error taxonomy shape.

Error catalog

Every error the real API returns, independent of any SDK.