145 lines
4.3 KiB
Python
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())
|