Skip to content

The Tenor Python SDK provides native-speed contract evaluation via PyO3 bindings (not WASM, not subprocess). The Rust evaluator core is compiled directly into a Python extension module, giving you the same evaluation performance as the CLI with a Pythonic API.

Installation

bash
pip install tenor-sdk

Pre-built wheels are available for:

  • Linux x86_64 (glibc 2.17+)
  • Linux aarch64 (glibc 2.17+)
  • macOS x86_64 (10.15+)
  • macOS aarch64 / Apple Silicon (11.0+)
  • Windows x86_64

No Rust toolchain is required for installation. The PyO3 native extension is bundled in the wheel.

Requires Python 3.9 or later.

TenorEvaluator — Local Native Evaluation

TenorEvaluator loads a compiled contract and evaluates it in-process via the Rust core. No network calls, no API key, no platform dependency.

Loading a Contract

python
from tenor import TenorEvaluator

# Load from file path
evaluator = TenorEvaluator.load("./contracts/escrow.tenor.wasm")

# Load from bytes
with open("./contracts/escrow.tenor.wasm", "rb") as f:
    evaluator = TenorEvaluator.from_bytes(f.read())

Evaluating a Contract

python
result = evaluator.evaluate(
    facts={
        "payment_received": True,
        "payment_amount": "5000.00",
        "customer_tier": "premium",
    },
    entities={
        "Order": {"order_001": "pending"},
        "Payment": {"pay_001": "cleared"},
    },
)

print(result.verdicts)
# {
#     "payment_ok": Verdict(value=True, type="Bool", stratum=0),
#     "eligible_for_release": Verdict(value=True, type="Bool", stratum=1),
# }

print(result.evaluation_time_ms)
# 0.3

Computing the Action Space

python
actions = evaluator.action_space(
    facts={
        "payment_received": True,
        "payment_amount": "5000.00",
        "customer_tier": "premium",
    },
    entities={
        "Order": {"order_001": "pending"},
        "Payment": {"pay_001": "cleared"},
    },
)

print(actions)
# {
#     "escrow_agent": [
#         Action(
#             operation="release_funds",
#             precondition_met=True,
#             entities_affected=[
#                 EntityTransition(entity="Order", instance="order_001", from_state="pending", to_state="released")
#             ],
#         )
#     ],
#     "buyer": [
#         Action(
#             operation="dispute_transaction",
#             precondition_met=True,
#             entities_affected=[
#                 EntityTransition(entity="Order", instance="order_001", from_state="pending", to_state="disputed")
#             ],
#         )
#     ],
#     "seller": [],
# }

Action Space for a Single Persona

python
buyer_actions = evaluator.action_space(
    facts={"payment_received": True, "payment_amount": "5000.00"},
    entities={"Order": {"order_001": "pending"}},
    persona="buyer",
)

for action in buyer_actions:
    print(f"{action.operation}: precondition_met={action.precondition_met}")
# dispute_transaction: precondition_met=True

Contract Metadata

python
meta = evaluator.metadata()

print(meta.entities)
# [
#     Entity(name="Order", states=["pending", "approved", "disputed", "released"], initial="pending"),
#     Entity(name="Payment", states=["held", "released", "refunded"], initial="held"),
# ]

print(meta.personas)
# ["escrow_agent", "buyer", "seller"]

print(meta.facts)
# [
#     FactDecl(name="payment_received", type="Bool"),
#     FactDecl(name="payment_amount", type="Money"),
#     FactDecl(name="customer_tier", type="Text"),
# ]

print(meta.contract_hash)
# "sha256:9f4a2b..."

Error Handling

python
from tenor import (
    TenorEvaluator,
    TenorTypeError,
    TenorUndeclaredFactError,
)

try:
    evaluator.evaluate(
        facts={"payment_received": "yes"},  # Wrong type: expected Bool
        entities={},
    )
except TenorTypeError as e:
    print(e.fact)           # "payment_received"
    print(e.expected_type)  # "Bool"
    print(e.received_type)  # "Text"
    print(e)                # "Fact 'payment_received' expected type Bool, got Text"

try:
    evaluator.evaluate(
        facts={"nonexistent_fact": True},  # Undeclared fact
        entities={},
    )
