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.
120 lines
3.4 KiB
Python
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())
|