Files
kanta/demo/main.py
T
Leo Vasanko 54d01f1e74 Fortify logging: no logging failure may break functionality
emit_event now swallows and reports any failure, including crashes in the
built-in default_emit formatting itself; log_change routes through it.
logfmt chain callbacks that raise are logged and treated as fall-through,
and logmigr callbacks get on_error reporting like fatal_error handlers,
so a broken logging callback can no longer abort a transaction or open.

Demo: raw user ids in v0 logs, logfmt-resolved names in v1 logs.
2026-08-07 05:23:33 +00:00

123 lines
3.4 KiB
Python

#!/usr/bin/env -S uv run
import asyncio
import sys
from datetime import UTC, datetime, timedelta
from pathlib import Path
import msgspec
from kanta import Kanta
from kanta.callbacks import DictPost, DictPre
from kanta.logging import configure_logging
filename = Path(__file__).with_name("demo.kantadb")
# For demonstration purposes, we use "original v0" and "modified v1" in this same script
# Normally your app would only have the latest supported data model
class Data(msgspec.Struct): # type: ignore - intentionally redefined later
users: dict[str, dict] = {}
counter: int = 0
kanta_v0 = Kanta(filename, Data())
@kanta_v0.bootstrap
def bootstrap(data: Data) -> None:
"""Create the initial admin user."""
data.users["userid001"] = {"name": "Alice", "role": "admin"}
# Redefinition to simulate new version
class Data(msgspec.Struct):
users: dict[str, dict] = {}
total: int = 0 # Replaces old counter field
lang: str = "en" # New field
def migrate_v1(d: dict) -> None:
"""Rename counter to total"""
d["total"] = d["counter"]
kanta_v1 = Kanta(filename, Data(), migrations=sys.modules[__name__])
@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
async def main() -> None:
filename.unlink(missing_ok=True)
print("# Database creation with v0 schema and basic ops, pretty logs")
# Open and close automatically; you can also `await kanta.open()` instead
async with kanta_v0 as kanta:
with kanta.transaction(action="create", user="userid001") as data:
data.users["userid002"] = {"name": "Bob", "role": "user"}
with kanta.transaction(action="update", user="userid001") as data:
data.users["userid002"]["role"] = "editor"
data.counter = 1
# Display-only extra string, appended after the action.
with kanta.transaction(
action="export", user="userid002", extra="extra info"
) as data:
data.counter = 2
try:
with kanta.transaction(action="reset") as data:
data.counter = 99
raise ValueError("simulated failure")
except ValueError:
print(f"# Reading does not need transaction: {data.counter=}", flush=True)
print(
"\n# A later version of our application with new data model, migrations and logfmt"
)
async with kanta_v1 as kanta:
with kanta.transaction(
action="import", user="userid001", logdiff=False
) as data:
data.total = 3
with kanta.transaction(action="delete", user="userid002") as data:
del data.users["userid001"]
with kanta.transaction(
action="update", user="userid002", extra=filename.name
) as data:
data.total = 4
# Fake clock for deterministic timestamps
_now = datetime(2027, 1, 1, tzinfo=UTC)
@kanta_v0.clock
@kanta_v1.clock
def fake_clock() -> datetime:
global _now
_now += timedelta(hours=1)
return _now
if __name__ == "__main__":
configure_logging()
asyncio.run(main())