truthound-dashboard 1.3.0__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.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.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
|