mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
feat(notifications): add Event dataclass for v0.3 pipeline
Adds meshai/notifications/events.py with: - Event dataclass with all fields for unified pipeline shape - Stable ID generation via sha1 hash for deduplication - make_event() factory with auto-timestamp and severity validation - to_dict/from_dict for serialization round-trip This is scaffolding for Phase 2 - not yet wired into any adapters. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5274933fa0
commit
dc52187c93
1 changed files with 186 additions and 0 deletions
186
meshai/notifications/events.py
Normal file
186
meshai/notifications/events.py
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
"""Event dataclass for the v0.3 notification pipeline.
|
||||||
|
|
||||||
|
This module defines the unified Event shape that flows through the
|
||||||
|
notification routing pipeline. All adapters emit Events, and the
|
||||||
|
router consumes them.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from meshai.notifications.events import Event, make_event
|
||||||
|
|
||||||
|
# Create an event
|
||||||
|
event = make_event(
|
||||||
|
source="nws",
|
||||||
|
category="tornado_warning",
|
||||||
|
severity="immediate",
|
||||||
|
title="Tornado Warning for Ada County",
|
||||||
|
summary="A tornado warning has been issued...",
|
||||||
|
lat=43.615,
|
||||||
|
lon=-116.2023,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Serialize for storage/webhook
|
||||||
|
data = event.to_dict()
|
||||||
|
|
||||||
|
# Restore from storage
|
||||||
|
event2 = Event.from_dict(data)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
|
||||||
|
# Valid severity levels
|
||||||
|
SEVERITY_LEVELS = frozenset({"routine", "priority", "immediate"})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Event:
|
||||||
|
"""Unified event shape for the notification pipeline.
|
||||||
|
|
||||||
|
All adapters (NWS, FIRMS, alert_engine, etc.) emit Events.
|
||||||
|
The router consumes Events and dispatches them to channels.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Identity
|
||||||
|
id: str = "" # stable hash for dedup, computed if not provided
|
||||||
|
source: str = "" # adapter name: "nws", "firms", "alert_engine", etc.
|
||||||
|
category: str = "" # specific event type within source
|
||||||
|
|
||||||
|
# Severity
|
||||||
|
severity: str = "routine" # "routine" | "priority" | "immediate"
|
||||||
|
|
||||||
|
# Geography
|
||||||
|
region: Optional[str] = None # primary region name, set by region tagger
|
||||||
|
regions: list[str] = field(default_factory=list) # all regions touched
|
||||||
|
lat: Optional[float] = None
|
||||||
|
lon: Optional[float] = None
|
||||||
|
nws_zones: list[str] = field(default_factory=list) # NWS zone codes
|
||||||
|
|
||||||
|
# Content
|
||||||
|
title: str = "" # one-line summary for digest headers
|
||||||
|
summary: str = "" # 1-3 sentence summary for immediate/mesh delivery
|
||||||
|
body: str = "" # full content for email/webhook delivery
|
||||||
|
|
||||||
|
# Affected entities (for mesh health events)
|
||||||
|
node_ids: list[str] = field(default_factory=list)
|
||||||
|
short_names: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
timestamp: float = 0.0 # event creation time
|
||||||
|
effective: Optional[float] = None # event start (NWS-style)
|
||||||
|
expires: Optional[float] = None # event end (NWS-style)
|
||||||
|
|
||||||
|
# Routing hints
|
||||||
|
group_key: Optional[str] = None # events with same key get merged
|
||||||
|
inhibit_keys: list[str] = field(default_factory=list) # suppression keys
|
||||||
|
|
||||||
|
# Raw adapter data (preserved for advanced rendering)
|
||||||
|
data: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compute_id(
|
||||||
|
source: str,
|
||||||
|
category: str,
|
||||||
|
group_key: Optional[str] = None,
|
||||||
|
lat: Optional[float] = None,
|
||||||
|
lon: Optional[float] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Compute a stable dedup ID for an event.
|
||||||
|
|
||||||
|
Two events with the same source+category+group_key+location
|
||||||
|
will have the same ID and can be deduplicated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Adapter name
|
||||||
|
category: Event category
|
||||||
|
group_key: Optional grouping key
|
||||||
|
lat: Optional latitude
|
||||||
|
lon: Optional longitude
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
16-character hex ID
|
||||||
|
"""
|
||||||
|
key_parts = [
|
||||||
|
source,
|
||||||
|
category,
|
||||||
|
group_key or "",
|
||||||
|
str(lat) if lat is not None else "",
|
||||||
|
str(lon) if lon is not None else "",
|
||||||
|
]
|
||||||
|
key_string = ":".join(key_parts)
|
||||||
|
return hashlib.sha1(key_string.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
"""Serialize event to a dict for JSON storage/webhook.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict representation of the event
|
||||||
|
"""
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: dict[str, Any]) -> "Event":
|
||||||
|
"""Restore an Event from a dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
d: Dict representation (from to_dict or JSON load)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Event instance
|
||||||
|
"""
|
||||||
|
return cls(**d)
|
||||||
|
|
||||||
|
|
||||||
|
def make_event(
|
||||||
|
source: str,
|
||||||
|
category: str,
|
||||||
|
severity: str,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Event:
|
||||||
|
"""Create an Event with automatic ID and timestamp.
|
||||||
|
|
||||||
|
This is the primary factory function for creating events.
|
||||||
|
It auto-computes the ID if not provided and sets timestamp
|
||||||
|
to the current time if not provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Adapter name (e.g., "nws", "firms", "alert_engine")
|
||||||
|
category: Event category (e.g., "tornado_warning", "infra_offline")
|
||||||
|
severity: One of "routine", "priority", "immediate"
|
||||||
|
**kwargs: Additional Event fields
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Event instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If severity is not valid
|
||||||
|
"""
|
||||||
|
# Validate severity
|
||||||
|
if severity not in SEVERITY_LEVELS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid severity '{severity}'. "
|
||||||
|
f"Must be one of: {', '.join(sorted(SEVERITY_LEVELS))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Auto-set timestamp if not provided
|
||||||
|
if "timestamp" not in kwargs or kwargs["timestamp"] == 0.0:
|
||||||
|
kwargs["timestamp"] = time.time()
|
||||||
|
|
||||||
|
# Auto-compute ID if not provided
|
||||||
|
if "id" not in kwargs or not kwargs["id"]:
|
||||||
|
kwargs["id"] = Event.compute_id(
|
||||||
|
source=source,
|
||||||
|
category=category,
|
||||||
|
group_key=kwargs.get("group_key"),
|
||||||
|
lat=kwargs.get("lat"),
|
||||||
|
lon=kwargs.get("lon"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
source=source,
|
||||||
|
category=category,
|
||||||
|
severity=severity,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue