Python HTTP Frameworks
Run Larkfield's pricing service in FastAPI and Django, with every quote admitted by the kernel before the view runs.
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.
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.
chio new pricing-python --example pricing-python && cd pricing-python
python3 run.pyPricing: 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.
python3 run.py quote sku-301 80
python3 run.py list{
"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
@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
"""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 errorThe 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
"""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.
python3 run.py --framework djangoRegister the middleware
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
urlpatterns = [path("healthz", health), path("v1/quotes", quotes), path("v1/quote", quote)]Issue the quote, then expose the receipt ID
issued = store.issue(payload["sku"], payload["percent"])
issued["receipt_id"] = request.chio_receipt["id"]
return JsonResponse(issued, status=201){
"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
"""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
| Integration | Receipt access |
|---|---|
FastAPI with Chio | 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., 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:
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}'{"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 422The 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.
{
"verdict": { "verdict": "allow" },
"response_status": 200,
"metadata": { "chio_http_status_scope": "decision" }
}Read your own receipts
uv run python receipts.py --decision denyd214…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
{
"error": "CapabilityGuard",
"message": "side-effect route requires a capability token",
"status": 403
}| Observed result | Meaning 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. |
{
"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
| Concern | FastAPI (ASGI) | Django (WSGI) |
|---|---|---|
Registration | app. 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.