Chio/Docs
LOGIN · JOIN

Python HTTP Frameworks

Run Larkfield's pricing service in FastAPI and Django, with every quote admitted by the kernel before the view runs.

Guide chio@5f63e6e

On this page

The pricing code from the Quickstart is Python, and so is the service most teams would wrap around it. FastAPI and Django each keep their routes, validation, and SQLite store; Chio decides who may ask for a quote and signs the decision.

Starts from chio new pricing-python --example pricing-python. The middleware runs in Python, and evaluation and signing run in a sidecar beside it. The architecture diagram shows that boundary. Callers send requests to the Python service directly.

Start

Give the pricing service a governed entry point

A quote at 80 percent off is a commitment to a customer, so POST /v1/quote is marked side-effecting and needs a grant. The service issues the quote, records it, and returns it; Chio admits the request first and signs a receipt for the decision, allowed or refused.

1. Start the service and its sidecar

You need Python 3.11 or newer and uv. The launcher installs chio-asgi and chio-django from PyPI, starts FastAPI on port 8091 beside a sidecar on 9098, and issues a five-minute grant to Basalt's buying agent. You can also download the project.

Terminal 1bash
chio new pricing-python --example pricing-python && cd pricing-python
python3 run.py
Output
Pricing: http://127.0.0.1:8091/v1/quote
Quote: python3 run.py quote sku-118 10
List: python3 run.py list
Revoke: python3 run.py revoke
State and receipts: .pricing/
Ctrl-C stops the services and keeps your quotes.

2. Quote a deep discount

In a second terminal in the same directory, quote sku-301 at 80 percent and list the quotes issued.

Terminal 2 · pricing-pythonbash
python3 run.py quote sku-301 80
python3 run.py list
Output
{
  "status": 201,
  "body": { "id": 1, "sku": "sku-301", "percent": 80.0, "price": "28.00", "currency": "USD" },
  "headers": { "x-chio-receipt": "9272…1575" }
}
{
  "status": 200,
  "body": { "quotes": [{ "id": 1, "sku": "sku-301", "percent": 80.0, "price": "28.00", "currency": "USD" }] },
  "headers": {}
}

You should see a 201 with USD 28.00 and the receipt ID in the X-Chio-Receipt header, then the one recorded quote. python3 run.py revoke withdraws the grant, and the same request then returns 403 and records nothing.

FastAPI: admission before the route

The route you already have

fastapi_app.py · the routepython
@app.post("/v1/quote", status_code=201)
def quote(body: QuoteRequest):
    return store.issue(body.sku, body.percent)

The route owns validation and pricing. Pydantic rejects an unknown field or a percent outside 0 to 100; the store rejects an unknown SKU.

The complete FastAPI application

Read fastapi_app.py · complete 44 lines · complete file
fastapi_app.py · completepython
"""Issue quotes through FastAPI and the Chio ASGI middleware."""
import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, ConfigDict, Field, StrictStr
from chio_asgi import ChioASGIConfig, ChioASGIMiddleware
from pricing_store import QuoteStore


class QuoteRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    sku: StrictStr
    percent: float = Field(ge=0, le=100)


store = QuoteStore("fastapi")
app = FastAPI(title="Larkfield pricing", docs_url=None, redoc_url=None, openapi_url=None)
app.add_middleware(
    ChioASGIMiddleware,
    config=ChioASGIConfig(
        sidecar_url=os.environ.get("CHIO_SIDECAR_URL", "http://127.0.0.1:9098"),
        max_body_bytes=8192,
        exclude_paths=frozenset({"/healthz"}),
        exclude_methods=frozenset(),
    ),
)


@app.get("/healthz")
def health():
    return {"status": "ready", "framework": "fastapi"}


@app.get("/v1/quotes")
def list_quotes():
    return {"quotes": store.list()}


@app.post("/v1/quote", status_code=201)
def quote(body: QuoteRequest):
    # The middleware adds the admission receipt ID as the X-Chio-Receipt header.
    try:
        return store.issue(body.sku, body.percent)
    except ValueError as error:
        raise HTTPException(400, str(error)) from error

The ASGI adapter buffers up to 8 KiB, hashes the body, and replays it to FastAPI after admission. The app reads CHIO_SIDECAR_URL itself; the config object does not read the environment. /healthz is deliberately outside the check.

Read the shared price table and quote store 31 lines · complete file
pricing_store.py · completepython
"""Larkfield's price table and the quotes the service issues, shared by both frameworks."""
from decimal import ROUND_HALF_UP, Decimal
import sqlite3
from pathlib import Path

