Files
kanta/demo/main.py
T

176 lines
5.0 KiB
Python

"""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
from datetime import UTC, datetime, timedelta
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.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)."""
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 resolve_actor(value: str) -> str | None:
"""Resolve the transaction user id to a display name."""
return USERS.get(value)
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)
return None
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}"
session = f"{_SESSION}{extra.get('session_id', '-'):>2}{_RESET}"
target = f"{_TARGET}{extra['target']}{_RESET}"
return f"{actor} {session} {_ACTION}{action}{_RESET} {target}"
def seed(data: DataV1) -> None:
"""Bootstrap: 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)
async def main() -> None:
DB.unlink(missing_ok=True)
section("Standard logging: bootstrap, diffs, toggles, rollback")
await kanta_v0.open()
with kanta_v0.transaction(action="create", user="u2") as data:
data.users["u2"] = {"name": "Bob", "role": "user"}
with kanta_v0.transaction(action="update", user="u1") as data:
data.users["u2"]["role"] = "editor"
data.counter = 1
with kanta_v0.transaction(action="delete", user="u1") as data:
del data.users["u2"]
# Display-only extra string, appended after the action.
with kanta_v0.transaction(action="export", user="u1", extra=DB.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_v0.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_v0.transaction(action="reset", user="u1") as data:
data.counter = 99
raise ValueError("simulated failure")
except ValueError:
pass
# Compact logging, header only.
with kanta_v0.transaction(
action="import", user="u1", log={"header": True, "diff": False}
) as data:
data.counter = 3
await kanta_v0.close()
section("Reopen with migrations and a custom log header")
await kanta_v1.open()
# No target given: defaults to the database filename.
with kanta_v1.transaction(
action="update", user="u1", extra={"session_id": 3}
) as data:
data.settings["theme"] = "light"
with kanta_v1.transaction(
action="update",
user="u2",
extra={"session_id": 7, "target": "settings (demo)"},
) as data:
data.settings["lang"] = "en"
await kanta_v1.close()
# The pretty names only exist in the logs; the database stores raw ids.
section("Raw database records (user ids and timestamps, not pretty names)")
print(DB.read_text(), end="", flush=True)
if __name__ == "__main__":
configure_logging()
logging.getLogger("kanta").setLevel(logging.DEBUG) # show migration diffs
asyncio.run(main())