"""steward/core/audit.py Helpers for writing audit log entries. Each call opens its own DB session so audit events are committed independently of the calling route's transaction (audit is only written after the main action succeeds, by calling this after the main `async with db.begin()` block). """ from __future__ import annotations import json import logging from datetime import datetime, timezone logger = logging.getLogger(__name__) async def log_audit( app, user_id: str | None, username: str, action: str, entity_type: str | None = None, entity_id: str | None = None, detail: dict | None = None, ) -> None: """Write one audit event, and its timeline entry when the action has one (steward.timeline.catalog.audit_type). Never raises — failures are logged and swallowed.""" from steward.core.ids import time_ordered_id from steward.models.audit import AuditEvent from steward.timeline.record import from_audit try: async with app.db_sessionmaker() as db: async with db.begin(): event = AuditEvent( id=time_ordered_id(), user_id=user_id, username=username or "unknown", action=action, entity_type=entity_type, entity_id=entity_id, detail_json=json.dumps(detail) if detail else None, timestamp=datetime.now(timezone.utc), ) db.add(event) try: # In a savepoint: an entry that fails leaves the audit row standing. async with db.begin_nested(): await from_audit(db, event, detail) except Exception: logger.exception("Audit event %r: its timeline entry could not be written", action) except Exception: logger.exception("Failed to write audit event action=%r", action) async def audit_request(action: str, entity_type: str | None = None, entity_id: str | None = None, detail: dict | None = None) -> None: """log_audit for the logged-in user of the current web request.""" from quart import current_app, session await log_audit(current_app, session.get("user_id"), session.get("username", ""), action, entity_type=entity_type, entity_id=entity_id, detail=detail)