dm_mac.webhook module¶
Status-change webhook notifier for external consumers (e.g. the ESB).
- class dm_mac.webhook.WebhookNotifier(url: str)¶
Bases:
objectFire status-change webhooks to a configured URL.
Enabled only when
STATUS_WEBHOOK_URLis set (seefrom_env()). Deliveries are fire-and-forget:notify()spawns anasyncio.create_task()so callers – the MCU update handler and the Slack command handlers – never block on the HTTP request, exactly as the Slack notifications do. Each delivery retries with exponential backoff and a per-attempt timeout; once the retries are exhausted the failure is logged and dropped (external consumers can reconcile viaGET /api/machines).The webhook only fires on meaningful status changes (login, logout, unauthorized/unknown fob, override login, oops, un-oops, lockout, unlock, reboot) because
notify()is called only from those code paths, never from ordinary MCU heartbeat updates.- BACKOFF_BASE_SEC: float = 1.0¶
Base backoff in seconds; before attempt N (1-indexed) we wait
BACKOFF_BASE_SEC * 2 ** (N - 2)(no wait before the first attempt).
- MAX_ATTEMPTS: int = 4¶
Total number of POST attempts per event (initial try plus retries).
- REQUEST_TIMEOUT_SEC: float = 5.0¶
Per-attempt HTTP timeout in seconds.
- async _deliver(payload: Dict[str, Any]) None¶
Deliver one webhook with retries and exponential backoff.
Returns as soon as an attempt gets a non-error (< 400) response. On network errors or 4xx/5xx responses it retries up to
MAX_ATTEMPTStimes, backing off exponentially between attempts, then logs a single error and gives up. A singleaiohttp.ClientSessionis reused across attempts so retries can benefit from connection pooling.
- static _safe_url(url: str) str¶
Return
scheme://host[:port]ofurlfor safe logging.Strips any userinfo, path, and query string so credentials embedded in the URL (userinfo or query params) are never written to logs.
- _tasks: Set[Task[None]]¶
Strong references to in-flight delivery tasks.
asyncioonly holds a weak reference to tasks created viaasyncio.create_task(), so without this a long-running_deliver()(up to ~27 s of backoff + timeouts) could be garbage-collected mid-flight. Each task removes itself via a done-callback (the pattern from the asyncio docs).
- build_payload(machine: Machine, event: str, user: User | None = None) Dict[str, Any]¶
Build the webhook payload for
eventonmachine.The payload is
Machine.status_dict(socurrent_usermeans the same thing here as inGET /api/machines) plus three fields:event– the status-change event name.timestamp– epoch seconds when the event fired.user– the user involved in this event (the actor), as{"account_id", "full_name"}orNone. This differs fromcurrent_userfor events likelogout/unauthorizedwhere a user acted but is not (or no longer) logged in.
- classmethod from_env() WebhookNotifier | None¶
Build a notifier from
STATUS_WEBHOOK_URL, or None if unset.