PRICES = {"sku-118": Decimal("24.00"), "sku-204": Decimal("58.00"), "sku-301": Decimal("140.00")}
CENT = Decimal("0.01")


class QuoteStore:
    def __init__(self, name):
        directory = Path(".pricing")
        directory.mkdir(mode=0o700, exist_ok=True)
        self.path = directory / f"{name}.db"
        with sqlite3.connect(self.path) as db:
            db.execute("CREATE TABLE IF NOT EXISTS quotes(id INTEGER PRIMARY KEY, sku TEXT NOT NULL, percent REAL NOT NULL, price TEXT NOT NULL)")

    def list(self):
        with sqlite3.connect(self.path) as db:
            rows = db.execute("SELECT id, sku, percent, price FROM quotes ORDER BY id LIMIT 100").fetchall()
        return [{"id": r[0], "sku": r[1], "percent": r[2], "price": r[3], "currency": "USD"} for r in rows]

    def issue(self, sku, percent):
        if sku not in PRICES:
            raise ValueError(f"unknown sku {sku}")
        if isinstance(percent, bool) or not isinstance(percent, (int, float)) or not 0 <= percent <= 100:
            raise ValueError("percent must be between 0 and 100")
        price = str((PRICES[sku] * (1 - Decimal(str(percent)) / 100)).quantize(CENT, ROUND_HALF_UP))
        with sqlite3.connect(self.path) as db:
            result = db.execute("INSERT INTO quotes(sku, percent, price) VALUES (?, ?, ?)", (sku, percent, price))
        return {"id": result.lastrowid, "sku": sku, "percent": percent, "price": price, "currency": "USD"}

Django: the same pricing contract

Stop the launcher and select Django; it keeps its quotes in .pricing/django.db.

Terminal 1bash
python3 run.py --framework django

Register the middleware

django_app.py · settingspython
settings.configure(
    DEBUG=False,
    SECRET_KEY="local-pricing-example-no-sessions",
    ALLOWED_HOSTS=["127.0.0.1", "localhost"],
    ROOT_URLCONF=__name__,
    MIDDLEWARE=["chio_django.ChioDjangoMiddleware"],
    CHIO_SIDECAR_URL=os.environ.get("CHIO_SIDECAR_URL", "http://127.0.0.1:9098"),
    CHIO_EXCLUDE_PATHS=["/healthz"],
    CHIO_EXCLUDE_METHODS=[],
    DATA_UPLOAD_MAX_MEMORY_SIZE=8192,
    USE_TZ=True,
)

The single-file app configures Django before django.setup(). In an existing settings module, these are ordinary assignments.

Keep the routes

django_app.py · URLspython
urlpatterns = [path("healthz", health), path("v1/quotes", quotes), path("v1/quote", quote)]

Issue the quote, then expose the receipt ID

django_app.py · in quotepython
issued = store.issue(payload["sku"], payload["percent"])
issued["receipt_id"] = request.chio_receipt["id"]
return JsonResponse(issued, status=201)
Output
{
  "id": 1,
  "sku": "sku-301",
  "percent": 80,
  "price": "28.00",
  "currency": "USD",
  "receipt_id": "9272…1575"
}

Django keeps request.body readable after the middleware hashes it. The receipt ID identifies admission; the returned quote and GET /v1/quotes show what the service did.

Read django_app.py · complete 58 lines · complete file
django_app.py · completepython
"""The same pricing contract as a complete Django application."""
import json
import os
from django.conf import settings

settings.configure(
    DEBUG=False,
    SECRET_KEY="local-pricing-example-no-sessions",
    ALLOWED_HOSTS=["127.0.0.1", "localhost"],
    ROOT_URLCONF=__name__,
    MIDDLEWARE=["chio_django.ChioDjangoMiddleware"],
    CHIO_SIDECAR_URL=os.environ.get("CHIO_SIDECAR_URL", "http://127.0.0.1:9098"),
    CHIO_EXCLUDE_PATHS=["/healthz"],
    CHIO_EXCLUDE_METHODS=[],
    DATA_UPLOAD_MAX_MEMORY_SIZE=8192,
    USE_TZ=True,
)

import django
django.setup()
from django.core.wsgi import get_wsgi_application
from django.http import JsonResponse
from django.urls import path
from pricing_store import QuoteStore

store = QuoteStore("django")


def health(request):
    return JsonResponse({"status": "ready", "framework": "django"})


def quotes(request):
    return JsonResponse({"quotes": store.list()})


