Demo: resolve user names from database state, kanta instances at top with stacked decorators
This commit is contained in:
+33
-36
@@ -17,24 +17,12 @@ from types import ModuleType
|
|||||||
import msgspec
|
import msgspec
|
||||||
|
|
||||||
from kanta import Kanta
|
from kanta import Kanta
|
||||||
|
from kanta.callbacks import DictPost, DictPre
|
||||||
from kanta.logging import _ACTION, _ACTOR, _RESET, _SESSION, _TARGET
|
from kanta.logging import _ACTION, _ACTOR, _RESET, _SESSION, _TARGET
|
||||||
from kanta.logging import configure_logging
|
from kanta.logging import configure_logging
|
||||||
|
|
||||||
DB = Path(__file__).with_name("demo.kantadb")
|
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):
|
class DataV1(msgspec.Struct):
|
||||||
"""Original schema (version 0)."""
|
"""Original schema (version 0)."""
|
||||||
@@ -59,19 +47,41 @@ def migrate_v1(d: dict) -> None:
|
|||||||
migrations = ModuleType("demo_migrations")
|
migrations = ModuleType("demo_migrations")
|
||||||
migrations.migrate_v1 = migrate_v1
|
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:
|
_now = datetime(2026, 8, 6, tzinfo=UTC)
|
||||||
"""Resolve the transaction user id to a display name."""
|
|
||||||
return USERS.get(value)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_user_key(value: str, path: str) -> str | None:
|
@kanta_v0.clock
|
||||||
"""Resolve user ids in diff paths to display names."""
|
@kanta_v1.clock
|
||||||
if path.startswith("users."):
|
def fake_now() -> datetime:
|
||||||
return USERS.get(value)
|
"""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
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@kanta_v1.logheader
|
||||||
def header(action: str, user: str | None, extra: dict | None) -> str:
|
def header(action: str, user: str | None, extra: dict | None) -> str:
|
||||||
"""Aligned rich header: actor, session id, action, target."""
|
"""Aligned rich header: actor, session id, action, target."""
|
||||||
actor = f"{_ACTOR}{user or '-':<8}{_RESET}"
|
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}"
|
return f"{actor} {session} {_ACTION}{action}{_RESET} {target}"
|
||||||
|
|
||||||
|
|
||||||
|
@kanta_v0.bootstrap
|
||||||
def seed(data: DataV1) -> None:
|
def seed(data: DataV1) -> None:
|
||||||
"""Bootstrap: create the initial admin user."""
|
"""Create the initial admin user."""
|
||||||
data.users["u1"] = {"name": "Alice", "role": "admin"}
|
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:
|
def section(title: str) -> None:
|
||||||
print(f"\n# {title}", flush=True)
|
print(f"\n# {title}", flush=True)
|
||||||
|
|
||||||
@@ -157,7 +154,7 @@ async def main() -> None:
|
|||||||
|
|
||||||
with kanta_v1.transaction(
|
with kanta_v1.transaction(
|
||||||
action="update",
|
action="update",
|
||||||
user="u2",
|
user="u3",
|
||||||
extra={"session_id": 7, "target": "settings (demo)"},
|
extra={"session_id": 7, "target": "settings (demo)"},
|
||||||
) as data:
|
) as data:
|
||||||
data.settings["lang"] = "en"
|
data.settings["lang"] = "en"
|
||||||
|
|||||||
Reference in New Issue
Block a user