Coverage for src / kemi / operations / _ops_webhooks.py: 26%
39 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-05 15:47 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-05 15:47 +0000
1"""Webhook operations: configure_webhooks, _dispatch_webhook_event.
3These free functions are called by the corresponding ``Memory`` methods.
4"""
6from __future__ import annotations
8import asyncio
9import logging
10from typing import TYPE_CHECKING, Any
12from kemi.webhooks import WebhookDispatcher, WebhookEventType, WebhookStore, build_payload
14if TYPE_CHECKING:
15 from kemi._memory_impl import Memory
17logger = logging.getLogger(__name__)
20def configure(memory: "Memory", db_path: str | None) -> None:
21 """Enable webhook dispatch for memory lifecycle events."""
22 if db_path is None:
23 try:
24 db_path = memory._store._db_path # type: ignore[attr-defined]
25 except AttributeError:
26 logger.warning("Cannot determine database path for webhook store")
27 return
29 try:
30 store = WebhookStore(db_path=db_path)
31 memory._webhook_dispatcher = WebhookDispatcher(store=store)
32 logger.info("Webhook dispatcher initialized (db: %s)", db_path)
33 except (OSError, ValueError) as e:
34 logger.warning("Failed to initialise webhook dispatcher: %s", e)
37def dispatch(
38 memory: "Memory",
39 event: WebhookEventType,
40 memory_id: str,
41 user_id: str,
42 snapshot: dict[str, Any] | None = None,
43 previous_state: dict[str, Any] | None = None,
44 **extra: Any,
45) -> None:
46 """Dispatch a webhook event if a dispatcher is configured.
48 Prefers async dispatch when an event loop is running; falls back to
49 synchronous dispatch otherwise (e.g. CLI commands).
50 """
51 if memory._webhook_dispatcher is None:
52 return
53 try:
54 payload = build_payload(
55 event=event,
56 memory_id=memory_id,
57 user_id=user_id,
58 snapshot=snapshot,
59 previous_state=previous_state,
60 extra=extra or None,
61 )
62 except (ValueError, TypeError) as e:
63 logger.warning("Webhook payload build failed for %s: %s", event.value, e)
64 return
66 try:
67 loop = asyncio.get_running_loop()
68 except RuntimeError:
69 # No event loop — fall back to sync dispatch.
70 try:
71 memory._webhook_dispatcher.dispatch_sync(payload, event)
72 except Exception:
73 # Broad catch: webhook transports vary (HTTP, CLI subprocess, etc.)
74 # Log and continue; webhooks must never break the calling operation.
75 logger.warning(
76 "Sync webhook dispatch failed for %s", event.value, exc_info=True
77 )
78 return
80 # Running event loop — fire-and-forget.
81 try:
82 asyncio.ensure_future(
83 memory._webhook_dispatcher.dispatch_async(payload, event)
84 )
85 except Exception:
86 # Broad catch: ensure_future can raise if loop is closing, etc.
87 logger.warning(
88 "Async webhook dispatch failed for %s", event.value, exc_info=True
89 )