All remaining events through logformat, cleaner migration message, prettier demo.
This commit is contained in:
+3
-5
@@ -57,7 +57,7 @@ def resolve_user(value: str, path: str, previous: DictPre) -> str | None:
|
||||
async def main() -> None:
|
||||
filename.unlink(missing_ok=True)
|
||||
|
||||
print("# Database creation with v0 schema and basic ops, pretty logs")
|
||||
print("Database creation with v0 schema and basic transactions:\n")
|
||||
# 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:
|
||||
@@ -73,9 +73,7 @@ async def main() -> None:
|
||||
) as data:
|
||||
data.counter = 2
|
||||
|
||||
print(
|
||||
"\n# A later version of our application with new data model, migrations and logfmt"
|
||||
)
|
||||
print("\nA new data model, migrations and logfmt pretty names:\n")
|
||||
async with kanta_v1 as kanta:
|
||||
with kanta.transaction(
|
||||
action="update", user="userid002", extra=filename.name
|
||||
@@ -87,7 +85,7 @@ async def main() -> None:
|
||||
data.total = 99
|
||||
raise ValueError("simulated failure")
|
||||
except ValueError:
|
||||
print(f"# Reading does not need transaction: {data.total=}", flush=True)
|
||||
print(f"\nReset rolled back: {data.total=} (we can always read data without tx)\n")
|
||||
|
||||
with kanta.transaction(action="delete", user="userid002") as data:
|
||||
del data.users["userid001"]
|
||||
|
||||
+7
-5
@@ -227,13 +227,15 @@ def resolve_user_key(value: str) -> str | None:
|
||||
#### Log Emitters
|
||||
|
||||
- Every change-related message Kanta emits (transaction/bootstrap/migration
|
||||
changes, `Created <file>`, migration summaries, aborted transactions) is
|
||||
described by a `kanta.logging.LogEvent` and dispatched through
|
||||
changes, file created/opened lines, migration summaries, aborted
|
||||
transactions) is described by a `kanta.logging.LogEvent` and dispatched
|
||||
through
|
||||
`kanta.logging.emit_event`. Kanta's own output goes through the same
|
||||
mechanism: when no `logemit` callback handles an event,
|
||||
`kanta.logging.default_emit` renders it with the built-in formatting.
|
||||
- A `LogEvent` carries the event `kind` (`"change"`, `"created"`,
|
||||
`"migrated"`, `"aborted"`), the preferred `logger` and `level`, the
|
||||
`"opened"`, `"migrated"`, `"aborted"`), the preferred `logger` and `level`,
|
||||
the
|
||||
`kanta` instance, and all relevant state: `action`, `user`, `extra`,
|
||||
`error` (for aborted transactions), `diff`, `previous`/`current` state
|
||||
dicts, the built `logfmt` chain, and version info for migration events.
|
||||
@@ -245,8 +247,8 @@ def resolve_user_key(value: str) -> str | None:
|
||||
- `event.header` — a lazy property producing the default one-line header
|
||||
for any kind: `<action>[ <extra>][ by <user>]` for changes,
|
||||
`<action>[ <extra>][ by <user>] transaction aborted: <error>` for aborts,
|
||||
and the
|
||||
plain `Created`/`Migrated` summaries. It is settable: assign
|
||||
and the `🛢️ <file> created|opened|migrated ...` summaries. It is
|
||||
settable: assign
|
||||
`event.header = ...` and return truthy to restyle the header while
|
||||
keeping the default diff routing.
|
||||
- `event.diff_lines` — a lazy property producing the pretty diff body for
|
||||
|
||||
+16
-6
@@ -242,9 +242,18 @@ class KantaImpl(PersistenceMixin, Generic[T]):
|
||||
)
|
||||
self.version = rr.version
|
||||
self.mtime = rr.m
|
||||
if log is not False:
|
||||
if log is not False and not migrations_ran:
|
||||
logger = log if isinstance(log, logging.Logger) else bootstrap_logger
|
||||
logger.debug("Using %s", self.filename.resolve())
|
||||
emit_event(
|
||||
LogEvent(
|
||||
kind="opened",
|
||||
logger=logger,
|
||||
level=logging.DEBUG,
|
||||
kanta=self._kanta,
|
||||
filename=str(self.filename.resolve()),
|
||||
),
|
||||
self.callback_registry.logemit_handlers,
|
||||
)
|
||||
normalized = struct_to_dict(self.data, serializer=self.serializer)
|
||||
if self.readonly:
|
||||
self.statedict = copy.deepcopy(normalized)
|
||||
@@ -257,6 +266,11 @@ class KantaImpl(PersistenceMixin, Generic[T]):
|
||||
f"migrate:v{self.version}" if migrations_ran else "migrate:msgspec"
|
||||
)
|
||||
record = self.queue_change(action, normalized, mtime=False)
|
||||
# The migration summary introduces the diff, so log it first.
|
||||
if migrations_ran and migration_result is not None:
|
||||
await self._handle_migration_log(
|
||||
migration_result, previous_version, log
|
||||
)
|
||||
if (
|
||||
record is not None
|
||||
and log is not False
|
||||
@@ -277,10 +291,6 @@ class KantaImpl(PersistenceMixin, Generic[T]):
|
||||
),
|
||||
self.callback_registry.logemit_handlers,
|
||||
)
|
||||
if migrations_ran and migration_result is not None:
|
||||
await self._handle_migration_log(
|
||||
migration_result, previous_version, log
|
||||
)
|
||||
if migrations_ran or record is not None:
|
||||
self.snapshot.request_force()
|
||||
await self.flush()
|
||||
|
||||
+11
-8
@@ -40,10 +40,11 @@ class LogEvent(msgspec.Struct, kw_only=True):
|
||||
"""All state describing one loggable event, passed to logemit callbacks.
|
||||
|
||||
``kind`` is ``"change"`` (transaction, bootstrap, or migration diff),
|
||||
``"created"`` (database file created), ``"migrated"`` (migration
|
||||
summary), or ``"aborted"`` (transaction rolled back). ``logger`` and
|
||||
``level`` are Kanta's preferred destination; a callback may use them,
|
||||
log elsewhere, or not log at all.
|
||||
``"created"`` (database file created), ``"opened"`` (database file
|
||||
opened), ``"migrated"`` (migration summary), or ``"aborted"``
|
||||
(transaction rolled back). ``logger`` and ``level`` are Kanta's
|
||||
preferred destination; a callback may use them, log elsewhere, or not
|
||||
log at all.
|
||||
|
||||
The event is mutable: a callback may modify it before returning a truthy
|
||||
value to pass it on, affecting later callbacks and the built-in fallback.
|
||||
@@ -75,8 +76,8 @@ class LogEvent(msgspec.Struct, kw_only=True):
|
||||
|
||||
Covers every event kind: ``"<action>[ <extra>][ by <user>]"`` for
|
||||
changes, ``"<action>[ <extra>][ by <user>] transaction aborted:
|
||||
<error>"`` for aborts, and the plain ``Created``/``Migrated``
|
||||
summaries.
|
||||
<error>"`` for aborts, and the ``🛢️ <filename> <verb>`` file
|
||||
summaries (created / opened / migrated).
|
||||
"""
|
||||
if self._header is None:
|
||||
self._header = self._build_header()
|
||||
@@ -93,11 +94,13 @@ class LogEvent(msgspec.Struct, kw_only=True):
|
||||
|
||||
def _build_header(self) -> str:
|
||||
if self.kind == "created":
|
||||
return f"Created {self.filename}"
|
||||
return f"🛢️ {self.filename} created"
|
||||
if self.kind == "opened":
|
||||
return f"🛢️ {self.filename} opened"
|
||||
if self.kind == "migrated":
|
||||
migrations = ", ".join(self.migrations)
|
||||
return (
|
||||
f"Migrated {self.filename} "
|
||||
f"🛢️ {self.filename} migrated "
|
||||
f"v{self.from_version} -> v{self.to_version}: {migrations}"
|
||||
)
|
||||
if self.kind == "change":
|
||||
|
||||
@@ -634,7 +634,7 @@ async def test_open_log_true_logs_bootstrap(tmp_path, format_config, caplog):
|
||||
|
||||
info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
|
||||
assert len(info_messages) >= 2
|
||||
assert "Created" in info_messages[0]
|
||||
assert "created" in info_messages[0]
|
||||
assert "bootstrap" in info_messages[1]
|
||||
|
||||
|
||||
@@ -665,7 +665,7 @@ async def test_open_log_custom_logger_logs_bootstrap(tmp_path, format_config, ca
|
||||
|
||||
info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
|
||||
assert len(info_messages) >= 2
|
||||
assert "Created" in info_messages[0]
|
||||
assert "created" in info_messages[0]
|
||||
assert "bootstrap" in info_messages[1]
|
||||
|
||||
|
||||
@@ -685,7 +685,7 @@ async def test_open_existing_database_logs_using_on_debug(
|
||||
await kanta2.close()
|
||||
|
||||
debug_messages = [r.message for r in caplog.records if r.levelno == logging.DEBUG]
|
||||
assert any("Using" in m and str(path.resolve()) in m for m in debug_messages)
|
||||
assert any("opened" in m and str(path.resolve()) in m for m in debug_messages)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -123,8 +123,8 @@ def test_default_emit_created_and_migrated(capsys):
|
||||
)
|
||||
)
|
||||
err = capsys.readouterr().err
|
||||
assert "Created x.kantadb" in err
|
||||
assert "Migrated x.kantadb v0 -> v1: migrate_v1 (rename)" in err
|
||||
assert "🛢️ x.kantadb created" in err
|
||||
assert "🛢️ x.kantadb migrated v0 -> v1: migrate_v1 (rename)" in err
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -295,7 +295,7 @@ async def test_aborted_transaction_includes_resolved_user(
|
||||
|
||||
def test_event_header_covers_all_kinds():
|
||||
created = LogEvent(kind="created", logger=transaction_logger, filename="x.db")
|
||||
assert created.header == "Created x.db"
|
||||
assert created.header == "🛢️ x.db created"
|
||||
|
||||
migrated = LogEvent(
|
||||
kind="migrated",
|
||||
@@ -305,7 +305,7 @@ def test_event_header_covers_all_kinds():
|
||||
to_version=1,
|
||||
migrations=["migrate_v1 (rename)"],
|
||||
)
|
||||
assert migrated.header == "Migrated x.db v0 -> v1: migrate_v1 (rename)"
|
||||
assert migrated.header == "🛢️ x.db migrated v0 -> v1: migrate_v1 (rename)"
|
||||
|
||||
aborted = LogEvent(
|
||||
kind="aborted",
|
||||
|
||||
Reference in New Issue
Block a user