Skip to main content
Version: 2.x

:::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 as default_plan: "free".
  • pricing - the completion and execution operations and the standard rate card (per-1M-token rates, the $0.04/job gpt-4o rule, the fallback expression, the reject unmatched policy).
  • credits - the promotional (priority 1) and purchased (priority 10) buckets, default_bucket: "purchased".
  • entitlements and admission - the voice_mode / max_context features and the max_in_flight: 4 policy.
  • plans - free (10,000-credit monthly allowance) and pro (500k output_tokens/day block quota, admission policy).
  • commerce - the Stripe provider, the pro_monthly subscription and credits_10k top-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:

  1. An unknown top-level key - extra inputs are forbidden, so a typo like billing_hook fails instead of silently doing nothing.
  2. A float money value - money is always a base-10 decimal string ("0.0025"), never a JSON float, because floats lose precision.
  3. A missing credits section - the document requires credits; a pricing-only file is rejected.
  4. An expression referencing an undeclared measure - expression safety is validated at config time, so a formula typo can never reach production.
  5. A plan referencing an unknown operation - allowed_operations and 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 (what config set does).
  • 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 schema prints 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, None dropped) that is exactly what gets persisted, keeping revision diffs meaningful.
  • Versioning publishes immutable revisions; publish_and_activate, publish_draft, and activate give 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.