Module loggers used __name__, splitting diagnostics across eight module-named loggers and colliding kanta.transaction with the transaction event channel. Diagnostics (integrity errors, flush failures, rotation notes) are few; they now all go through the "kanta" logger, following the application's root logging configuration like any ordinary library output.
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""Transaction context manager for Kanta."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import contextmanager
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from kanta.diff import diff
|
|
from kanta.exceptions import DataIntegrityError
|
|
from kanta.callbacks import InjectionContext
|
|
from kanta.logging import _USER_PATH, LogEvent, emit_event, transaction_logger
|
|
from kanta.serialization import restore_data_in_place, struct_to_dict
|
|
|
|
_logger = logging.getLogger("kanta")
|
|
|
|
|
|
def _build_logfmt(impl, previous: dict, current: dict):
|
|
"""Build the logfmt chain for a state transition."""
|
|
return impl.callback_registry.build_logfmt(
|
|
InjectionContext(
|
|
previous_state=previous,
|
|
current_state=current,
|
|
kanta=impl._kanta,
|
|
)
|
|
)
|
|
|
|
|
|
def _resolve_user(logfmt, user: str | None) -> str | None:
|
|
"""Resolve *user* for display via the logfmt chain (raw as fallback)."""
|
|
if user is None:
|
|
return None
|
|
resolved = logfmt(user, _USER_PATH)
|
|
return resolved if resolved is not None else user
|
|
|
|
|
|
@contextmanager
|
|
def transaction(
|
|
impl,
|
|
action: str,
|
|
*,
|
|
user: str | None = None,
|
|
extra: Any = None,
|
|
mtime: bool | datetime = True,
|
|
log: bool | logging.Logger = True,
|
|
logdiff: bool = True,
|
|
):
|
|
"""Wrap writes in a transaction and yield the live db object."""
|
|
if impl.readonly:
|
|
raise DataIntegrityError(
|
|
"Cannot start transaction in read-only mode",
|
|
db_path=impl.filename,
|
|
action=action,
|
|
)
|
|
|
|
if impl.in_transaction:
|
|
raise RuntimeError(
|
|
"Nested or simultaneous transactions are not supported "
|
|
"(don't await inside transactions)."
|
|
)
|
|
|
|
current_dict = struct_to_dict(impl.data, serializer=impl.serializer)
|
|
|
|
if current_dict != impl.statedict:
|
|
is_bootstrap = action in {"bootstrap"}
|
|
if not (is_bootstrap and not impl.statedict):
|
|
delta = diff(impl.statedict, current_dict)
|
|
if delta:
|
|
_logger.critical(
|
|
"Database state modified outside of transaction! "
|
|
"This indicates a bug where changes occurred without a transaction wrapper.\n"
|
|
"Changes detected: %s",
|
|
delta,
|
|
)
|
|
raise DataIntegrityError(
|
|
"Database state modified outside of transaction",
|
|
db_path=impl.db_path,
|
|
action=action,
|
|
diff=delta,
|
|
)
|
|
|
|
impl.in_transaction = True
|
|
impl.transaction_snapshot = current_dict
|
|
|
|
try:
|
|
yield impl.data
|
|
new_dict = struct_to_dict(impl.data, serializer=impl.serializer)
|
|
delta = diff(impl.statedict, new_dict)
|
|
if delta:
|
|
if impl.callback_registry.has("validate"):
|
|
impl.callback_registry.invoke_sync(
|
|
"validate",
|
|
InjectionContext(data=impl.data, kanta=impl._kanta),
|
|
)
|
|
previous = impl.statedict
|
|
record = impl.queue_change(action, new_dict, user=user, mtime=mtime)
|
|
if record is not None:
|
|
if log is not False:
|
|
logfmt = _build_logfmt(impl, previous, new_dict)
|
|
logger = (
|
|
log if isinstance(log, logging.Logger) else transaction_logger
|
|
)
|
|
emit_event(
|
|
LogEvent(
|
|
kind="change",
|
|
logger=logger,
|
|
kanta=impl._kanta,
|
|
action=action,
|
|
user=_resolve_user(logfmt, user),
|
|
extra=extra,
|
|
diff=record.diff,
|
|
previous=previous,
|
|
current=new_dict,
|
|
logfmt=logfmt,
|
|
show_diff=logdiff,
|
|
),
|
|
impl.callback_registry.logemit_handlers,
|
|
)
|
|
except Exception as exc:
|
|
resolved_user = None
|
|
if user is not None:
|
|
logfmt = _build_logfmt(impl, impl.statedict, impl.statedict)
|
|
resolved_user = _resolve_user(logfmt, user)
|
|
emit_event(
|
|
LogEvent(
|
|
kind="aborted",
|
|
logger=transaction_logger,
|
|
level=logging.WARNING,
|
|
kanta=impl._kanta,
|
|
action=action,
|
|
user=resolved_user,
|
|
extra=extra,
|
|
error=exc,
|
|
),
|
|
impl.callback_registry.logemit_handlers,
|
|
)
|
|
if impl.transaction_snapshot is not None:
|
|
impl.data = restore_data_in_place(
|
|
impl.data,
|
|
impl.transaction_snapshot,
|
|
impl.data_type,
|
|
serializer=impl.serializer,
|
|
)
|
|
raise
|
|
finally:
|
|
impl.in_transaction = False
|
|
impl.transaction_snapshot = None
|