Files
kanta/demo/main.py
T
Leo Vasanko 6eb9087863 Add logemit event callbacks and kanta.tty terminal formatting
All change-related output (transactions, bootstrap, migrations) is now
described by a mutable LogEvent carrying full state plus the preferred
logger and level, and dispatched through emit_event. @kanta.logemit
callbacks receive the event and decide what is logged where: falsy return
marks it handled, truthy passes it (possibly modified) down the chain,
with default_emit - Kanta's own formatting, now just another emitter - as
the fallback. Pretty header and diff lines are lazy event properties.

New kanta.tty module: Line builder (call to append content, .colorname
arms a palette color for the next call with automatic folded reset,
width/align padding), a mutable Colors palette storing bare SGR params
(0 clears, sequential last-wins stacking), and strip_ansi/displaywidth/pad
helpers that count wide chars and emoji correctly.
2026-08-07 05:11:54 +00:00

120 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_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
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
with kanta.transaction(action="delete", user="userid002") as data:
del data.users["userid001"]
# 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)
with kanta.transaction(action="import", logdiff=False) as data:
data.counter = 3
print("\n# A later version of our application with new data model and migrations")
async with kanta_v1 as kanta:
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())