"""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, and migrations. The database is recreated on every run; everything else lives in this file. """ import asyncio import logging from pathlib import Path from types import ModuleType import msgspec from kanta import Kanta from kanta.logging import _ACTION, _ACTOR, _RESET, _SESSION, _TARGET from kanta.logging import configure_logging DB = Path(__file__).with_name("demo.db") # Fake directory: user id -> display name, resolved by the logfmt callbacks. USERS = {"u1": "Alice", "u2": "Bob", "u3": "Carol"} class DataV1(msgspec.Struct): """Original schema (version 0).""" users: dict[str, dict] = {} counter: int = 0 class Data(msgspec.Struct): """Current schema: migration v1 adds the settings section.""" users: dict[str, dict] = {} counter: int = 0 settings: dict[str, str] = {} def migrate_v1(d: dict) -> None: """Add settings section""" d["settings"] = {"theme": "dark"} migrations = ModuleType("demo_migrations") migrations.migrate_v1 = migrate_v1 def add_logfmts(kanta: Kanta) -> None: """Resolve user ids to display names in headers and diff paths.""" @kanta.logfmt(path="$user") def resolve_actor(value: str) -> str | None: return USERS.get(value) @kanta.logfmt def resolve_user_key(value: str, path: str) -> str | None: if path.startswith("users."): return USERS.get(value) return None def add_header(kanta: Kanta) -> None: """Aligned rich header: actor, session id, action, target.""" @kanta.logheader def header(action: str, user: str | None, extra: dict | None) -> str: actor = f"{_ACTOR}{user or '-':<8}{_RESET}" session = f"{_SESSION}{extra.get('session_id', '-'):>2}{_RESET}" target = f"{_TARGET}{extra['target']}{_RESET}" return f"{actor} {session} {_ACTION}{action}{_RESET} {target}" async def main() -> None: DB.unlink(missing_ok=True) print("=== Standard logging: bootstrap, diffs, toggles, rollback ===", flush=True) kanta = Kanta(DB, DataV1()) add_logfmts(kanta) @kanta.bootstrap def seed(data: DataV1) -> None: data.users["u1"] = {"name": "Alice", "role": "admin"} await kanta.open() 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=DB.name) as data: data.counter = 2 # Compact logging: diff only (no header) ... with kanta.transaction( action="repair", 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 # ... and header only (no diff). with kanta.transaction( action="import", user="u1", log={"header": True, "diff": False} ) as data: data.counter = 3 await kanta.close() print("=== Reopen with migrations and a custom log header ===", flush=True) kanta = Kanta(DB, Data(), migrations=migrations) add_logfmts(kanta) add_header(kanta) await kanta.open() # No target given: defaults to the database filename. with kanta.transaction(action="update", user="u1", extra={"session_id": 3}) as data: data.settings["theme"] = "light" with kanta.transaction( action="update", user="u2", extra={"session_id": 7, "target": "settings (demo)"}, ) as data: data.settings["lang"] = "en" await kanta.close() print(f"=== Database written to {DB} ===", flush=True) if __name__ == "__main__": configure_logging() logging.getLogger("kanta").setLevel(logging.DEBUG) # show migration diffs asyncio.run(main())