except TenorUndeclaredFactError as e:
    print(e.fact)  # "nonexistent_fact"
    print(e)       # "Fact 'nonexistent_fact' is not declared in this contract"

TenorClient — Remote HTTP API

TenorClient calls the Tenor platform for evaluation and execution against managed state.

Constructor

python
from tenor import TenorClient

client = TenorClient(
    org="acme",
    api_key="tk_live_9f4a2b3c4d5e6f7a8b9c0d1e2f3a4b5c",
    # Optional overrides:
    base_url="https://api.tenor.run",  # default
    timeout=10.0,  # seconds, default 10.0
    retries=3,  # automatic retry on 429/5xx, default 3
)

Evaluate

python
result = client.evaluate(
    contract="escrow",
    facts={"payment_received": True, "payment_amount": "5000.00"},
)

print(result.verdicts)
# {"payment_ok": Verdict(value=True, ...), "eligible_for_release": Verdict(value=True, ...)}

print(result.action_space)
# {"escrow_agent": [...], "buyer": [...], "seller": []}

Execute a Flow

python
execution = client.execute_flow(
    contract="escrow",
    flow="release_flow",
    persona="escrow_agent",
    facts={
        "release_approved": True,
        "release_reason": "Goods received and inspected",
    },
    entity_bindings={
        "Order": "order_001",
        "Payment": "pay_001",
    },
    idempotency_key="idem_20260215_001",
)

print(execution.status)
# "completed"

for step in execution.steps_executed:
    print(f"{step.step}: {step.result}")
    for t in step.entity_transitions:
        print(f"  {t.entity}:{t.instance} {t.from_state} -> {t.to_state}")
# verify_approval: success
#   Order:order_001 pending -> approved
# release_payment: success
#   Payment:pay_001 held -> released

print(execution.provenance_chain_id)
# "chain_20260215_001"

Simulate

python
simulation = client.simulate(
    contract="escrow",
    flow="release_flow",
    persona="escrow_agent",
    facts={"release_approved": True},
    entity_bindings={"Order": "order_001", "Payment": "pay_001"},
)

print(simulation.status)
# "would_succeed"

for step in simulation.projected_steps:
    print(f"{step.step}: would_succeed={step.would_succeed}")
# verify_approval: would_succeed=True
# release_payment: would_succeed=True

Error Handling

python
from tenor import (
    TenorClient,
    TenorApiError,
    TenorForbiddenError,
    TenorPreconditionError,
    TenorConflictError,
    TenorRateLimitError,
)

try:
    client.execute_flow(
        contract="escrow",
        flow="release_flow",
        persona="buyer",  # Not authorized for release_funds
        facts={"release_approved": True},
        entity_bindings={"Order": "order_001"},
    )
except TenorForbiddenError as e:
    print(e.persona)           # "buyer"
    print(e.operation)         # "release_funds"
    print(e.allowed_personas)  # ["escrow_agent"]

except TenorPreconditionError as e:
    print(e.operation)      # "verify_release"
    print(e.precondition)   # "verdict_present(release_approval_ok)"
    print(e.error_message)  # "Release conditions not met."

except TenorConflictError as e:
    print(e.entity)          # "Order"
    print(e.instance)        # "order_001"
    print(e.expected_state)  # "pending"
    print(e.actual_state)    # "disputed"

Full Working Example: FastAPI Service

A complete example using the Python SDK with FastAPI to build an escrow management API:

python
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
from tenor import TenorClient, TenorForbiddenError, TenorPreconditionError
import os

app = FastAPI(title="Escrow Service")

tenor = TenorClient(
    org="acme",
    api_key=os.environ["TENOR_API_KEY"],
)


class ReleaseRequest(BaseModel):
    reason: str


class DisputeRequest(BaseModel):
    reason: str


@app.get("/orders/{order_id}/actions")
async def get_actions(order_id: str, x_persona: str = Header()):
    """Return available actions for the given persona and order."""
    try:
        actions = tenor.actions(
            contract="escrow",
            persona=x_persona,
        )
        return {
            "persona": x_persona,
            "order_id": order_id,
            "actions": [
                {
                    "operation": a.operation,
                    "available": a.precondition_met,
                }
                for a in actions
            ],
        }
    except TenorForbiddenError:
        raise HTTPException(status_code=403, detail=f"Unknown persona: {x_persona}")


