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.
- 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.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.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.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Configuration parser for routing rules.
|
|
2
|
+
|
|
3
|
+
This module provides YAML/JSON configuration parsing for
|
|
4
|
+
creating routes and rules from configuration files.
|
|
5
|
+
|
|
6
|
+
Example YAML config:
|
|
7
|
+
routes:
|
|
8
|
+
- name: critical_pagerduty
|
|
9
|
+
rule:
|
|
10
|
+
type: severity
|
|
11
|
+
min_severity: critical
|
|
12
|
+
actions:
|
|
13
|
+
- pagerduty-channel
|
|
14
|
+
priority: 100
|
|
15
|
+
|
|
16
|
+
- name: production_alerts
|
|
17
|
+
rule:
|
|
18
|
+
type: all_of
|
|
19
|
+
rules:
|
|
20
|
+
- type: tag
|
|
21
|
+
tags: ["production"]
|
|
22
|
+
- type: severity
|
|
23
|
+
min_severity: high
|
|
24
|
+
actions:
|
|
25
|
+
- slack-alerts
|
|
26
|
+
priority: 50
|
|
27
|
+
|
|
28
|
+
- name: default
|
|
29
|
+
rule:
|
|
30
|
+
type: always
|
|
31
|
+
actions:
|
|
32
|
+
- slack-general
|
|
33
|
+
priority: 0
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from .engine import ActionRouter, Route
|
|
42
|
+
from .rules import BaseRule
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RouteConfigParser:
|
|
46
|
+
"""Parser for routing configuration files.
|
|
47
|
+
|
|
48
|
+
Supports both YAML and JSON configuration formats.
|
|
49
|
+
|
|
50
|
+
Example usage:
|
|
51
|
+
# Parse from file
|
|
52
|
+
router = RouteConfigParser.from_file("routes.yaml")
|
|
53
|
+
|
|
54
|
+
# Parse from dict
|
|
55
|
+
router = RouteConfigParser.from_dict(config)
|
|
56
|
+
|
|
57
|
+
# Parse from YAML string
|
|
58
|
+
router = RouteConfigParser.from_yaml(yaml_string)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_file(cls, path: str | Path) -> ActionRouter:
|
|
63
|
+
"""Load routing configuration from file.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: Path to configuration file (.yaml, .yml, or .json).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Configured ActionRouter.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
FileNotFoundError: If file doesn't exist.
|
|
73
|
+
ValueError: If file format is unsupported.
|
|
74
|
+
"""
|
|
75
|
+
path = Path(path)
|
|
76
|
+
|
|
77
|
+
if not path.exists():
|
|
78
|
+
raise FileNotFoundError(f"Configuration file not found: {path}")
|
|
79
|
+
|
|
80
|
+
content = path.read_text()
|
|
81
|
+
|
|
82
|
+
if path.suffix in (".yaml", ".yml"):
|
|
83
|
+
return cls.from_yaml(content)
|
|
84
|
+
elif path.suffix == ".json":
|
|
85
|
+
return cls.from_json(content)
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unsupported file format: {path.suffix}")
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_yaml(cls, content: str) -> ActionRouter:
|
|
91
|
+
"""Parse routing configuration from YAML string.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
content: YAML configuration string.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Configured ActionRouter.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
import yaml
|
|
101
|
+
except ImportError:
|
|
102
|
+
raise ImportError(
|
|
103
|
+
"PyYAML is required for YAML config parsing. "
|
|
104
|
+
"Install with: pip install pyyaml"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
data = yaml.safe_load(content)
|
|
108
|
+
return cls.from_dict(data)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_json(cls, content: str) -> ActionRouter:
|
|
112
|
+
"""Parse routing configuration from JSON string.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
content: JSON configuration string.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Configured ActionRouter.
|
|
119
|
+
"""
|
|
120
|
+
import json
|
|
121
|
+
|
|
122
|
+
data = json.loads(content)
|
|
123
|
+
return cls.from_dict(data)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_dict(cls, data: dict[str, Any]) -> ActionRouter:
|
|
127
|
+
"""Parse routing configuration from dictionary.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
data: Configuration dictionary.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Configured ActionRouter.
|
|
134
|
+
"""
|
|
135
|
+
routes: list[Route] = []
|
|
136
|
+
default_route: Route | None = None
|
|
137
|
+
|
|
138
|
+
# Parse routes
|
|
139
|
+
routes_data = data.get("routes", data.get("routing", {}).get("routes", []))
|
|
140
|
+
for route_data in routes_data:
|
|
141
|
+
route = cls._parse_route(route_data)
|
|
142
|
+
if route:
|
|
143
|
+
routes.append(route)
|
|
144
|
+
|
|
145
|
+
# Parse default route
|
|
146
|
+
default_data = data.get("default_route") or data.get("routing", {}).get("default_route")
|
|
147
|
+
if default_data:
|
|
148
|
+
default_route = cls._parse_route(default_data)
|
|
149
|
+
|
|
150
|
+
return ActionRouter(routes=routes, default_route=default_route)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _parse_route(cls, data: dict[str, Any]) -> Route | None:
|
|
154
|
+
"""Parse a single route from configuration.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
data: Route configuration dictionary.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Parsed Route or None if invalid.
|
|
161
|
+
"""
|
|
162
|
+
rule_data = data.get("rule")
|
|
163
|
+
if not rule_data:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
rule = cls._parse_rule(rule_data)
|
|
167
|
+
if rule is None:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
return Route(
|
|
171
|
+
name=data.get("name", "unnamed"),
|
|
172
|
+
rule=rule,
|
|
173
|
+
actions=data.get("actions", []),
|
|
174
|
+
priority=data.get("priority", 0),
|
|
175
|
+
is_active=data.get("is_active", True),
|
|
176
|
+
escalation_policy_id=data.get("escalation_policy_id"),
|
|
177
|
+
stop_on_match=data.get("stop_on_match", False),
|
|
178
|
+
metadata=data.get("metadata", {}),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def _parse_rule(cls, data: dict[str, Any]) -> BaseRule | None:
|
|
183
|
+
"""Parse a rule from configuration.
|
|
184
|
+
|
|
185
|
+
Handles nested rules for combinators.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
data: Rule configuration dictionary.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Parsed rule or None if invalid.
|
|
192
|
+
"""
|
|
193
|
+
rule_type = data.get("type")
|
|
194
|
+
if not rule_type:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
# Handle combinator rules with nested rules
|
|
198
|
+
if rule_type in ("all_of", "any_of"):
|
|
199
|
+
nested_rules = []
|
|
200
|
+
for nested_data in data.get("rules", []):
|
|
201
|
+
nested_rule = cls._parse_rule(nested_data)
|
|
202
|
+
if nested_rule:
|
|
203
|
+
nested_rules.append(nested_rule)
|
|
204
|
+
|
|
205
|
+
from .combinators import AllOf, AnyOf
|
|
206
|
+
|
|
207
|
+
if rule_type == "all_of":
|
|
208
|
+
return AllOf(rules=nested_rules)
|
|
209
|
+
else:
|
|
210
|
+
return AnyOf(rules=nested_rules)
|
|
211
|
+
|
|
212
|
+
elif rule_type == "not":
|
|
213
|
+
nested_data = data.get("rule")
|
|
214
|
+
if nested_data:
|
|
215
|
+
nested_rule = cls._parse_rule(nested_data)
|
|
216
|
+
if nested_rule:
|
|
217
|
+
from .combinators import NotRule
|
|
218
|
+
|
|
219
|
+
return NotRule(rule=nested_rule)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
# Use registry for simple rules
|
|
223
|
+
return BaseRule.from_dict(data)
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def to_yaml(cls, router: ActionRouter) -> str:
|
|
227
|
+
"""Export router configuration to YAML string.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
router: ActionRouter to export.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
YAML configuration string.
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
import yaml
|
|
237
|
+
except ImportError:
|
|
238
|
+
raise ImportError(
|
|
239
|
+
"PyYAML is required for YAML config export. "
|
|
240
|
+
"Install with: pip install pyyaml"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
data = cls.to_dict(router)
|
|
244
|
+
return yaml.dump(data, default_flow_style=False, sort_keys=False)
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def to_json(cls, router: ActionRouter, indent: int = 2) -> str:
|
|
248
|
+
"""Export router configuration to JSON string.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
router: ActionRouter to export.
|
|
252
|
+
indent: JSON indentation level.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
JSON configuration string.
|
|
256
|
+
"""
|
|
257
|
+
import json
|
|
258
|
+
|
|
259
|
+
data = cls.to_dict(router)
|
|
260
|
+
return json.dumps(data, indent=indent)
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def to_dict(cls, router: ActionRouter) -> dict[str, Any]:
|
|
264
|
+
"""Export router configuration to dictionary.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
router: ActionRouter to export.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Configuration dictionary.
|
|
271
|
+
"""
|
|
272
|
+
return {
|
|
273
|
+
"routes": [route.to_dict() for route in router.routes],
|
|
274
|
+
"default_route": router.default_route.to_dict() if router.default_route else None,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def validate(cls, data: dict[str, Any]) -> list[str]:
|
|
279
|
+
"""Validate routing configuration.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
data: Configuration dictionary to validate.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of validation error messages (empty if valid).
|
|
286
|
+
"""
|
|
287
|
+
errors: list[str] = []
|
|
288
|
+
|
|
289
|
+
routes_data = data.get("routes", [])
|
|
290
|
+
if not isinstance(routes_data, list):
|
|
291
|
+
errors.append("'routes' must be a list")
|
|
292
|
+
return errors
|
|
293
|
+
|
|
294
|
+
route_names = set()
|
|
295
|
+
for i, route_data in enumerate(routes_data):
|
|
296
|
+
route_errors = cls._validate_route(route_data, i)
|
|
297
|
+
errors.extend(route_errors)
|
|
298
|
+
|
|
299
|
+
# Check for duplicate names
|
|
300
|
+
name = route_data.get("name")
|
|
301
|
+
if name:
|
|
302
|
+
if name in route_names:
|
|
303
|
+
errors.append(f"Route {i}: Duplicate route name '{name}'")
|
|
304
|
+
route_names.add(name)
|
|
305
|
+
|
|
306
|
+
return errors
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def _validate_route(cls, data: dict[str, Any], index: int) -> list[str]:
|
|
310
|
+
"""Validate a single route configuration."""
|
|
311
|
+
errors: list[str] = []
|
|
312
|
+
prefix = f"Route {index}"
|
|
313
|
+
|
|
314
|
+
if not isinstance(data, dict):
|
|
315
|
+
errors.append(f"{prefix}: Must be an object")
|
|
316
|
+
return errors
|
|
317
|
+
|
|
318
|
+
# Required fields
|
|
319
|
+
if "rule" not in data:
|
|
320
|
+
errors.append(f"{prefix}: Missing required field 'rule'")
|
|
321
|
+
|
|
322
|
+
if "actions" not in data:
|
|
323
|
+
errors.append(f"{prefix}: Missing required field 'actions'")
|
|
324
|
+
elif not isinstance(data["actions"], list):
|
|
325
|
+
errors.append(f"{prefix}: 'actions' must be a list")
|
|
326
|
+
elif not data["actions"]:
|
|
327
|
+
errors.append(f"{prefix}: 'actions' must not be empty")
|
|
328
|
+
|
|
329
|
+
# Validate rule
|
|
330
|
+
rule_data = data.get("rule", {})
|
|
331
|
+
rule_errors = cls._validate_rule(rule_data, prefix)
|
|
332
|
+
errors.extend(rule_errors)
|
|
333
|
+
|
|
334
|
+
return errors
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def _validate_rule(cls, data: dict[str, Any], prefix: str) -> list[str]:
|
|
338
|
+
"""Validate a rule configuration."""
|
|
339
|
+
from .rules import RuleRegistry
|
|
340
|
+
|
|
341
|
+
errors: list[str] = []
|
|
342
|
+
|
|
343
|
+
if not isinstance(data, dict):
|
|
344
|
+
errors.append(f"{prefix}: Rule must be an object")
|
|
345
|
+
return errors
|
|
346
|
+
|
|
347
|
+
rule_type = data.get("type")
|
|
348
|
+
if not rule_type:
|
|
349
|
+
errors.append(f"{prefix}: Rule missing required field 'type'")
|
|
350
|
+
return errors
|
|
351
|
+
|
|
352
|
+
# Check if rule type exists
|
|
353
|
+
if rule_type not in RuleRegistry.list_types():
|
|
354
|
+
errors.append(f"{prefix}: Unknown rule type '{rule_type}'")
|
|
355
|
+
return errors
|
|
356
|
+
|
|
357
|
+
# Validate nested rules for combinators
|
|
358
|
+
if rule_type in ("all_of", "any_of"):
|
|
359
|
+
nested_rules = data.get("rules", [])
|
|
360
|
+
if not isinstance(nested_rules, list):
|
|
361
|
+
errors.append(f"{prefix}: '{rule_type}' rules must be a list")
|
|
362
|
+
else:
|
|
363
|
+
for i, nested_data in enumerate(nested_rules):
|
|
364
|
+
nested_errors = cls._validate_rule(nested_data, f"{prefix}/{rule_type}[{i}]")
|
|
365
|
+
errors.extend(nested_errors)
|
|
366
|
+
|
|
367
|
+
elif rule_type == "not":
|
|
368
|
+
nested_data = data.get("rule")
|
|
369
|
+
if not nested_data:
|
|
370
|
+
errors.append(f"{prefix}: 'not' rule missing nested 'rule'")
|
|
371
|
+
else:
|
|
372
|
+
nested_errors = cls._validate_rule(nested_data, f"{prefix}/not")
|
|
373
|
+
errors.extend(nested_errors)
|
|
374
|
+
|
|
375
|
+
return errors
|