From 72944c9410a07b625b24f7885cab891b6a9837cd Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 7 Aug 2026 00:06:54 +0000 Subject: [PATCH] Demo: resolve user names from database state, kanta instances at top with stacked decorators --- demo/main.py | 69 +++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/demo/main.py b/demo/main.py index e826eba..cab1588 100644 --- a/demo/main.py +++ b/demo/main.py @@ -17,24 +17,12 @@ from types import ModuleType import msgspec from kanta import Kanta +from kanta.callbacks import DictPost, DictPre from kanta.logging import _ACTION, _ACTOR, _RESET, _SESSION, _TARGET from kanta.logging import configure_logging DB = Path(__file__).with_name("demo.kantadb") -# Fake directory: user id -> display name, resolved by the logfmt callbacks. -USERS = {"u1": "Alice", "u2": "Bob", "u3": "Carol"} - -_now = datetime(2026, 8, 6, tzinfo=UTC) - - -def fake_now() -> datetime: - """Deterministic clock: starts at midnight, +1h on every read.""" - global _now - ts = _now - _now += timedelta(hours=1) - return ts - class DataV1(msgspec.Struct): """Original schema (version 0).""" @@ -59,19 +47,41 @@ def migrate_v1(d: dict) -> None: migrations = ModuleType("demo_migrations") migrations.migrate_v1 = migrate_v1 +# Phase 1 instance: default logging, original schema. +kanta_v0 = Kanta(DB, DataV1()) +# Phase 2 instance: migrations and a custom log header. +kanta_v1 = Kanta(DB, Data(), migrations=migrations) -def resolve_actor(value: str) -> str | None: - """Resolve the transaction user id to a display name.""" - return USERS.get(value) +_now = datetime(2026, 8, 6, tzinfo=UTC) -def resolve_user_key(value: str, path: str) -> str | None: - """Resolve user ids in diff paths to display names.""" - if path.startswith("users."): - return USERS.get(value) +@kanta_v0.clock +@kanta_v1.clock +def fake_now() -> datetime: + """Deterministic clock: starts at midnight, +1h on every read.""" + global _now + ts = _now + _now += timedelta(hours=1) + return ts + + +@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 | None, extra: dict | None) -> str: """Aligned rich header: actor, session id, action, target.""" actor = f"{_ACTOR}{user or '-':<8}{_RESET}" @@ -80,25 +90,12 @@ def header(action: str, user: str | None, extra: dict | None) -> str: return f"{actor} {session} {_ACTION}{action}{_RESET} {target}" +@kanta_v0.bootstrap def seed(data: DataV1) -> None: - """Bootstrap: create the initial admin user.""" + """Create the initial admin user.""" data.users["u1"] = {"name": "Alice", "role": "admin"} -# Phase 1 instance: default logging, original schema. -kanta_v0 = Kanta(DB, DataV1()) -# Phase 2 instance: migrations and a custom log header. -kanta_v1 = Kanta(DB, Data(), migrations=migrations) - -for k in (kanta_v0, kanta_v1): - k.clock(fake_now) - k.logfmt(resolve_user_key) - k.logfmt(resolve_actor, path="$user") - -kanta_v0.bootstrap(seed) -kanta_v1.logheader(header) - - def section(title: str) -> None: print(f"\n# {title}", flush=True) @@ -157,7 +154,7 @@ async def main() -> None: with kanta_v1.transaction( action="update", - user="u2", + user="u3", extra={"session_id": 7, "target": "settings (demo)"}, ) as data: data.settings["lang"] = "en"