109 lines
3.2 KiB
Python
109 lines
3.2 KiB
Python
"""CloudEvents-compatible event envelope and durable outbox helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from typing import Callable, Protocol
|
|
|
|
from .store import TaskPlaneStore
|
|
|
|
|
|
@dataclass
|
|
class CloudEvent:
|
|
specversion: str = "1.0"
|
|
id: str = ""
|
|
type: str = ""
|
|
source: str = "spiffe://aios/task-plane"
|
|
subject: str = ""
|
|
datacontenttype: str = "application/json"
|
|
data: dict | None = None
|
|
|
|
@property
|
|
def time(self) -> str:
|
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
|
|
def asdict(self) -> dict:
|
|
return {
|
|
"specversion": self.specversion,
|
|
"id": self.id,
|
|
"type": self.type,
|
|
"source": self.source,
|
|
"subject": self.subject,
|
|
"time": self.time,
|
|
"datacontenttype": self.datacontenttype,
|
|
"data": self.data or {},
|
|
}
|
|
|
|
|
|
NOT_DELIVERED = "NOT_DELIVERED"
|
|
DELIVERED = "DELIVERED"
|
|
FAILED_DELIVERY = "FAILED"
|
|
|
|
|
|
class NotificationAdapter(Protocol):
|
|
name: str
|
|
|
|
def deliver(self, envelope: dict) -> str | None:
|
|
"""Return a provider receipt, or None when delivery is unconfirmed."""
|
|
|
|
|
|
class CallbackNotificationAdapter:
|
|
"""Adapter seam for webhook, NATS, GPT, dashboard, and WeChat transports."""
|
|
|
|
def __init__(self, name: str, callback: Callable[[dict], str | None]):
|
|
self.name = name
|
|
self._callback = callback
|
|
|
|
def deliver(self, envelope: dict) -> str | None:
|
|
return self._callback(envelope)
|
|
|
|
|
|
class WebhookAdapter(CallbackNotificationAdapter):
|
|
def __init__(self, callback: Callable[[dict], str | None]):
|
|
super().__init__("webhook", callback)
|
|
|
|
|
|
class NatsJetStreamAdapter(CallbackNotificationAdapter):
|
|
def __init__(self, callback: Callable[[dict], str | None]):
|
|
super().__init__("nats-jetstream", callback)
|
|
|
|
|
|
class GPTResultCallbackAdapter(CallbackNotificationAdapter):
|
|
def __init__(self, callback: Callable[[dict], str | None]):
|
|
super().__init__("gpt-result-callback", callback)
|
|
|
|
|
|
class DashboardAdapter(CallbackNotificationAdapter):
|
|
def __init__(self, callback: Callable[[dict], str | None]):
|
|
super().__init__("dashboard", callback)
|
|
|
|
|
|
class WeChatGatewayAdapter(CallbackNotificationAdapter):
|
|
def __init__(self, callback: Callable[[dict], str | None]):
|
|
super().__init__("wechat-gateway", callback)
|
|
|
|
|
|
class DurableOutboxDispatcher:
|
|
def __init__(self, store: TaskPlaneStore):
|
|
self.store = store
|
|
|
|
def dispatch(self, message_id: str, adapter: NotificationAdapter) -> dict:
|
|
row = self.store.get_outbox_message(message_id)
|
|
import json
|
|
|
|
envelope = json.loads(row["payload"])
|
|
try:
|
|
receipt = adapter.deliver(envelope)
|
|
except Exception:
|
|
return self.store.record_outbox_delivery(
|
|
message_id,
|
|
receipt=None,
|
|
error=f"{adapter.name} delivery failed",
|
|
)
|
|
return self.store.record_outbox_delivery(
|
|
message_id,
|
|
receipt=str(receipt) if receipt else None,
|
|
error=None if receipt else f"{adapter.name} receipt unavailable",
|
|
)
|