Files
kanta/demo/main.py
T

145 lines
4.3 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
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
class DataV1(msgspec.Struct):
"""Original schema (version 0)."""
users: dict[str, dict] = {}
counter: int = 0
class Data(msgspec.Struct):
"""Current schema: migration v1 renames counter to total."""
users: dict[str, dict] = {}
total: int = 0
def migrate_v1(d: dict) -> None:
"""Rename counter to total"""
d["total"] = d.pop("counter")
filename = Path(__file__).with_name("demo.kantadb")
# For demonstration purposes, we use "original v0" and "modified v1" in this same script
kanta_v0 = Kanta(filename, DataV1())
kanta_v1 = Kanta(filename, Data(), migrations=sys.modules[__name__])
_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
@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
@kanta_v1.logheader
def header(action: str, user: str, extra: str) -> str:
"""Custom header: Kanta colors the parts, we just arrange them."""
return f"{user} {action} {extra}"
@kanta_v0.bootstrap
def seed(data: DataV1) -> None:
"""Create the initial admin user."""
data.users["u1"] = {"name": "Alice", "role": "admin"}
def section(title: str) -> None:
print(f"\n# {title}", flush=True)
async def main() -> None:
filename.unlink(missing_ok=True)
section("Database creation with v0 schema and basic access")
# Open and close automatically; you can also `await kanta.open()` instead
async with kanta_v0 as kanta:
with kanta.transaction(action="create", user="u2") as data:
data.users["u2"] = {"name": "Bob", "role": "user"}
with kanta.transaction(action="update", user="u1") as data:
data.users["u2"]["role"] = "editor"
data.counter = 1
with kanta.transaction(action="delete", user="u1") as data:
del data.users["u2"]
# Display-only extra string, appended after the action.
with kanta.transaction(action="export", user="u1", extra=filename.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.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.transaction(action="reset", user="u1") as data:
data.counter = 99
raise ValueError("simulated failure")
except ValueError:
pass
# Compact logging, header only.
with kanta.transaction(
action="import", user="u1", log={"header": True, "diff": False}
) as data:
data.counter = 3
section("A later version of our application with new data model and migrations")
async with kanta_v1 as kanta:
with kanta.transaction(action="update", user="u1", extra=filename.name) as data:
data.total = 4
with kanta.transaction(action="create", user="u3", extra="Dave (u4)") as data:
data.users["u4"] = {"name": "Dave", "role": "user"}
if __name__ == "__main__":
configure_logging()
logging.getLogger("kanta").setLevel(logging.DEBUG) # show migration diffs
asyncio.run(main())