def quote(request):
    if request.method != "POST":
        return JsonResponse({"error": "method_not_allowed"}, status=405)
    try:
        payload = json.loads(request.body)
        if not isinstance(payload, dict) or set(payload) != {"sku", "percent"}:
            raise ValueError("body must contain exactly sku and percent")
        issued = store.issue(payload["sku"], payload["percent"])
    except (ValueError, UnicodeError) as error:
        return JsonResponse({"error": str(error)}, status=400)
    issued["receipt_id"] = request.chio_receipt["id"]
    return JsonResponse(issued, status=201)


urlpatterns = [path("healthz", health), path("v1/quotes", quotes), path("v1/quote", quote)]
application = get_wsgi_application()

if __name__ == "__main__":
    from wsgiref.simple_server import make_server
    with make_server("127.0.0.1", 8092, application) as server:
        print("Django pricing: http://127.0.0.1:8092", flush=True)
        server.serve_forever()

Read the receipt in FastAPI and Django

IntegrationReceipt access
FastAPI with ChioASGIMiddleware
The X-Chio-Receipt response header. An optional on_receipt callback receives each verified allow receipt; the middleware does not set request.state.
Django middleware
request.chio_receipt, a dictionary. Allowed responses also carry X-Chio-Receipt.
FastAPI route decorator
The separate chio-fastapi decorator writes request state after admission and needs a Request argument. See the route-decorator section.

An allow receipt covers admission, not the view. Ask for a 140 percent discount with a valid grant:

Terminal 2 · pricing-pythonbash
curl -sS -w ' HTTP %{http_code}\n' http://127.0.0.1:8091/v1/quote \
  -H 'Content-Type: application/json' \
  -H "X-Chio-Capability: $(jq -c . .pricing/capability.json)" \
  -d '{"sku":"sku-118","percent":140}'
Output
{"detail":[{"type":"less_than_equal","loc":["body","percent"],"msg":"Input should be less than or equal to 100","input":140,"ctx":{"le":100.0}}]} HTTP 422

The kernel admitted the request, and FastAPI rejected it. The signed record says so: its status scope is the decision, and the middleware does not overwrite it with the view's 422.

The 140 percent request · receipt, selected fieldsjson
{
  "verdict": { "verdict": "allow" },
  "response_status": 200,
  "metadata": { "chio_http_status_scope": "decision" }
}

Read your own receipts

Terminal 2 · pricing-pythonbash
uv run python receipts.py --decision deny
Output
d214…a801  DENY       POST   /v1/quote  HTTP 403
3e15…158e  DENY       POST   /v1/quote  HTTP 403

receipts.py opens .pricing/receipts.db read-only and prints each decision's ID, verdict, method, route, and status; pass an ID in place of --decision to print that complete signed receipt. To check a signature, post the complete JSON to /chio/verify on the sidecar, as receipt verification shows.

Tell a refusal from an outage

FastAPI · POST /v1/quote without a grantjson
{
  "error": "CapabilityGuard",
  "message": "side-effect route requires a capability token",
  "status": 403
}
Observed resultMeaning and next action
403, side-effect route requires a capability token
Admission refused the quote before the view. Issue a grant, or check its expiry and scope.
FastAPI 422 or Django 400
The view rejected the request after admission. Fix the SKU or percent; an allow receipt does not mean a quote was recorded.
503, sidecar unavailable
The service could not get a decision. Restart Chio and check the sidecar URL. An outage is not a signed refusal.
/healthz 200 while /v1/quote returns 503
The excluded health route proves the service is up, not that Chio is.
FastAPI · POST /v1/quote while the sidecar is stoppedjson
{
  "error": "SidecarUnavailable",
  "message": "Chio sidecar unavailable",
  "status": 503
}

Both frameworks keep their recorded quotes through an outage and resume governed requests when the sidecar returns. The adapters fail closed.

Choose the integration

ConcernFastAPI (ASGI)Django (WSGI)
Registration
app.add_middleware with ChioASGIConfig
MIDDLEWARE and CHIO_* settings
Body handling
Bounded buffering and replay before FastAPI parses
Django caches request.body; the app sets its upload limit
Receipt access
Response header or allow-receipt callback
Request dictionary and response header
Validation
Pydantic model, then the store
View checks, then the store

Check the whole contract in one run

With the launcher stopped, python3 run.py check starts both processes, refuses a quote with no grant, issues one with Basalt's grant, rejects sku-999 after admission, revokes the grant, refuses again, and confirms that no refused request recorded a quote. It keeps the observations in .pricing/verification.json. Add --framework django to check Django.

Next: the same service in other languages