OriginChain docs
05 · python sdk

Python SDK.

A thin shell around the HTTP API. Constructor takes base_url, bearer, and tenant; auto-generates idempotency keys for retries; auto-backs off on 429 and 5xx; raises a typed exception per status class. The async variant mirrors the surface exactly.

install
pip install originchain
requires
python >= 3.10 · httpx
version
stable surface · semver-tracked
construct a client
OriginChain(...)
from originchain import OriginChain

db = OriginChain(
    base_url="https://acme.originchain.ai",
    bearer="oc_live_...",
    tenant="01HW7G5...",
    timeout=30.0,        # per-request, seconds
    max_retries=3,       # 429 / 5xx with exponential backoff + jitter
)

db.schemas

Register a TOML manifest, list ids, fetch raw TOML.

db.schemas.{register,list,get}
# TOML manifest source (read from disk or composed inline).
toml_src = open("trading.orders.toml").read()

resp = db.schemas.register(toml_src)
# -> {"id": "trading.orders", "version": 1}

db.rows

Typed CRUD over a registered schema. put_batch chunks a generator into atomic WAL frames; each chunk gets a derived idempotency key, so partial retries don't double-write.

db.rows.{put,get,put_batch}
# Single-row upsert. Idempotency-Key auto-generated if you don't pass one.
import uuid

resp = db.rows.put(
    "trading.orders",
    { "order_id": "o-0001", "symbol": "AAPL", "qty": 100 },
    expect_insert=False,                # set True to skip the prior-state read
    idempotency_key=str(uuid.uuid4()),  # safe to retry
)
# -> {"ok": True, "lsn": {"segment": 4, "offset": 8421007}}

db.ask · db.query

Two paths into the executor. db.ask compiles a natural-language query into a Plan; db.query takes a hand-authored Plan tree directly.

db.ask · db.query
# Compiled to a Plan via the rule grammar (and optionally Bedrock).
res = db.ask("last 10 orders where status is pending",
             schemas=["trading.orders"])
for row in res["rows"]:
    print(row)

AsyncOriginChain

Same surface, asyncio-friendly. The async client owns its own httpx.AsyncClient so it composes cleanly with FastAPI / Starlette workers. Always await db.aclose() on shutdown.

AsyncOriginChain
import asyncio
from originchain import AsyncOriginChain

async def main():
    db = AsyncOriginChain(
        base_url="https://acme.originchain.ai",
        bearer="oc_live_...",
        tenant="01HW7G5...",
    )
    try:
        await db.schemas.list()
        await db.rows.put("trading.orders",
                          { "order_id": "o-1", "symbol": "AAPL", "qty": 100 })
    finally:
        await db.aclose()

asyncio.run(main())

Typed errors

Every HTTP failure surfaces as a subclass of OCError. Catch the specific class to decide retryability without inspecting status codes.

Exception Status Retry? Meaning
OCAuthError401 / 403noBearer missing or wrong-tenant.
OCNotFoundError404noResource doesn't exist.
OCValidationError400noBody / params malformed.
OCRateLimitedError429yes (after retry_after)Per-bearer bucket drained.
OCServerError5xxif idempotentServer-side failure.
OCReplicationDegraded2xx (warning)n/aWrite committed, follower lagged. Promote to alert if RPO=0 matters.
exception handling
from originchain import (
    OriginChain,
    OCAuthError, OCNotFoundError, OCRateLimitedError,
    OCValidationError, OCServerError,
)

db = OriginChain.from_env()
try:
    db.rows.put("trading.orders", { "qty": "not-an-int" })
except OCValidationError as e:
    # 400 — DON'T retry. Body is malformed.
    print("client bug:", e, e.body)
except OCAuthError as e:
    # 401 / 403 — DON'T retry. Bearer is wrong or out of scope.
    raise
except OCNotFoundError as e:
    # 404 — schema or row doesn't exist.
    raise
except OCRateLimitedError as e:
    # 429 — Retry after `e.retry_after` seconds.
    time.sleep(e.retry_after)
except OCServerError as e:
    # 5xx — server-side. Retry with backoff if idempotent.
    raise
retries & idempotency

The client retries up to max_retries (default 3) on 429 and 5xx with exponential backoff (0.25 s, 0.5 s, 1 s — jitter capped at 4 s). When the server returns Retry-After, that wins. Mutating writes are safe to retry only when Idempotency-Key is set; the client auto-derives a per-chunk key in put_batch, but for single-row put you should pass one yourself.

Raw HTTP escape-hatch

The SQL, vector, full-text, graph, watch, and migrations endpoints are reached via the underlying db._client (an httpx.Client) — typed wrappers are landing in upcoming releases. The auth header and base URL are already wired.

The HTTP shapes are fully documented on the API reference page; copy-paste the JSON body and you're good.

db._client.post / .get
# /v1/sql doesn't have a typed Python method yet — drop into
# the underlying httpx.Client to call it directly.
resp = db._client.post(
    f"/v1/tenants/{db.tenant}/sql",
    json={ "sql": "SELECT order_id, symbol FROM trading.orders WHERE status = 'pending' LIMIT 10" },
)
resp.raise_for_status()
data = resp.json()
# -> {"kind": "select", "rows": [...]}