"""Kanta feature demo. Run from the project root: python demo/main.py Demonstrates bootstrap, colored transaction diffs, logfmt value formatting, custom log headers, logging toggles, rollback, migrations, and a custom clock. The database is recreated with fixed timestamps on every run; everything else lives in this file. """ import asyncio import logging import sys from datetime import UTC, datetime, timedelta from pathlib import Path import msgspec from kanta import Kanta from kanta.callbacks import DictPost, DictPre from kanta.logging import configure_logging class DataV1(msgspec.Struct): """Original schema (version 0).""" users: dict[str, dict] = {} counter: int = 0 class Data(msgspec.Struct): """Current schema: migration v1 renames counter to total.""" users: dict[str, dict] = {} total: int = 0 def migrate_v1(d: dict) -> None: """Rename counter to total""" d["total"] = d.pop("counter") filename = Path(__file__).with_name("demo.kantadb") # For demonstration purposes, we use "original v0" and "modified v1" in this same script kanta_v0 = Kanta(filename, DataV1()) kanta_v1 = Kanta(filename, Data(), migrations=sys.modules[__name__]) _now = datetime(2027, 1, 1, tzinfo=UTC) @kanta_v0.clock @kanta_v1.clock def fake_clock() -> datetime: global _now _now += timedelta(hours=1) return _now @kanta_v0.logfmt @kanta_v1.logfmt def resolve_user( value: str, path: str, previous: DictPre, current: DictPost ) -> str | None: """Resolve user ids to names from the database state itself.""" if path != "$user" and not path.startswith("users."): return None # Post-change state first, then pre-change (deleted users are only there). for state in (current, previous): name = state.get("users", {}).get(value, {}).get("name") if name: return name return None @kanta_v1.logheader def header(action: str, user: str, extra: str) -> str: """Custom header: Kanta colors the parts, we just arrange them.""" return f"{user} {action} {extra}" @kanta_v0.bootstrap def seed(data: DataV1) -> None: """Create the initial admin user.""" data.users["u1"] = {"name": "Alice", "role": "admin"} def section(title: str) -> None: print(f"\n# {title}", flush=True) async def main() -> None: filename.unlink(missing_ok=True) section("Database creation with v0 schema and basic access") # Open and close automatically; you can also `await kanta.open()` instead async with kanta_v0 as kanta: with kanta.transaction(action="create", user="u2") as data: data.users["u2"] = {"name": "Bob", "role": "user"} with kanta.transaction(action="update", user="u1") as data: data.users["u2"]["role"] = "editor" data.counter = 1 with kanta.transaction(action="delete", user="u1") as data: del data.users["u2"] # Display-only extra string, appended after the action. with kanta.transaction(action="export", user="u1", extra=filename.name) as data: data.counter = 2 # Compact logging, diff only: a system fix stamped by the clock, but the # modification time (m) is not updated. with kanta.transaction( action="repair", mtime=False, log={"header": False, "diff": True} ) as data: data.users["u3"] = {"name": "Carol", "role": "user"} # A failing transaction rolls back and logs a warning. try: with kanta.transaction(action="reset", user="u1") as data: data.counter = 99 raise ValueError("simulated failure") except ValueError: pass # Compact logging, header only. with kanta.transaction( action="import", user="u1", log={"header": True, "diff": False} ) as data: data.counter = 3 section("A later version of our application with new data model and migrations") async with kanta_v1 as kanta: with kanta.transaction(action="update", user="u1", extra=filename.name) as data: data.total = 4 with kanta.transaction(action="create", user="u3", extra="Dave (u4)") as data: data.users["u4"] = {"name": "Dave", "role": "user"} if __name__ == "__main__": configure_logging() logging.getLogger("kanta").setLevel(logging.DEBUG) # show migration diffs asyncio.run(main())