From 5b0d676175c081a2b0bd4df9a0eeeb1f1315057d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 6 Aug 2026 23:28:58 +0000 Subject: [PATCH] Add feature demo app under demo/ --- demo/.gitignore | 1 + demo/main.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 demo/.gitignore create mode 100644 demo/main.py diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..3f68360 --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1 @@ +demo.db diff --git a/demo/main.py b/demo/main.py new file mode 100644 index 0000000..05b0491 --- /dev/null +++ b/demo/main.py @@ -0,0 +1,152 @@ +"""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())