truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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 (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,625 @@
1
+ """Routing rule implementations.
2
+
3
+ This module provides 11 built-in rule types for matching notification
4
+ events against configurable conditions.
5
+
6
+ Rule Types:
7
+ - SeverityRule: Match by issue severity
8
+ - IssueCountRule: Match by issue count
9
+ - PassRateRule: Match by validation pass rate
10
+ - TimeWindowRule: Match by time of day/week
11
+ - TagRule: Match by tags
12
+ - DataAssetRule: Match by data asset pattern
13
+ - MetadataRule: Match by metadata fields
14
+ - StatusRule: Match by validation status
15
+ - ErrorRule: Match by error patterns
16
+ - AlwaysRule: Always matches
17
+ - NeverRule: Never matches
18
+
19
+ Each rule can be serialized to/from JSON for configuration storage.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import fnmatch
25
+ import re
26
+ from abc import ABC, abstractmethod
27
+ from dataclasses import dataclass, field
28
+ from datetime import datetime
29
+ from enum import Enum
30
+ from typing import TYPE_CHECKING, Any, ClassVar
31
+
32
+ if TYPE_CHECKING:
33
+ from .engine import RouteContext
34
+
35
+
36
+ class Severity(str, Enum):
37
+ """Issue severity levels for routing."""
38
+
39
+ CRITICAL = "critical"
40
+ HIGH = "high"
41
+ MEDIUM = "medium"
42
+ LOW = "low"
43
+ INFO = "info"
44
+
45
+ @classmethod
46
+ def from_string(cls, value: str) -> "Severity":
47
+ """Convert string to Severity enum."""
48
+ try:
49
+ return cls(value.lower())
50
+ except ValueError:
51
+ return cls.INFO
52
+
53
+ def __ge__(self, other: "Severity") -> bool:
54
+ """Compare severity levels."""
55
+ order = [cls.INFO, cls.LOW, cls.MEDIUM, cls.HIGH, cls.CRITICAL]
56
+ return order.index(self) >= order.index(other)
57
+
58
+ def __gt__(self, other: "Severity") -> bool:
59
+ """Compare severity levels."""
60
+ order = [cls.INFO, cls.LOW, cls.MEDIUM, cls.HIGH, cls.CRITICAL]
61
+ return order.index(self) > order.index(other)
62
+
63
+
64
+ class RuleRegistry:
65
+ """Registry for routing rule types.
66
+
67
+ Provides a plugin system for registering custom rule implementations.
68
+
69
+ Usage:
70
+ @RuleRegistry.register("custom")
71
+ class CustomRule(BaseRule):
72
+ ...
73
+
74
+ rule = RuleRegistry.create("custom", params={...})
75
+ """
76
+
77
+ _rules: ClassVar[dict[str, type["BaseRule"]]] = {}
78
+
79
+ @classmethod
80
+ def register(cls, rule_type: str):
81
+ """Decorator to register a rule type."""
82
+
83
+ def decorator(rule_class: type["BaseRule"]) -> type["BaseRule"]:
84
+ rule_class.rule_type = rule_type
85
+ cls._rules[rule_type] = rule_class
86
+ return rule_class
87
+
88
+ return decorator
89
+
90
+ @classmethod
91
+ def get(cls, rule_type: str) -> type["BaseRule"] | None:
92
+ """Get a registered rule class by type."""
93
+ return cls._rules.get(rule_type)
94
+
95
+ @classmethod
96
+ def create(cls, rule_type: str, **params: Any) -> "BaseRule | None":
97
+ """Create a rule instance by type."""
98
+ rule_class = cls.get(rule_type)
99
+ if rule_class is None:
100
+ return None
101
+ return rule_class(**params)
102
+
103
+ @classmethod
104
+ def list_types(cls) -> list[str]:
105
+ """Get list of registered rule types."""
106
+ return list(cls._rules.keys())
107
+
108
+ @classmethod
109
+ def get_all_schemas(cls) -> dict[str, dict[str, Any]]:
110
+ """Get parameter schemas for all registered rules."""
111
+ return {
112
+ rule_type: rule_class.get_param_schema()
113
+ for rule_type, rule_class in cls._rules.items()
114
+ }
115
+
116
+
117
+ @dataclass
118
+ class BaseRule(ABC):
119
+ """Abstract base class for routing rules.
120
+
121
+ All rules must implement the `matches` method to evaluate
122
+ whether an event context matches the rule's conditions.
123
+
124
+ Rules can be serialized to/from JSON using `to_dict` and `from_dict`.
125
+ """
126
+
127
+ rule_type: ClassVar[str] = "base"
128
+
129
+ @abstractmethod
130
+ async def matches(self, context: "RouteContext") -> bool:
131
+ """Check if the context matches this rule.
132
+
133
+ Args:
134
+ context: The routing context containing event and metadata.
135
+
136
+ Returns:
137
+ True if the rule matches.
138
+ """
139
+ ...
140
+
141
+ @classmethod
142
+ def get_param_schema(cls) -> dict[str, Any]:
143
+ """Get parameter schema for this rule type.
144
+
145
+ Returns:
146
+ Dictionary describing the rule's parameters.
147
+ """
148
+ return {}
149
+
150
+ def to_dict(self) -> dict[str, Any]:
151
+ """Serialize rule to dictionary."""
152
+ return {"type": self.rule_type}
153
+
154
+ @classmethod
155
+ def from_dict(cls, data: dict[str, Any]) -> "BaseRule | None":
156
+ """Deserialize rule from dictionary."""
157
+ rule_type = data.get("type")
158
+ if rule_type is None:
159
+ return None
160
+
161
+ params = {k: v for k, v in data.items() if k != "type"}
162
+ return RuleRegistry.create(rule_type, **params)
163
+
164
+
165
+ @RuleRegistry.register("severity")
166
+ @dataclass
167
+ class SeverityRule(BaseRule):
168
+ """Match events by minimum severity level.
169
+
170
+ Matches if the event's severity is greater than or equal to
171
+ the specified minimum severity.
172
+
173
+ Attributes:
174
+ min_severity: Minimum severity to match (critical, high, medium, low, info).
175
+ """
176
+
177
+ min_severity: str = "high"
178
+
179
+ @classmethod
180
+ def get_param_schema(cls) -> dict[str, Any]:
181
+ return {
182
+ "min_severity": {
183
+ "type": "string",
184
+ "required": True,
185
+ "description": "Minimum severity level",
186
+ "enum": ["critical", "high", "medium", "low", "info"],
187
+ }
188
+ }
189
+
190
+ async def matches(self, context: "RouteContext") -> bool:
191
+ """Check if event severity meets minimum threshold."""
192
+ event_severity = context.get_severity()
193
+ if event_severity is None:
194
+ return False
195
+
196
+ min_sev = Severity.from_string(self.min_severity)
197
+ actual_sev = Severity.from_string(event_severity)
198
+ return actual_sev >= min_sev
199
+
200
+ def to_dict(self) -> dict[str, Any]:
201
+ return {"type": self.rule_type, "min_severity": self.min_severity}
202
+
203
+
204
+ @RuleRegistry.register("issue_count")
205
+ @dataclass
206
+ class IssueCountRule(BaseRule):
207
+ """Match events by minimum issue count.
208
+
209
+ Matches if the event has at least the specified number of issues.
210
+
211
+ Attributes:
212
+ min_count: Minimum number of issues to match.
213
+ """
214
+
215
+ min_count: int = 1
216
+
217
+ @classmethod
218
+ def get_param_schema(cls) -> dict[str, Any]:
219
+ return {
220
+ "min_count": {
221
+ "type": "integer",
222
+ "required": True,
223
+ "description": "Minimum issue count",
224
+ "minimum": 0,
225
+ }
226
+ }
227
+
228
+ async def matches(self, context: "RouteContext") -> bool:
229
+ """Check if event has minimum issue count."""
230
+ issue_count = context.get_issue_count()
231
+ return issue_count is not None and issue_count >= self.min_count
232
+
233
+ def to_dict(self) -> dict[str, Any]:
234
+ return {"type": self.rule_type, "min_count": self.min_count}
235
+
236
+
237
+ @RuleRegistry.register("pass_rate")
238
+ @dataclass
239
+ class PassRateRule(BaseRule):
240
+ """Match events by maximum pass rate.
241
+
242
+ Matches if the validation pass rate is below the specified threshold.
243
+
244
+ Attributes:
245
+ max_pass_rate: Maximum pass rate (0.0 to 1.0) to match.
246
+ """
247
+
248
+ max_pass_rate: float = 0.9
249
+
250
+ @classmethod
251
+ def get_param_schema(cls) -> dict[str, Any]:
252
+ return {
253
+ "max_pass_rate": {
254
+ "type": "number",
255
+ "required": True,
256
+ "description": "Maximum pass rate (0.0 to 1.0)",
257
+ "minimum": 0.0,
258
+ "maximum": 1.0,
259
+ }
260
+ }
261
+
262
+ async def matches(self, context: "RouteContext") -> bool:
263
+ """Check if pass rate is below threshold."""
264
+ pass_rate = context.get_pass_rate()
265
+ return pass_rate is not None and pass_rate <= self.max_pass_rate
266
+
267
+ def to_dict(self) -> dict[str, Any]:
268
+ return {"type": self.rule_type, "max_pass_rate": self.max_pass_rate}
269
+
270
+
271
+ @RuleRegistry.register("time_window")
272
+ @dataclass
273
+ class TimeWindowRule(BaseRule):
274
+ """Match events by time of day and day of week.
275
+
276
+ Matches if the current time falls within the specified window.
277
+ Useful for business hours routing or off-hours escalation.
278
+
279
+ Attributes:
280
+ start_hour: Start hour (0-23).
281
+ end_hour: End hour (0-23).
282
+ weekdays: List of weekday numbers (0=Monday, 6=Sunday).
283
+ timezone: Optional timezone name.
284
+ """
285
+
286
+ start_hour: int = 9
287
+ end_hour: int = 17
288
+ weekdays: list[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
289
+ timezone: str | None = None
290
+
291
+ @classmethod
292
+ def get_param_schema(cls) -> dict[str, Any]:
293
+ return {
294
+ "start_hour": {
295
+ "type": "integer",
296
+ "required": True,
297
+ "description": "Start hour (0-23)",
298
+ "minimum": 0,
299
+ "maximum": 23,
300
+ },
301
+ "end_hour": {
302
+ "type": "integer",
303
+ "required": True,
304
+ "description": "End hour (0-23)",
305
+ "minimum": 0,
306
+ "maximum": 23,
307
+ },
308
+ "weekdays": {
309
+ "type": "array",
310
+ "required": False,
311
+ "description": "Weekdays (0=Monday, 6=Sunday)",
312
+ "items": {"type": "integer", "minimum": 0, "maximum": 6},
313
+ },
314
+ "timezone": {
315
+ "type": "string",
316
+ "required": False,
317
+ "description": "Timezone name (e.g., 'America/New_York')",
318
+ },
319
+ }
320
+
321
+ async def matches(self, context: "RouteContext") -> bool:
322
+ """Check if current time is within window."""
323
+ now = datetime.now()
324
+
325
+ # Apply timezone if specified
326
+ if self.timezone:
327
+ try:
328
+ import zoneinfo
329
+ tz = zoneinfo.ZoneInfo(self.timezone)
330
+ now = datetime.now(tz)
331
+ except ImportError:
332
+ pass
333
+
334
+ # Check weekday
335
+ if now.weekday() not in self.weekdays:
336
+ return False
337
+
338
+ # Check hour range
339
+ current_hour = now.hour
340
+ if self.start_hour <= self.end_hour:
341
+ # Normal range (e.g., 9-17)
342
+ return self.start_hour <= current_hour < self.end_hour
343
+ else:
344
+ # Overnight range (e.g., 22-6)
345
+ return current_hour >= self.start_hour or current_hour < self.end_hour
346
+
347
+ def to_dict(self) -> dict[str, Any]:
348
+ data = {
349
+ "type": self.rule_type,
350
+ "start_hour": self.start_hour,
351
+ "end_hour": self.end_hour,
352
+ "weekdays": self.weekdays,
353
+ }
354
+ if self.timezone:
355
+ data["timezone"] = self.timezone
356
+ return data
357
+
358
+
359
+ @RuleRegistry.register("tag")
360
+ @dataclass
361
+ class TagRule(BaseRule):
362
+ """Match events by tags.
363
+
364
+ Matches if the event or context has any of the specified tags.
365
+
366
+ Attributes:
367
+ tags: List of tags to match.
368
+ match_all: If True, all tags must match; if False, any tag matches.
369
+ """
370
+
371
+ tags: list[str] = field(default_factory=list)
372
+ match_all: bool = False
373
+
374
+ @classmethod
375
+ def get_param_schema(cls) -> dict[str, Any]:
376
+ return {
377
+ "tags": {
378
+ "type": "array",
379
+ "required": True,
380
+ "description": "Tags to match",
381
+ "items": {"type": "string"},
382
+ },
383
+ "match_all": {
384
+ "type": "boolean",
385
+ "required": False,
386
+ "description": "Require all tags to match",
387
+ },
388
+ }
389
+
390
+ async def matches(self, context: "RouteContext") -> bool:
391
+ """Check if context has matching tags."""
392
+ context_tags = set(context.get_tags())
393
+
394
+ if not self.tags:
395
+ return True
396
+
397
+ if self.match_all:
398
+ return set(self.tags).issubset(context_tags)
399
+ else:
400
+ return bool(set(self.tags) & context_tags)
401
+
402
+ def to_dict(self) -> dict[str, Any]:
403
+ return {
404
+ "type": self.rule_type,
405
+ "tags": self.tags,
406
+ "match_all": self.match_all,
407
+ }
408
+
409
+
410
+ @RuleRegistry.register("data_asset")
411
+ @dataclass
412
+ class DataAssetRule(BaseRule):
413
+ """Match events by data asset pattern.
414
+
415
+ Matches if the source name or path matches a glob pattern.
416
+
417
+ Attributes:
418
+ pattern: Glob pattern to match (e.g., "*.parquet", "prod/*").
419
+ """
420
+
421
+ pattern: str = "*"
422
+
423
+ @classmethod
424
+ def get_param_schema(cls) -> dict[str, Any]:
425
+ return {
426
+ "pattern": {
427
+ "type": "string",
428
+ "required": True,
429
+ "description": "Glob pattern to match data assets",
430
+ }
431
+ }
432
+
433
+ async def matches(self, context: "RouteContext") -> bool:
434
+ """Check if data asset matches pattern."""
435
+ asset_name = context.get_data_asset()
436
+ if asset_name is None:
437
+ return False
438
+
439
+ return fnmatch.fnmatch(asset_name.lower(), self.pattern.lower())
440
+
441
+ def to_dict(self) -> dict[str, Any]:
442
+ return {"type": self.rule_type, "pattern": self.pattern}
443
+
444
+
445
+ @RuleRegistry.register("metadata")
446
+ @dataclass
447
+ class MetadataRule(BaseRule):
448
+ """Match events by metadata field value.
449
+
450
+ Matches if a metadata field equals a specific value.
451
+
452
+ Attributes:
453
+ key: Metadata field name.
454
+ value: Expected value (supports string, number, boolean).
455
+ operator: Comparison operator (eq, ne, contains, regex).
456
+ """
457
+
458
+ key: str = ""
459
+ value: Any = None
460
+ operator: str = "eq"
461
+
462
+ @classmethod
463
+ def get_param_schema(cls) -> dict[str, Any]:
464
+ return {
465
+ "key": {
466
+ "type": "string",
467
+ "required": True,
468
+ "description": "Metadata field name",
469
+ },
470
+ "value": {
471
+ "type": "any",
472
+ "required": True,
473
+ "description": "Expected value",
474
+ },
475
+ "operator": {
476
+ "type": "string",
477
+ "required": False,
478
+ "description": "Comparison operator",
479
+ "enum": ["eq", "ne", "contains", "regex", "gt", "lt", "gte", "lte"],
480
+ },
481
+ }
482
+
483
+ async def matches(self, context: "RouteContext") -> bool:
484
+ """Check if metadata field matches value."""
485
+ actual = context.get_metadata(self.key)
486
+ if actual is None:
487
+ return False
488
+
489
+ if self.operator == "eq":
490
+ return actual == self.value
491
+ elif self.operator == "ne":
492
+ return actual != self.value
493
+ elif self.operator == "contains":
494
+ return str(self.value) in str(actual)
495
+ elif self.operator == "regex":
496
+ return bool(re.search(str(self.value), str(actual)))
497
+ elif self.operator in ("gt", "lt", "gte", "lte"):
498
+ try:
499
+ a = float(actual)
500
+ b = float(self.value)
501
+ if self.operator == "gt":
502
+ return a > b
503
+ elif self.operator == "lt":
504
+ return a < b
505
+ elif self.operator == "gte":
506
+ return a >= b
507
+ elif self.operator == "lte":
508
+ return a <= b
509
+ except (ValueError, TypeError):
510
+ return False
511
+
512
+ return False
513
+
514
+ def to_dict(self) -> dict[str, Any]:
515
+ return {
516
+ "type": self.rule_type,
517
+ "key": self.key,
518
+ "value": self.value,
519
+ "operator": self.operator,
520
+ }
521
+
522
+
523
+ @RuleRegistry.register("status")
524
+ @dataclass
525
+ class StatusRule(BaseRule):
526
+ """Match events by validation status.
527
+
528
+ Matches if the validation status is in the specified list.
529
+
530
+ Attributes:
531
+ statuses: List of statuses to match (failure, error, warning, success).
532
+ """
533
+
534
+ statuses: list[str] = field(default_factory=lambda: ["failure", "error"])
535
+
536
+ @classmethod
537
+ def get_param_schema(cls) -> dict[str, Any]:
538
+ return {
539
+ "statuses": {
540
+ "type": "array",
541
+ "required": True,
542
+ "description": "Validation statuses to match",
543
+ "items": {
544
+ "type": "string",
545
+ "enum": ["failure", "error", "warning", "success"],
546
+ },
547
+ }
548
+ }
549
+
550
+ async def matches(self, context: "RouteContext") -> bool:
551
+ """Check if validation status is in list."""
552
+ status = context.get_status()
553
+ return status is not None and status.lower() in [s.lower() for s in self.statuses]
554
+
555
+ def to_dict(self) -> dict[str, Any]:
556
+ return {"type": self.rule_type, "statuses": self.statuses}
557
+
558
+
559
+ @RuleRegistry.register("error")
560
+ @dataclass
561
+ class ErrorRule(BaseRule):
562
+ """Match events by error pattern.
563
+
564
+ Matches if the error message contains or matches a pattern.
565
+
566
+ Attributes:
567
+ error_pattern: Regex pattern to match error messages.
568
+ """
569
+
570
+ error_pattern: str = ".*"
571
+
572
+ @classmethod
573
+ def get_param_schema(cls) -> dict[str, Any]:
574
+ return {
575
+ "error_pattern": {
576
+ "type": "string",
577
+ "required": True,
578
+ "description": "Regex pattern to match errors",
579
+ }
580
+ }
581
+
582
+ async def matches(self, context: "RouteContext") -> bool:
583
+ """Check if error message matches pattern."""
584
+ error = context.get_error_message()
585
+ if error is None:
586
+ return False
587
+
588
+ return bool(re.search(self.error_pattern, error, re.IGNORECASE))
589
+
590
+ def to_dict(self) -> dict[str, Any]:
591
+ return {"type": self.rule_type, "error_pattern": self.error_pattern}
592
+
593
+
594
+ @RuleRegistry.register("always")
595
+ @dataclass
596
+ class AlwaysRule(BaseRule):
597
+ """Rule that always matches.
598
+
599
+ Useful as a fallback or default route.
600
+ """
601
+
602
+ @classmethod
603
+ def get_param_schema(cls) -> dict[str, Any]:
604
+ return {}
605
+
606
+ async def matches(self, context: "RouteContext") -> bool:
607
+ """Always returns True."""
608
+ return True
609
+
610
+
611
+ @RuleRegistry.register("never")
612
+ @dataclass
613
+ class NeverRule(BaseRule):
614
+ """Rule that never matches.
615
+
616
+ Useful for disabled routes without removing configuration.
617
+ """
618
+
619
+ @classmethod
620
+ def get_param_schema(cls) -> dict[str, Any]:
621
+ return {}
622
+
623
+ async def matches(self, context: "RouteContext") -> bool:
624
+ """Always returns False."""
625
+ return False