truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__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.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +645 -23
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +15 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,40 +1,10 @@
1
- """Routing engine for notification dispatch.
1
+ """Dashboard-specific routing adapters using truthound.checkpoint.routing.
2
2
 
3
- This module provides the core routing engine that evaluates events
4
- against configured routes and determines target channels.
3
+ This module provides adapters that convert Dashboard notification events
4
+ into truthound RouteContext objects for use with truthound's routing system.
5
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}")
6
+ The main purpose is to bridge the Dashboard's event system with truthound's
7
+ routing infrastructure.
38
8
  """
39
9
 
40
10
  from __future__ import annotations
@@ -43,18 +13,18 @@ from dataclasses import dataclass, field
43
13
  from datetime import datetime
44
14
  from typing import TYPE_CHECKING, Any
45
15
 
46
- from .rules import BaseRule
16
+ from truthound.checkpoint.routing.base import RouteContext
47
17
 
48
18
  if TYPE_CHECKING:
49
19
  from truthound_dashboard.core.notifications.base import NotificationEvent
50
20
 
51
21
 
52
22
  @dataclass
53
- class RouteContext:
54
- """Context for route evaluation.
23
+ class DashboardRouteContext:
24
+ """Dashboard-specific context that wraps truthound's RouteContext.
55
25
 
56
- Holds the event data and additional metadata that rules
57
- can use to make matching decisions.
26
+ This provides helper methods for extracting data from Dashboard
27
+ notification events.
58
28
 
59
29
  Attributes:
60
30
  event: The notification event being routed.
@@ -68,315 +38,118 @@ class RouteContext:
68
38
 
69
39
  def get_severity(self) -> str | None:
70
40
  """Get event severity if available."""
71
- # Try event data first
72
41
  if hasattr(self.event, "severity"):
73
42
  return self.event.severity
74
43
  if hasattr(self.event, "has_critical") and self.event.has_critical:
75
44
  return "critical"
76
45
  if hasattr(self.event, "has_high") and self.event.has_high:
77
46
  return "high"
78
-
79
- # Try metadata
80
47
  return self.metadata.get("severity")
81
48
 
82
- def get_issue_count(self) -> int | None:
49
+ def get_issue_count(self) -> int:
83
50
  """Get issue count if available."""
84
51
  if hasattr(self.event, "total_issues"):
85
52
  return self.event.total_issues
86
- return self.metadata.get("issue_count")
53
+ return self.metadata.get("issue_count", 0)
87
54
 
88
- def get_pass_rate(self) -> float | None:
55
+ def get_pass_rate(self) -> float:
89
56
  """Get validation pass rate if available."""
90
57
  if hasattr(self.event, "pass_rate"):
91
58
  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", []))
59
+ return self.metadata.get("pass_rate", 100.0)
97
60
 
98
- # Add event-derived tags
61
+ def get_tags(self) -> dict[str, str]:
62
+ """Get context tags as dict."""
63
+ tags = dict(self.metadata.get("tags", {}))
99
64
  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
-
65
+ tags["source"] = self.event.source_name
66
+ tags["event_type"] = self.event.event_type
104
67
  return tags
105
68
 
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
-
69
+ def to_truthound_context(self) -> RouteContext:
70
+ """Convert to truthound RouteContext.
134
71
 
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", {}),
72
+ Creates a RouteContext object that can be used with truthound's
73
+ ActionRouter for rule evaluation.
74
+ """
75
+ # Determine severity counts
76
+ severity = self.get_severity()
77
+ critical_issues = 1 if severity == "critical" else 0
78
+ high_issues = 1 if severity == "high" else 0
79
+ medium_issues = 1 if severity == "medium" else 0
80
+ low_issues = 1 if severity == "low" else 0
81
+ info_issues = 1 if severity == "info" else 0
82
+
83
+ # Determine status
84
+ status = "success"
85
+ if hasattr(self.event, "event_type"):
86
+ if "failed" in self.event.event_type.lower():
87
+ status = "failure"
88
+ elif "error" in self.event.event_type.lower():
89
+ status = "error"
90
+ elif "warning" in self.event.event_type.lower():
91
+ status = "warning"
92
+
93
+ return RouteContext(
94
+ checkpoint_name=self.event.source_name or "dashboard_event",
95
+ run_id=self.metadata.get("run_id", "dashboard"),
96
+ status=status,
97
+ data_asset=self.event.source_name or "unknown",
98
+ run_time=self.timestamp,
99
+ total_issues=self.get_issue_count(),
100
+ critical_issues=critical_issues,
101
+ high_issues=high_issues,
102
+ medium_issues=medium_issues,
103
+ low_issues=low_issues,
104
+ info_issues=info_issues,
105
+ pass_rate=self.get_pass_rate(),
106
+ tags=self.get_tags(),
107
+ metadata=self.metadata,
108
+ error=self.metadata.get("error"),
192
109
  )
193
110
 
194
111
 
195
112
  @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.
113
+ class DashboardRoutingResult:
114
+ """Result of routing evaluation for Dashboard notifications.
226
115
 
227
116
  Attributes:
228
- routes: List of configured routes.
229
- default_route: Optional fallback route if nothing matches.
117
+ matched_routes: List of route names that matched.
118
+ channel_ids: Set of channel IDs to send notifications to.
119
+ timestamp: When routing was evaluated.
120
+ context: The context that was evaluated.
230
121
  """
231
122
 
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
123
+ matched_routes: list[str] = field(default_factory=list)
124
+ channel_ids: set[str] = field(default_factory=set)
125
+ timestamp: datetime = field(default_factory=datetime.utcnow)
126
+ context: DashboardRouteContext | None = None
331
127
 
332
- elapsed_ms = (time.perf_counter() - start_time) * 1000
333
128
 
334
- return RoutingResult(
335
- matched_routes=matched_routes,
336
- all_actions=list(all_actions),
337
- evaluation_time_ms=elapsed_ms,
338
- context=context,
339
- )
129
+ def create_route_context_from_event(
130
+ event: "NotificationEvent",
131
+ metadata: dict[str, Any] | None = None,
132
+ ) -> RouteContext:
133
+ """Create a truthound RouteContext from a Dashboard notification event.
340
134
 
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.
135
+ This is a convenience function for converting Dashboard events into
136
+ truthound's routing context.
347
137
 
348
- Args:
349
- event: The notification event.
350
- metadata: Optional additional metadata.
138
+ Args:
139
+ event: The notification event.
140
+ metadata: Optional additional metadata.
351
141
 
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
142
+ Returns:
143
+ RouteContext for use with truthound's ActionRouter.
361
144
 
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
- }
145
+ Example:
146
+ from truthound.checkpoint.routing import ActionRouter
368
147
 
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)
148
+ context = create_route_context_from_event(event, {"env": "prod"})
149
+ matched = router.route(context)
150
+ """
151
+ dashboard_context = DashboardRouteContext(
152
+ event=event,
153
+ metadata=metadata or {},
154
+ )
155
+ return dashboard_context.to_truthound_context()