from __future__ import annotations
import hashlib
import hmac
import json
from typing import List, Optional
from briefcase.events.types import BriefcaseEvent
try:
import httpx as httpx
except ImportError:
httpx = None
class WebhookEmitter:
def __init__(
self,
url: str,
secret: str = "",
subscribed_events: Optional[List[str]] = None,
timeout: float = 10.0,
) -> None:
self._url = url
self._secret = secret
self._subscribed_events = subscribed_events or []
self._timeout = timeout
def _is_subscribed(self, event: BriefcaseEvent) -> bool:
if not self._subscribed_events:
return True
return event.event_type in self._subscribed_events
@staticmethod
def _sign(body: bytes, secret: str) -> str:
return hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
def _build_headers(self, event: BriefcaseEvent, body: bytes) -> dict:
return {
"Content-Type": "application/json",
"X-Briefcase-Signature": self._sign(body, self._secret),
"ce-id": event.idempotency_key,
"ce-type": event.event_type,
"ce-source": "briefcase-ai",
"ce-specversion": "1.0",
"ce-time": event.timestamp.isoformat(),
}
async def emit(self, event: BriefcaseEvent) -> bool:
if httpx is None:
raise ImportError(
"httpx is required for WebhookEmitter. "
"Install it with: pip install 'briefcase-ai[splunk]'"
)
if not self._is_subscribed(event):
return False
body = json.dumps(
{
"event_type": event.event_type,
"decision_id": event.decision_id,
"timestamp": event.timestamp.isoformat(),
"idempotency_key": event.idempotency_key,
"payload": event.payload,
},
default=str,
).encode()
headers = self._build_headers(event, body)
max_attempts = 3
async with httpx.AsyncClient(timeout=self._timeout) as client:
for attempt in range(max_attempts):
try:
response = await client.post(self._url, content=body, headers=headers)
if response.status_code < 500:
return response.status_code < 400
if attempt >= max_attempts - 1:
return False
except httpx.TimeoutException:
return False
except Exception:
return False
return False