:::info Executable tutorial
This page is generated from a tested Jupyter notebook. Open it in Google Colab or view the source notebook.
:::
Validate the complete configuration schema
The Bursar configuration connects pricing, credit buckets, plans, quotas, admission policies, offers, and auto-recharge in one validated document. This tutorial inspects the complete shape, canonicalizes it, publishes a revision, reads the public projection, and locates the generated JSON Schema.
Learning objectives
After completing this tutorial, you can:
- Validate every top-level configuration section
- Explain canonicalization and immutable catalog revisions
- Distinguish the internal document from its public projection
- Use the generated JSON Schema in editors and continuous integration
Prerequisites
- Complete the pricing configuration and CLI tutorials
- Start the notebook server from
samples/python/notebooks/
The document at a glance
The shared base_config() drives every tutorial in this collection:
version- the schema version of the document itself.catalog- defaults such asdefault_plan: "free".pricing- thecompletionandexecutionoperations and thestandardrate card (per-1M-token rates, the $0.04/jobgpt-4orule, the fallback expression, therejectunmatched policy).credits- thepromotional(priority 1) andpurchased(priority 10) buckets,default_bucket: "purchased".entitlementsandadmission- thevoice_mode/max_contextfeatures and themax_in_flight: 4policy.plans-free(10,000-credit monthly allowance) andpro(500koutput_tokens/day block quota, admission policy).commerce- the Stripe provider, thepro_monthlysubscription andcredits_10ktop-up offers, and auto-recharge guardrails.
load_config_from_dict parses and validates the whole thing in one call.
from shared import base_config
from bursar.config import load_config_from_dict
config = load_config_from_dict(base_config())
print("top-level sections:", sorted(config.model_dump().keys()))
print("plans:", list(config.plans.keys()))
print("offers:", list(config.commerce.offers.keys()))
print("credit buckets:", {k: b.priority for k, b in config.credits.buckets.items()})
print("rate cards:", list(config.pricing.rate_cards.keys()))
Validation - what the document refuses
Every invalid document is rejected with a ConfigError (from bursar.config) that carries a structured errors() payload. Five representative failures, each a mistake a real operator would make:
- An unknown top-level key - extra inputs are forbidden, so a typo like
billing_hookfails instead of silently doing nothing. - A float money value - money is always a base-10 decimal string (
"0.0025"), never a JSON float, because floats lose precision. - A missing
creditssection - the document requires credits; a pricing-only file is rejected. - An expression referencing an undeclared measure - expression safety is validated at config time, so a formula typo can never reach production.
- A plan referencing an unknown operation -
allowed_operationsand rate-card resolution are checked across the whole document.
Each case below prints the head of the ConfigError message plus the first structured error.
from bursar.config import ConfigError
def reject(label, mutate):
doc = base_config()
mutate(doc)
try:
load_config_from_dict(doc)
print(f"{label}: ACCEPTED (unexpected)")
except ConfigError as exc:
head = str(exc).splitlines()[:3]
print(f"{label}:")
for line in head:
print(" ", line)
err = exc.errors()[0]
print(" loc:", err["loc"], "| type:", err["type"])
reject("unknown top-level key", lambda d: d.update({"billing_hook": True}))
reject(
"float money value",
lambda d: d["pricing"]["rate_cards"]["standard"]["operations"]["completion"]["rules"][0]
["charge"]["components"][0].update({"rate": 0.0025}),
)
reject("missing credits section", lambda d: d.pop("credits"))
reject(
"undeclared measure in formula",
lambda d: d["pricing"]["rate_cards"]["standard"]["operations"]["completion"]["unmatched"]
["charge"].update({"formula": "bogus_measure * 0.005 + output_tokens * 0.015"}),
)
reject(
"unknown operation in plan",
lambda d: d["plans"]["free"]["allowed_operations"].append("streaming"),
)
Canonicalization - one shape to publish
canonical_bursar_config_dict validates the document and returns the canonical JSON dict: defaults filled in, None values dropped, money emitted as base-10 decimal strings. This is exactly the shape that gets persisted - the catalog publishes canonical output, never the raw input - so revision diffs are stable and meaningful.
Inside the typed BursarConfig model, amounts are Decimal; in the canonical dict they are strings.
from bursar.config import canonical_bursar_config_dict
canonical = canonical_bursar_config_dict(base_config())
print("canonical sections:", sorted(canonical.keys()))
allowance = canonical["plans"]["free"]["credit_allowance"]["amount"]
quota = canonical["plans"]["pro"]["quotas"]["daily_tokens"]["limit"]
grant = canonical["commerce"]["offers"]["pro_monthly"]["cycle_grant"]["amount"]
print("free allowance amount:", repr(allowance), type(allowance).__name__)
print("pro quota limit: ", repr(quota), type(quota).__name__)
print("cycle grant amount: ", repr(grant), type(grant).__name__)
roundtrip = canonical_bursar_config_dict(canonical)
print("canonical is idempotent:", roundtrip == canonical)
Versioning - publish, draft, activate
The store persists every publish as an immutable revision with a monotonically increasing version number and a creation timestamp; exactly one revision is active at any time. The Python API mirrors the CLI:
bursar.catalog.publish_and_activate(config, label)- validate, publish, and activate in one call (whatconfig setdoes).bursar.catalog.publish_draft(config, label)- publish an inactive draft; the live catalog is untouched.bursar.catalog.activate(version)- make a published revision active; the rollback path.
Below we publish v1 and v2, then stage a draft (which becomes v3, but is not active) and activate it only after inspecting the history.
from shared import start_postgres_store, cleanup
from bursar import Bursar
store, pgdata = start_postgres_store()
bursar = Bursar.create(credit_store=store)
bursar.catalog.publish_and_activate(base_config(), label="initial")
print("v1 active:", bursar.catalog.get_active().version)
v2 = base_config()
v2["plans"]["pro"]["quotas"]["daily_tokens"]["limit"] = "600000"
bursar.catalog.publish_and_activate(v2, label="deploy-42: pro quota 500k -> 600k")
print("v2 active:", bursar.catalog.get_active().version)
v3 = base_config()
v3["pricing"]["rate_cards"]["standard"]["operations"]["completion"]["unmatched"]["charge"][
"formula"
] = "input_tokens * 0.006 + output_tokens * 0.016"
draft_id = bursar.catalog.publish_draft(v3, label="staged: fallback rate bump")
print("draft id:", draft_id)
print("active unchanged by draft:", bursar.catalog.get_active().version)
history = store.get_pricing_history()
for item in history:
print(f" {'*' if item.active else ' '} v{item.version} ({item.label}) active={item.active}")
draft_version = next(item.version for item in history if item.id == draft_id)
bursar.catalog.activate(draft_version)
print("activated:", bursar.catalog.get_active().version, "| id matches draft:", bursar.catalog.get_active().id == draft_id)
The public view - what clients may see
Clients never receive the full document: it contains provider product identifiers (Stripe price_ids) that belong in your backend, not your storefront. bursar.catalog.public_view() projects a provider-secret-free catalog - plans sorted by rank, their offers, and top-ups, amounts as strings - ready to render on a pricing page or power a purchase flow.
The projection runs against the same live revision the versioning cell left active (v3, the staged fallback-rate bump).
try:
view = bursar.catalog.public_view()
print("view sections:", list(view.keys()))
for plan in view["plans"]:
offers = ", ".join(o["key"] for o in plan["offers"])
print(f"plan {plan['key']!r}: rank={plan['rank']} display_name={plan['display_name']!r} offers=[{offers}]")
topup = view["topups"][0]
print("topup:", topup["key"], "| credits_per_unit:", topup["credits_per_unit"], "| quantity:", topup["quantity"])
print("offer fields:", sorted(view["plans"][1]["offers"][0].keys()))
print("provider secrets present:", "providers" in view["plans"][1]["offers"][0])
finally:
cleanup(pgdata)
The JSON Schema - at every layer
The same schema that validates documents in Python is available everywhere:
- Editor support.
bursar config schemaprints the JSON Schema (a ~64 kB document) for autocompletion and validation in your editor or a lint script:bursar config schema > pricing.schema.json - The repository copy. The identical schema ships in the repo as
docs/pricing-config.schema.json- the reference artifact for code review. - Publish-time enforcement in Postgres. The migrations install
bursar.require_catalog_document_shape, which validates every document against the embedded schema inside the database before a revision is written. Even a caller that bypasses the SDK (raw SQL, a rogue script) cannot publish a malformed catalog. - CI gates.
bursar config validate --json(notebook 13) gives pipelines machine-readable errors without a database.
One schema, four layers: the typed BursarConfig model in Python, the JSON Schema artifact, the in-database validator, and the CLI gate.
Summary
The configuration document is the whole product surface:
- Validation rejects unknown keys, non-decimal money, missing sections, unsafe expressions, and dangling references - with structured
ConfigError.errors(). - Canonicalization produces one stable JSON shape (money as strings, defaults filled,
Nonedropped) that is exactly what gets persisted, keeping revision diffs meaningful. - Versioning publishes immutable revisions;
publish_and_activate,publish_draft, andactivategive you staged rollouts and one-command rollbacks. - The public view projects plans and offers without provider secrets.
- The JSON Schema backs editors, the CLI, and the database itself.
From load_config_from_dict to public_view, it is one document - validated once, versioned forever.