Per-transaction logdiff=False skips building and printing the diff body, logging only the header. Globally, configure_logging(diff=False) disables the kanta.transaction.diff child logger, which now carries all diff lines, so applications can route or silence diffs separately from headers.
123 lines
3.5 KiB
Python
123 lines
3.5 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", flush=True)
|
|
# 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",
|
|
flush=True,
|
|
)
|
|
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())
|