Demo: deterministic clock, # section banners, .kantadb extension, raw record dump
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
demo.db
|
||||
demo.kantadb
|
||||
|
||||
+50
-9
@@ -3,12 +3,14 @@
|
||||
Run from the project root: python demo/main.py
|
||||
|
||||
Demonstrates bootstrap, colored transaction diffs, logfmt value formatting,
|
||||
custom log headers, logging toggles, rollback, and migrations. The database
|
||||
is recreated on every run; everything else lives in this file.
|
||||
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
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
@@ -18,12 +20,25 @@ from kanta import Kanta
|
||||
from kanta.logging import _ACTION, _ACTOR, _RESET, _SESSION, _TARGET
|
||||
from kanta.logging import configure_logging
|
||||
|
||||
DB = Path(__file__).with_name("demo.db")
|
||||
DB = Path(__file__).with_name("demo.kantadb")
|
||||
|
||||
# Fake directory: user id -> display name, resolved by the logfmt callbacks.
|
||||
USERS = {"u1": "Alice", "u2": "Bob", "u3": "Carol"}
|
||||
|
||||
|
||||
class Clock:
|
||||
"""Deterministic clock: manually advanced, so every run is identical."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.current = datetime(2026, 8, 6, 12, 0, tzinfo=UTC)
|
||||
|
||||
def advance(self, **kwargs) -> None:
|
||||
self.current += timedelta(**kwargs)
|
||||
|
||||
|
||||
clock = Clock()
|
||||
|
||||
|
||||
class DataV1(msgspec.Struct):
|
||||
"""Original schema (version 0)."""
|
||||
|
||||
@@ -48,6 +63,14 @@ migrations = ModuleType("demo_migrations")
|
||||
migrations.migrate_v1 = migrate_v1
|
||||
|
||||
|
||||
def add_clock(kanta: Kanta) -> None:
|
||||
"""Use the shared deterministic clock for all record timestamps."""
|
||||
|
||||
@kanta.clock
|
||||
def fake_now() -> datetime:
|
||||
return clock.current
|
||||
|
||||
|
||||
def add_logfmts(kanta: Kanta) -> None:
|
||||
"""Resolve user ids to display names in headers and diff paths."""
|
||||
|
||||
@@ -73,12 +96,17 @@ def add_header(kanta: Kanta) -> None:
|
||||
return f"{actor} {session} {_ACTION}{action}{_RESET} {target}"
|
||||
|
||||
|
||||
def section(title: str) -> None:
|
||||
print(f"\n# {title}", flush=True)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
DB.unlink(missing_ok=True)
|
||||
|
||||
print("=== Standard logging: bootstrap, diffs, toggles, rollback ===", flush=True)
|
||||
section("Standard logging: bootstrap, diffs, toggles, rollback")
|
||||
|
||||
kanta = Kanta(DB, DataV1())
|
||||
add_clock(kanta)
|
||||
add_logfmts(kanta)
|
||||
|
||||
@kanta.bootstrap
|
||||
@@ -87,23 +115,29 @@ async def main() -> None:
|
||||
|
||||
await kanta.open()
|
||||
|
||||
clock.advance(minutes=2)
|
||||
with kanta.transaction(action="create", user="u2") as data:
|
||||
data.users["u2"] = {"name": "Bob", "role": "user"}
|
||||
|
||||
clock.advance(minutes=5)
|
||||
with kanta.transaction(action="update", user="u1") as data:
|
||||
data.users["u2"]["role"] = "editor"
|
||||
data.counter = 1
|
||||
|
||||
clock.advance(seconds=30)
|
||||
with kanta.transaction(action="delete", user="u1") as data:
|
||||
del data.users["u2"]
|
||||
|
||||
# Display-only extra string, appended after the action.
|
||||
clock.advance(hours=1)
|
||||
with kanta.transaction(action="export", user="u1", extra=DB.name) as data:
|
||||
data.counter = 2
|
||||
|
||||
# Compact logging: diff only (no header) ...
|
||||
# Compact logging, diff only: a system fix stamped by the clock, but the
|
||||
# modification time (m) is not updated.
|
||||
clock.advance(minutes=10)
|
||||
with kanta.transaction(
|
||||
action="repair", log={"header": False, "diff": True}
|
||||
action="repair", mtime=False, log={"header": False, "diff": True}
|
||||
) as data:
|
||||
data.users["u3"] = {"name": "Carol", "role": "user"}
|
||||
|
||||
@@ -115,7 +149,8 @@ async def main() -> None:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# ... and header only (no diff).
|
||||
# Compact logging, header only.
|
||||
clock.advance(minutes=5)
|
||||
with kanta.transaction(
|
||||
action="import", user="u1", log={"header": True, "diff": False}
|
||||
) as data:
|
||||
@@ -123,17 +158,21 @@ async def main() -> None:
|
||||
|
||||
await kanta.close()
|
||||
|
||||
print("=== Reopen with migrations and a custom log header ===", flush=True)
|
||||
section("Reopen with migrations and a custom log header")
|
||||
|
||||
clock.advance(days=1)
|
||||
kanta = Kanta(DB, Data(), migrations=migrations)
|
||||
add_clock(kanta)
|
||||
add_logfmts(kanta)
|
||||
add_header(kanta)
|
||||
await kanta.open()
|
||||
|
||||
# No target given: defaults to the database filename.
|
||||
clock.advance(minutes=3)
|
||||
with kanta.transaction(action="update", user="u1", extra={"session_id": 3}) as data:
|
||||
data.settings["theme"] = "light"
|
||||
|
||||
clock.advance(minutes=1)
|
||||
with kanta.transaction(
|
||||
action="update",
|
||||
user="u2",
|
||||
@@ -143,7 +182,9 @@ async def main() -> None:
|
||||
|
||||
await kanta.close()
|
||||
|
||||
print(f"=== Database written to {DB} ===", flush=True)
|
||||
# The pretty names only exist in the logs; the database stores raw ids.
|
||||
section("Raw database records (user ids and timestamps, not pretty names)")
|
||||
print(DB.read_text(), end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -163,6 +163,15 @@ when they have a default value.
|
||||
- Multiple handlers are supported and invoked in registration order. A failing
|
||||
handler is logged and does not prevent subsequent handlers from running.
|
||||
|
||||
#### Clock
|
||||
|
||||
- `@kanta.clock` registers a callback `() -> datetime` that replaces the
|
||||
default UTC clock. Its value is used for all record timestamps (`ts`, and
|
||||
`m` when `mtime` is `True`) and for snapshot timestamps.
|
||||
- Register before `open()` so that bootstrap and migration records use the
|
||||
custom clock as well. This is mainly useful for tests and reproducible
|
||||
demos.
|
||||
|
||||
#### Transaction Log Formatting
|
||||
|
||||
- Logfmt callbacks prettify identifiers in the change log and are registered with
|
||||
|
||||
Reference in New Issue
Block a user