truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Routing engine for notification dispatch.
|
|
2
|
+
|
|
3
|
+
This module provides the core routing engine that evaluates events
|
|
4
|
+
against configured routes and determines target channels.
|
|
5
|
+
|
|
6
|
+
Components:
|
|
7
|
+
- RouteContext: Holds event data and metadata for rule evaluation
|
|
8
|
+
- Route: Defines a route with rule, actions, and priority
|
|
9
|
+
- RoutingResult: Result of route matching
|
|
10
|
+
- ActionRouter: Main routing engine
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
# Create routes
|
|
14
|
+
routes = [
|
|
15
|
+
Route(
|
|
16
|
+
name="critical_alerts",
|
|
17
|
+
rule=SeverityRule(min_severity="critical"),
|
|
18
|
+
actions=["pagerduty-channel"],
|
|
19
|
+
priority=100,
|
|
20
|
+
),
|
|
21
|
+
Route(
|
|
22
|
+
name="default",
|
|
23
|
+
rule=AlwaysRule(),
|
|
24
|
+
actions=["slack-channel"],
|
|
25
|
+
priority=0,
|
|
26
|
+
),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Create router
|
|
30
|
+
router = ActionRouter(routes=routes)
|
|
31
|
+
|
|
32
|
+
# Match event
|
|
33
|
+
context = RouteContext(event=validation_event)
|
|
34
|
+
result = await router.match(context)
|
|
35
|
+
|
|
36
|
+
for route in result.matched_routes:
|
|
37
|
+
print(f"Matched: {route.name} -> {route.actions}")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from datetime import datetime
|
|
44
|
+
from typing import TYPE_CHECKING, Any
|
|
45
|
+
|
|
46
|
+
from .rules import BaseRule
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from truthound_dashboard.core.notifications.base import NotificationEvent
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class RouteContext:
|
|
54
|
+
"""Context for route evaluation.
|
|
55
|
+
|
|
56
|
+
Holds the event data and additional metadata that rules
|
|
57
|
+
can use to make matching decisions.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
event: The notification event being routed.
|
|
61
|
+
metadata: Additional context metadata.
|
|
62
|
+
timestamp: When routing is being evaluated.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
event: "NotificationEvent"
|
|
66
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
68
|
+
|
|
69
|
+
def get_severity(self) -> str | None:
|
|
70
|
+
"""Get event severity if available."""
|
|
71
|
+
# Try event data first
|
|
72
|
+
if hasattr(self.event, "severity"):
|
|
73
|
+
return self.event.severity
|
|
74
|
+
if hasattr(self.event, "has_critical") and self.event.has_critical:
|
|
75
|
+
return "critical"
|
|
76
|
+
if hasattr(self.event, "has_high") and self.event.has_high:
|
|
77
|
+
return "high"
|
|
78
|
+
|
|
79
|
+
# Try metadata
|
|
80
|
+
return self.metadata.get("severity")
|
|
81
|
+
|
|
82
|
+
def get_issue_count(self) -> int | None:
|
|
83
|
+
"""Get issue count if available."""
|
|
84
|
+
if hasattr(self.event, "total_issues"):
|
|
85
|
+
return self.event.total_issues
|
|
86
|
+
return self.metadata.get("issue_count")
|
|
87
|
+
|
|
88
|
+
def get_pass_rate(self) -> float | None:
|
|
89
|
+
"""Get validation pass rate if available."""
|
|
90
|
+
if hasattr(self.event, "pass_rate"):
|
|
91
|
+
return self.event.pass_rate
|
|
92
|
+
return self.metadata.get("pass_rate")
|
|
93
|
+
|
|
94
|
+
def get_tags(self) -> list[str]:
|
|
95
|
+
"""Get context tags."""
|
|
96
|
+
tags = list(self.metadata.get("tags", []))
|
|
97
|
+
|
|
98
|
+
# Add event-derived tags
|
|
99
|
+
if self.event.source_name:
|
|
100
|
+
tags.append(f"source:{self.event.source_name}")
|
|
101
|
+
if self.event.event_type:
|
|
102
|
+
tags.append(f"type:{self.event.event_type}")
|
|
103
|
+
|
|
104
|
+
return tags
|
|
105
|
+
|
|
106
|
+
def get_data_asset(self) -> str | None:
|
|
107
|
+
"""Get data asset name/path."""
|
|
108
|
+
if self.event.source_name:
|
|
109
|
+
return self.event.source_name
|
|
110
|
+
return self.metadata.get("data_asset")
|
|
111
|
+
|
|
112
|
+
def get_metadata(self, key: str) -> Any:
|
|
113
|
+
"""Get metadata value by key."""
|
|
114
|
+
# Check event data first
|
|
115
|
+
if hasattr(self.event, "data") and key in self.event.data:
|
|
116
|
+
return self.event.data[key]
|
|
117
|
+
return self.metadata.get(key)
|
|
118
|
+
|
|
119
|
+
def get_status(self) -> str | None:
|
|
120
|
+
"""Get validation status."""
|
|
121
|
+
if hasattr(self.event, "status"):
|
|
122
|
+
return self.event.status
|
|
123
|
+
# Infer from event type
|
|
124
|
+
if self.event.event_type in ("validation_failed", "schedule_failed"):
|
|
125
|
+
return "failure"
|
|
126
|
+
return self.metadata.get("status")
|
|
127
|
+
|
|
128
|
+
def get_error_message(self) -> str | None:
|
|
129
|
+
"""Get error message if available."""
|
|
130
|
+
if hasattr(self.event, "error_message"):
|
|
131
|
+
return self.event.error_message
|
|
132
|
+
return self.metadata.get("error_message")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class Route:
|
|
137
|
+
"""A routing rule with associated actions.
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
name: Unique route name.
|
|
141
|
+
rule: The rule to evaluate.
|
|
142
|
+
actions: List of channel IDs to notify.
|
|
143
|
+
priority: Route priority (higher = evaluated first).
|
|
144
|
+
is_active: Whether route is active.
|
|
145
|
+
escalation_policy_id: Optional escalation policy to trigger.
|
|
146
|
+
stop_on_match: If True, stop evaluating lower priority routes.
|
|
147
|
+
metadata: Additional route metadata.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
name: str
|
|
151
|
+
rule: BaseRule
|
|
152
|
+
actions: list[str]
|
|
153
|
+
priority: int = 0
|
|
154
|
+
is_active: bool = True
|
|
155
|
+
escalation_policy_id: str | None = None
|
|
156
|
+
stop_on_match: bool = False
|
|
157
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
158
|
+
|
|
159
|
+
def to_dict(self) -> dict[str, Any]:
|
|
160
|
+
"""Serialize route to dictionary."""
|
|
161
|
+
return {
|
|
162
|
+
"name": self.name,
|
|
163
|
+
"rule": self.rule.to_dict(),
|
|
164
|
+
"actions": self.actions,
|
|
165
|
+
"priority": self.priority,
|
|
166
|
+
"is_active": self.is_active,
|
|
167
|
+
"escalation_policy_id": self.escalation_policy_id,
|
|
168
|
+
"stop_on_match": self.stop_on_match,
|
|
169
|
+
"metadata": self.metadata,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dict(cls, data: dict[str, Any]) -> "Route | None":
|
|
174
|
+
"""Create Route from dictionary."""
|
|
175
|
+
rule_data = data.get("rule")
|
|
176
|
+
if not rule_data:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
rule = BaseRule.from_dict(rule_data)
|
|
180
|
+
if rule is None:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
return cls(
|
|
184
|
+
name=data.get("name", "unnamed"),
|
|
185
|
+
rule=rule,
|
|
186
|
+
actions=data.get("actions", []),
|
|
187
|
+
priority=data.get("priority", 0),
|
|
188
|
+
is_active=data.get("is_active", True),
|
|
189
|
+
escalation_policy_id=data.get("escalation_policy_id"),
|
|
190
|
+
stop_on_match=data.get("stop_on_match", False),
|
|
191
|
+
metadata=data.get("metadata", {}),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class RoutingResult:
|
|
197
|
+
"""Result of route evaluation.
|
|
198
|
+
|
|
199
|
+
Attributes:
|
|
200
|
+
matched_routes: Routes that matched the context.
|
|
201
|
+
all_actions: Deduplicated list of all action channel IDs.
|
|
202
|
+
evaluation_time_ms: Time taken to evaluate routes.
|
|
203
|
+
context: The evaluated context.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
matched_routes: list[Route]
|
|
207
|
+
all_actions: list[str]
|
|
208
|
+
evaluation_time_ms: float
|
|
209
|
+
context: RouteContext
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def has_matches(self) -> bool:
|
|
213
|
+
"""Check if any routes matched."""
|
|
214
|
+
return len(self.matched_routes) > 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class ActionRouter:
|
|
218
|
+
"""Main routing engine.
|
|
219
|
+
|
|
220
|
+
Evaluates events against configured routes and returns
|
|
221
|
+
matching routes sorted by priority.
|
|
222
|
+
|
|
223
|
+
Routes are evaluated in priority order (highest first).
|
|
224
|
+
If a route has `stop_on_match=True`, lower priority routes
|
|
225
|
+
are skipped once it matches.
|
|
226
|
+
|
|
227
|
+
Attributes:
|
|
228
|
+
routes: List of configured routes.
|
|
229
|
+
default_route: Optional fallback route if nothing matches.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
routes: list[Route] | None = None,
|
|
235
|
+
default_route: Route | None = None,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Initialize the router.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
routes: List of routes to evaluate.
|
|
241
|
+
default_route: Fallback route if nothing matches.
|
|
242
|
+
"""
|
|
243
|
+
self.routes = routes or []
|
|
244
|
+
self.default_route = default_route
|
|
245
|
+
self._sort_routes()
|
|
246
|
+
|
|
247
|
+
def _sort_routes(self) -> None:
|
|
248
|
+
"""Sort routes by priority (highest first)."""
|
|
249
|
+
self.routes.sort(key=lambda r: r.priority, reverse=True)
|
|
250
|
+
|
|
251
|
+
def add_route(self, route: Route) -> None:
|
|
252
|
+
"""Add a route to the router.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
route: Route to add.
|
|
256
|
+
"""
|
|
257
|
+
self.routes.append(route)
|
|
258
|
+
self._sort_routes()
|
|
259
|
+
|
|
260
|
+
def remove_route(self, name: str) -> bool:
|
|
261
|
+
"""Remove a route by name.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: Route name to remove.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if route was found and removed.
|
|
268
|
+
"""
|
|
269
|
+
for i, route in enumerate(self.routes):
|
|
270
|
+
if route.name == name:
|
|
271
|
+
del self.routes[i]
|
|
272
|
+
return True
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
def get_route(self, name: str) -> Route | None:
|
|
276
|
+
"""Get a route by name.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
name: Route name.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Route if found, None otherwise.
|
|
283
|
+
"""
|
|
284
|
+
for route in self.routes:
|
|
285
|
+
if route.name == name:
|
|
286
|
+
return route
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
async def match(self, context: RouteContext) -> RoutingResult:
|
|
290
|
+
"""Evaluate all routes against the context.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
context: The routing context to evaluate.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
RoutingResult with matched routes and actions.
|
|
297
|
+
"""
|
|
298
|
+
import time
|
|
299
|
+
|
|
300
|
+
start_time = time.perf_counter()
|
|
301
|
+
matched_routes: list[Route] = []
|
|
302
|
+
all_actions: set[str] = set()
|
|
303
|
+
|
|
304
|
+
for route in self.routes:
|
|
305
|
+
# Skip inactive routes
|
|
306
|
+
if not route.is_active:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# Evaluate rule
|
|
310
|
+
try:
|
|
311
|
+
if await route.rule.matches(context):
|
|
312
|
+
matched_routes.append(route)
|
|
313
|
+
all_actions.update(route.actions)
|
|
314
|
+
|
|
315
|
+
# Stop if this route has stop_on_match
|
|
316
|
+
if route.stop_on_match:
|
|
317
|
+
break
|
|
318
|
+
except Exception:
|
|
319
|
+
# Log error but continue with other routes
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
# Use default route if nothing matched
|
|
323
|
+
if not matched_routes and self.default_route:
|
|
324
|
+
if self.default_route.is_active:
|
|
325
|
+
try:
|
|
326
|
+
if await self.default_route.rule.matches(context):
|
|
327
|
+
matched_routes.append(self.default_route)
|
|
328
|
+
all_actions.update(self.default_route.actions)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
333
|
+
|
|
334
|
+
return RoutingResult(
|
|
335
|
+
matched_routes=matched_routes,
|
|
336
|
+
all_actions=list(all_actions),
|
|
337
|
+
evaluation_time_ms=elapsed_ms,
|
|
338
|
+
context=context,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
async def get_channels_for_event(
|
|
342
|
+
self,
|
|
343
|
+
event: "NotificationEvent",
|
|
344
|
+
metadata: dict[str, Any] | None = None,
|
|
345
|
+
) -> list[str]:
|
|
346
|
+
"""Convenience method to get channel IDs for an event.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
event: The notification event.
|
|
350
|
+
metadata: Optional additional metadata.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List of channel IDs to notify.
|
|
354
|
+
"""
|
|
355
|
+
context = RouteContext(
|
|
356
|
+
event=event,
|
|
357
|
+
metadata=metadata or {},
|
|
358
|
+
)
|
|
359
|
+
result = await self.match(context)
|
|
360
|
+
return result.all_actions
|
|
361
|
+
|
|
362
|
+
def to_dict(self) -> dict[str, Any]:
|
|
363
|
+
"""Serialize router configuration."""
|
|
364
|
+
return {
|
|
365
|
+
"routes": [r.to_dict() for r in self.routes],
|
|
366
|
+
"default_route": self.default_route.to_dict() if self.default_route else None,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def from_dict(cls, data: dict[str, Any]) -> "ActionRouter":
|
|
371
|
+
"""Create router from dictionary."""
|
|
372
|
+
routes = []
|
|
373
|
+
for route_data in data.get("routes", []):
|
|
374
|
+
route = Route.from_dict(route_data)
|
|
375
|
+
if route:
|
|
376
|
+
routes.append(route)
|
|
377
|
+
|
|
378
|
+
default_route = None
|
|
379
|
+
if data.get("default_route"):
|
|
380
|
+
default_route = Route.from_dict(data["default_route"])
|
|
381
|
+
|
|
382
|
+
return cls(routes=routes, default_route=default_route)
|