diff --git a/demo/main.py b/demo/main.py index a0b2135..5609918 100644 --- a/demo/main.py +++ b/demo/main.py @@ -18,11 +18,8 @@ import msgspec from kanta import Kanta from kanta.callbacks import DictPost, DictPre -from kanta.logging import _ACTION, _ACTOR, _RESET, _SESSION, _TARGET from kanta.logging import configure_logging -DB = Path(__file__).with_name("demo.kantadb") - class DataV1(msgspec.Struct): """Original schema (version 0).""" @@ -32,34 +29,31 @@ class DataV1(msgspec.Struct): class Data(msgspec.Struct): - """Current schema: migration v1 adds the settings section.""" + """Current schema: migration v1 renames counter to total.""" users: dict[str, dict] = {} - counter: int = 0 - settings: dict[str, str] = {} + total: int = 0 def migrate_v1(d: dict) -> None: - """Add settings section""" - d["settings"] = {"theme": "dark"} + """Rename counter to total""" + d["total"] = d.pop("counter") -# Phase 1 instance: default logging, original schema. -kanta_v0 = Kanta(DB, DataV1()) -# Phase 2 instance: migrations (scanned from this script) and a custom header. -kanta_v1 = Kanta(DB, Data(), migrations=sys.modules[__name__]) +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(2026, 8, 6, tzinfo=UTC) +_now = datetime(2027, 1, 1, tzinfo=UTC) @kanta_v0.clock @kanta_v1.clock -def fake_now() -> datetime: - """Deterministic clock: starts at midnight, +1h on every read.""" +def fake_clock() -> datetime: global _now - ts = _now _now += timedelta(hours=1) - return ts + return _now @kanta_v0.logfmt @@ -79,12 +73,9 @@ def resolve_user( @kanta_v1.logheader -def header(action: str, user: str | None, extra: dict | None) -> str: - """Aligned rich header: actor, session id, action, target.""" - actor = f"{_ACTOR}{user or '-':<8}{_RESET}" - session = f"{_SESSION}{extra.get('session_id', '-'):>2}{_RESET}" - target = f"{_TARGET}{extra['target']}{_RESET}" - return f"{actor} {session} {_ACTION}{action}{_RESET} {target}" +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 @@ -98,69 +89,53 @@ def section(title: str) -> None: async def main() -> None: - DB.unlink(missing_ok=True) + filename.unlink(missing_ok=True) - section("Standard logging: bootstrap, diffs, toggles, rollback") - await kanta_v0.open() + 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_v0.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_v0.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"] - with kanta_v0.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 - # Display-only extra string, appended after the action. - with kanta_v0.transaction(action="export", user="u1", extra=DB.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"} - # Compact logging, diff only: a system fix stamped by the clock, but the - # modification time (m) is not updated. - with kanta_v0.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 - # A failing transaction rolls back and logs a warning. - try: - with kanta_v0.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 - # Compact logging, header only. - with kanta_v0.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 - await kanta_v0.close() - - section("Reopen with migrations and a custom log header") - await kanta_v1.open() - - # No target given: defaults to the database filename. - with kanta_v1.transaction( - action="update", user="u1", extra={"session_id": 3} - ) as data: - data.settings["theme"] = "light" - - with kanta_v1.transaction( - action="update", - user="u3", - extra={"session_id": 7, "target": "settings (demo)"}, - ) as data: - data.settings["lang"] = "en" - - await kanta_v1.close() - - # 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) + with kanta.transaction(action="create", user="u3", extra="Dave (u4)") as data: + data.users["u4"] = {"name": "Dave", "role": "user"} if __name__ == "__main__": diff --git a/docs/database.md b/docs/database.md index e4d2aaa..a69c634 100644 --- a/docs/database.md +++ b/docs/database.md @@ -212,33 +212,28 @@ def resolve_user_key(value: str) -> str | None: - By default a transaction is logged with an `action by user` header followed by the diff lines. Added paths are colored green, deleted paths red. -- `kanta.transaction(..., extra=...)` accepts display-only metadata that is - used for logging and is never persisted in the `ChangeRecord`: - - a string is appended literally after the action in the default header, - - a dict is passed to a registered `@kanta.logheader` callback; if it has - no `"target"` key, the database filename is inserted as the target. -- Register a `@kanta.logheader` callback to replace the entire header line. - It may declare `action: str`, `user: str | None` and `extra: dict | None` - parameters, and can also have `DictPre`/`DictPost` state dicts and the - `Kanta` instance injected. It must be synchronous and return `str | None`. +- `kanta.transaction(..., extra="...")` accepts a display-only string that is + appended after the action in the default header (colored by Kanta); it is + never persisted in the `ChangeRecord`. +- Register a `@kanta.logheader` callback to compose a custom header. Declare + any of `action: str`, `user: str`, `extra: str`: Kanta passes the parts + with its header colors already applied (missing `user`/`extra` as empty + strings), so callbacks only arrange text — no color codes, fallbacks, or + padding. `DictPre`/`DictPost` state dicts and the `Kanta` instance can + also be injected. The callback must be synchronous and return `str | None`. - Multiple logheader callbacks are stacked in registration order; the first callback to return a non-`None` result wins. If all return `None`, Kanta - falls back to the default header. The `user` value passed to the callback - has already been through the `logfmt` formatters. + falls back to the default header. The `user` part has already been through + the `logfmt` formatters. - The header and diff parts can be toggled independently per transaction: `kanta.transaction(..., log={"header": True, "diff": False})`. ```python @kanta.logheader -def format_header(action: str, user: str | None, extra: dict | None) -> str: - session = extra.get("session_id", "-") - return f"{user:<20} {session:>2} {action} {extra['target']}" +def format_header(action: str, user: str, extra: str) -> str: + return f"{user} {action} {extra}" -with kanta.transaction( - action="update", - user="alice", - extra={"session_id": 3, "target": "Project Name (abcd1234)"}, -) as data: +with kanta.transaction(action="update", user="alice", extra="Project X") as data: ... ```