@app.post("/orders/{order_id}/release")
async def release_order(order_id: str, body: ReleaseRequest, x_persona: str = Header()):
    """Execute the release flow for an order."""
    try:
        result = tenor.execute_flow(
            contract="escrow",
            flow="release_flow",
            persona=x_persona,
            facts={
                "release_approved": True,
                "release_reason": body.reason,
            },
            entity_bindings={"Order": order_id},
        )
        return {
            "status": result.status,
            "provenance_chain_id": result.provenance_chain_id,
            "steps": [
                {"step": s.step, "result": s.result}
                for s in result.steps_executed
            ],
        }
    except TenorForbiddenError as e:
        raise HTTPException(
            status_code=403,
            detail=f"Persona '{e.persona}' cannot perform '{e.operation}'",
        )
    except TenorPreconditionError as e:
        raise HTTPException(status_code=422, detail=e.error_message)


@app.post("/orders/{order_id}/dispute")
async def dispute_order(order_id: str, body: DisputeRequest, x_persona: str = Header()):
    """Execute the dispute flow for an order."""
    try:
        result = tenor.execute_flow(
            contract="escrow",
            flow="dispute_flow",
            persona=x_persona,
            facts={"dispute_reason": body.reason},
            entity_bindings={"Order": order_id},
        )
        return {
            "status": result.status,
            "provenance_chain_id": result.provenance_chain_id,
        }
    except TenorForbiddenError as e:
        raise HTTPException(
            status_code=403,
            detail=f"Persona '{e.persona}' cannot perform '{e.operation}'",
        )
    except TenorPreconditionError as e:
        raise HTTPException(status_code=422, detail=e.error_message)

Full Working Example: Contract Testing with pytest

python
import pytest
from tenor import TenorEvaluator


@pytest.fixture
def evaluator():
    return TenorEvaluator.load("./contracts/escrow.tenor.wasm")


def test_payment_verdict_fires_when_payment_received(evaluator):
    result = evaluator.evaluate(
        facts={"payment_received": True, "payment_amount": "5000.00"},
        entities={"Order": {"order_001": "pending"}},
    )
    assert result.verdicts["payment_ok"].value is True
    assert result.verdicts["payment_ok"].stratum == 0


def test_payment_verdict_absent_when_no_payment(evaluator):
    result = evaluator.evaluate(
        facts={"payment_received": False, "payment_amount": "0.00"},
        entities={"Order": {"order_001": "pending"}},
    )
    assert "payment_ok" not in result.verdicts


def test_release_requires_payment_and_approval(evaluator):
    result = evaluator.evaluate(
        facts={
            "payment_received": True,
            "payment_amount": "5000.00",
            "release_approved": True,
        },
        entities={"Order": {"order_001": "pending"}},
    )
    assert result.verdicts["eligible_for_release"].value is True
    assert result.verdicts["eligible_for_release"].stratum == 1


def test_release_blocked_without_payment(evaluator):
    result = evaluator.evaluate(
        facts={
            "payment_received": False,
            "release_approved": True,
        },
        entities={"Order": {"order_001": "pending"}},
    )
    assert "eligible_for_release" not in result.verdicts


def test_action_space_escrow_agent_can_release(evaluator):
    actions = evaluator.action_space(
        facts={
            "payment_received": True,
            "payment_amount": "5000.00",
            "release_approved": True,
        },
        entities={"Order": {"order_001": "pending"}},
        persona="escrow_agent",
    )
    ops = [a.operation for a in actions if a.precondition_met]
    assert "release_funds" in ops


def test_action_space_buyer_cannot_release(evaluator):
    actions = evaluator.action_space(
        facts={
            "payment_received": True,
            "payment_amount": "5000.00",
            "release_approved": True,
        },
        entities={"Order": {"order_001": "pending"}},
        persona="buyer",
    )
    ops = [a.operation for a in actions]
    assert "release_funds" not in ops


def test_terminal_state_has_no_actions(evaluator):
    actions = evaluator.action_space(
        facts={"payment_received": True, "payment_amount": "5000.00"},
        entities={"Order": {"order_001": "released"}},
    )
    for persona, persona_actions in actions.items():
        order_actions = [
            a for a in persona_actions
            if any(e.entity == "Order" for e in a.entities_affected)
        ]
        assert len(order_actions) == 0, f"{persona} has actions on terminal Order"