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,867 @@
|
|
|
1
|
+
"""YAML/JSON configuration parser for routing rules.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive configuration parsing for creating
|
|
4
|
+
routes and rules from YAML/JSON files or programmatic builders.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- RouteConfig and RoutingConfig dataclasses for structured configuration
|
|
8
|
+
- ConfigParser for parsing YAML/JSON configurations
|
|
9
|
+
- ConfigBuilder for programmatic configuration creation
|
|
10
|
+
- Nested rule support with combinators (all_of, any_of, not)
|
|
11
|
+
- Validation against RuleRegistry
|
|
12
|
+
|
|
13
|
+
Example YAML config:
|
|
14
|
+
version: "1.0"
|
|
15
|
+
defaults:
|
|
16
|
+
priority: 0
|
|
17
|
+
stop_on_match: false
|
|
18
|
+
routes:
|
|
19
|
+
- name: critical_alerts
|
|
20
|
+
rules:
|
|
21
|
+
- type: any_of
|
|
22
|
+
rules:
|
|
23
|
+
- type: severity
|
|
24
|
+
params:
|
|
25
|
+
min_level: critical
|
|
26
|
+
- type: all_of
|
|
27
|
+
rules:
|
|
28
|
+
- type: issue_count
|
|
29
|
+
params:
|
|
30
|
+
min_count: 10
|
|
31
|
+
- type: tag
|
|
32
|
+
params:
|
|
33
|
+
tags: ["production"]
|
|
34
|
+
actions: ["pagerduty", "slack-critical"]
|
|
35
|
+
priority: 100
|
|
36
|
+
|
|
37
|
+
Example usage:
|
|
38
|
+
# Parse from file
|
|
39
|
+
config = ConfigParser.parse_file(Path("routes.yaml"))
|
|
40
|
+
|
|
41
|
+
# Build programmatically
|
|
42
|
+
config = (
|
|
43
|
+
ConfigBuilder()
|
|
44
|
+
.with_defaults(priority=0)
|
|
45
|
+
.add_route(
|
|
46
|
+
name="critical",
|
|
47
|
+
rules=[{"type": "severity", "params": {"min_severity": "critical"}}],
|
|
48
|
+
actions=["pagerduty"],
|
|
49
|
+
priority=100,
|
|
50
|
+
)
|
|
51
|
+
.build()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Export to YAML/JSON
|
|
55
|
+
yaml_str = ConfigParser.to_yaml(config)
|
|
56
|
+
json_str = ConfigParser.to_json(config)
|
|
57
|
+
|
|
58
|
+
# Validate configuration
|
|
59
|
+
errors = ConfigParser.validate(config)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import json
|
|
65
|
+
from dataclasses import dataclass, field
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
68
|
+
|
|
69
|
+
if TYPE_CHECKING:
|
|
70
|
+
from .engine import ActionRouter
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class RouteConfig:
|
|
75
|
+
"""Configuration for a single routing rule.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
name: Unique route identifier.
|
|
79
|
+
rules: List of rule configurations (can be nested for combinators).
|
|
80
|
+
actions: List of action/channel identifiers to trigger on match.
|
|
81
|
+
priority: Route evaluation priority (higher = evaluated first).
|
|
82
|
+
stop_on_match: If True, stop evaluating lower priority routes on match.
|
|
83
|
+
metadata: Additional route metadata.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
name: str
|
|
87
|
+
rules: list[dict[str, Any]]
|
|
88
|
+
actions: list[str]
|
|
89
|
+
priority: int = 0
|
|
90
|
+
stop_on_match: bool = False
|
|
91
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
"""Serialize route configuration to dictionary."""
|
|
95
|
+
return {
|
|
96
|
+
"name": self.name,
|
|
97
|
+
"rules": self.rules,
|
|
98
|
+
"actions": self.actions,
|
|
99
|
+
"priority": self.priority,
|
|
100
|
+
"stop_on_match": self.stop_on_match,
|
|
101
|
+
"metadata": self.metadata,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(cls, data: dict[str, Any], defaults: dict[str, Any] | None = None) -> RouteConfig:
|
|
106
|
+
"""Create RouteConfig from dictionary.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
data: Route configuration dictionary.
|
|
110
|
+
defaults: Optional default values to apply.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
RouteConfig instance.
|
|
114
|
+
"""
|
|
115
|
+
defaults = defaults or {}
|
|
116
|
+
|
|
117
|
+
# Handle single rule vs rules list
|
|
118
|
+
rules = data.get("rules", [])
|
|
119
|
+
if "rule" in data and not rules:
|
|
120
|
+
# Support single rule format for backward compatibility
|
|
121
|
+
rules = [data["rule"]]
|
|
122
|
+
|
|
123
|
+
return cls(
|
|
124
|
+
name=data.get("name", "unnamed"),
|
|
125
|
+
rules=rules,
|
|
126
|
+
actions=data.get("actions", []),
|
|
127
|
+
priority=data.get("priority", defaults.get("priority", 0)),
|
|
128
|
+
stop_on_match=data.get("stop_on_match", defaults.get("stop_on_match", False)),
|
|
129
|
+
metadata=data.get("metadata", defaults.get("metadata", {})),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class RoutingConfig:
|
|
135
|
+
"""Full routing configuration.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
version: Configuration version string.
|
|
139
|
+
routes: List of route configurations.
|
|
140
|
+
defaults: Default values applied to routes.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
version: str = "1.0"
|
|
144
|
+
routes: list[RouteConfig] = field(default_factory=list)
|
|
145
|
+
defaults: dict[str, Any] = field(default_factory=dict)
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> dict[str, Any]:
|
|
148
|
+
"""Serialize routing configuration to dictionary."""
|
|
149
|
+
return {
|
|
150
|
+
"version": self.version,
|
|
151
|
+
"defaults": self.defaults,
|
|
152
|
+
"routes": [route.to_dict() for route in self.routes],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_dict(cls, data: dict[str, Any]) -> RoutingConfig:
|
|
157
|
+
"""Create RoutingConfig from dictionary.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
data: Configuration dictionary.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
RoutingConfig instance.
|
|
164
|
+
"""
|
|
165
|
+
version = data.get("version", "1.0")
|
|
166
|
+
defaults = data.get("defaults", {})
|
|
167
|
+
routes_data = data.get("routes", [])
|
|
168
|
+
|
|
169
|
+
routes = [RouteConfig.from_dict(route_data, defaults) for route_data in routes_data]
|
|
170
|
+
|
|
171
|
+
return cls(
|
|
172
|
+
version=version,
|
|
173
|
+
routes=routes,
|
|
174
|
+
defaults=defaults,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ConfigParser:
|
|
179
|
+
"""Parser for YAML/JSON routing configurations.
|
|
180
|
+
|
|
181
|
+
Provides methods for parsing, serializing, and validating
|
|
182
|
+
routing configurations from various formats.
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
# Parse from YAML string
|
|
186
|
+
config = ConfigParser.parse_yaml(yaml_content)
|
|
187
|
+
|
|
188
|
+
# Parse from JSON string
|
|
189
|
+
config = ConfigParser.parse_json(json_content)
|
|
190
|
+
|
|
191
|
+
# Parse from file (auto-detect format)
|
|
192
|
+
config = ConfigParser.parse_file(Path("routes.yaml"))
|
|
193
|
+
|
|
194
|
+
# Export to YAML
|
|
195
|
+
yaml_str = ConfigParser.to_yaml(config)
|
|
196
|
+
|
|
197
|
+
# Validate configuration
|
|
198
|
+
errors = ConfigParser.validate(config)
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def parse_yaml(cls, yaml_content: str) -> RoutingConfig:
|
|
203
|
+
"""Parse routing configuration from YAML string.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
yaml_content: YAML configuration string.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Parsed RoutingConfig.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
ImportError: If PyYAML is not installed.
|
|
213
|
+
ValueError: If YAML is invalid.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
import yaml
|
|
217
|
+
except ImportError as err:
|
|
218
|
+
raise ImportError(
|
|
219
|
+
"PyYAML is required for YAML config parsing. "
|
|
220
|
+
"Install with: pip install pyyaml"
|
|
221
|
+
) from err
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
data = yaml.safe_load(yaml_content)
|
|
225
|
+
except yaml.YAMLError as e:
|
|
226
|
+
raise ValueError(f"Invalid YAML: {e}") from e
|
|
227
|
+
|
|
228
|
+
if not isinstance(data, dict):
|
|
229
|
+
raise ValueError("YAML content must be a mapping")
|
|
230
|
+
|
|
231
|
+
return RoutingConfig.from_dict(data)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def parse_json(cls, json_content: str) -> RoutingConfig:
|
|
235
|
+
"""Parse routing configuration from JSON string.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
json_content: JSON configuration string.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Parsed RoutingConfig.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If JSON is invalid.
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
data = json.loads(json_content)
|
|
248
|
+
except json.JSONDecodeError as e:
|
|
249
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
250
|
+
|
|
251
|
+
if not isinstance(data, dict):
|
|
252
|
+
raise ValueError("JSON content must be an object")
|
|
253
|
+
|
|
254
|
+
return RoutingConfig.from_dict(data)
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def parse_file(cls, file_path: Path | str) -> RoutingConfig:
|
|
258
|
+
"""Parse routing configuration from file.
|
|
259
|
+
|
|
260
|
+
Automatically detects format based on file extension.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
file_path: Path to configuration file (.yaml, .yml, or .json).
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Parsed RoutingConfig.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
FileNotFoundError: If file doesn't exist.
|
|
270
|
+
ValueError: If file format is unsupported.
|
|
271
|
+
"""
|
|
272
|
+
path = Path(file_path)
|
|
273
|
+
|
|
274
|
+
if not path.exists():
|
|
275
|
+
raise FileNotFoundError(f"Configuration file not found: {path}")
|
|
276
|
+
|
|
277
|
+
content = path.read_text(encoding="utf-8")
|
|
278
|
+
|
|
279
|
+
if path.suffix in (".yaml", ".yml"):
|
|
280
|
+
return cls.parse_yaml(content)
|
|
281
|
+
elif path.suffix == ".json":
|
|
282
|
+
return cls.parse_json(content)
|
|
283
|
+
else:
|
|
284
|
+
raise ValueError(f"Unsupported file format: {path.suffix}")
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def to_yaml(cls, config: RoutingConfig) -> str:
|
|
288
|
+
"""Export routing configuration to YAML string.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
config: RoutingConfig to export.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
YAML configuration string.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ImportError: If PyYAML is not installed.
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
import yaml
|
|
301
|
+
except ImportError as err:
|
|
302
|
+
raise ImportError(
|
|
303
|
+
"PyYAML is required for YAML config export. "
|
|
304
|
+
"Install with: pip install pyyaml"
|
|
305
|
+
) from err
|
|
306
|
+
|
|
307
|
+
data = config.to_dict()
|
|
308
|
+
return yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def to_json(cls, config: RoutingConfig, indent: int = 2) -> str:
|
|
312
|
+
"""Export routing configuration to JSON string.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
config: RoutingConfig to export.
|
|
316
|
+
indent: JSON indentation level.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
JSON configuration string.
|
|
320
|
+
"""
|
|
321
|
+
data = config.to_dict()
|
|
322
|
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def validate(
|
|
326
|
+
cls,
|
|
327
|
+
config: RoutingConfig,
|
|
328
|
+
max_depth: int = 10,
|
|
329
|
+
max_rules_per_combinator: int = 50,
|
|
330
|
+
check_circular_refs: bool = True,
|
|
331
|
+
) -> list[str]:
|
|
332
|
+
"""Validate routing configuration.
|
|
333
|
+
|
|
334
|
+
Checks:
|
|
335
|
+
- Route names are unique
|
|
336
|
+
- Rule types exist in RuleRegistry
|
|
337
|
+
- Required fields are present
|
|
338
|
+
- Actions are not empty
|
|
339
|
+
- Circular references in rules
|
|
340
|
+
- Maximum nesting depth
|
|
341
|
+
- Maximum rules per combinator
|
|
342
|
+
- Reserved field names
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
config: RoutingConfig to validate.
|
|
346
|
+
max_depth: Maximum nesting depth for rules (default: 10).
|
|
347
|
+
max_rules_per_combinator: Maximum rules per combinator (default: 50).
|
|
348
|
+
check_circular_refs: Whether to check for circular references (default: True).
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
List of validation error messages (empty if valid).
|
|
352
|
+
"""
|
|
353
|
+
from .rules import RuleRegistry
|
|
354
|
+
from .validator import RuleValidationConfig, RuleValidator
|
|
355
|
+
|
|
356
|
+
errors: list[str] = []
|
|
357
|
+
|
|
358
|
+
# Check version
|
|
359
|
+
if not config.version:
|
|
360
|
+
errors.append("Missing 'version' field")
|
|
361
|
+
|
|
362
|
+
# Check for duplicate route names
|
|
363
|
+
route_names: set[str] = set()
|
|
364
|
+
for i, route in enumerate(config.routes):
|
|
365
|
+
if route.name in route_names:
|
|
366
|
+
errors.append(f"Route {i}: Duplicate route name '{route.name}'")
|
|
367
|
+
route_names.add(route.name)
|
|
368
|
+
|
|
369
|
+
# Validate each route structure
|
|
370
|
+
route_errors = cls._validate_route(route, i, RuleRegistry)
|
|
371
|
+
errors.extend(route_errors)
|
|
372
|
+
|
|
373
|
+
# Use RuleValidator for comprehensive validation of each rule
|
|
374
|
+
if route.rules:
|
|
375
|
+
validation_config = RuleValidationConfig(
|
|
376
|
+
max_depth=max_depth,
|
|
377
|
+
max_rules_per_combinator=max_rules_per_combinator,
|
|
378
|
+
check_circular_refs=check_circular_refs,
|
|
379
|
+
)
|
|
380
|
+
validator = RuleValidator(validation_config)
|
|
381
|
+
|
|
382
|
+
for j, rule_data in enumerate(route.rules):
|
|
383
|
+
result = validator.validate(rule_data)
|
|
384
|
+
if not result.valid:
|
|
385
|
+
for error in result.errors:
|
|
386
|
+
path_prefix = f"Route {i} ('{route.name}')/rules[{j}]"
|
|
387
|
+
if error.path:
|
|
388
|
+
errors.append(f"{path_prefix}/{error.path}: {error.message}")
|
|
389
|
+
else:
|
|
390
|
+
errors.append(f"{path_prefix}: {error.message}")
|
|
391
|
+
|
|
392
|
+
return errors
|
|
393
|
+
|
|
394
|
+
@classmethod
|
|
395
|
+
def _validate_route(cls, route: RouteConfig, index: int, registry: type) -> list[str]:
|
|
396
|
+
"""Validate a single route configuration.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
route: RouteConfig to validate.
|
|
400
|
+
index: Route index for error messages.
|
|
401
|
+
registry: RuleRegistry class for rule type validation.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
List of validation errors.
|
|
405
|
+
"""
|
|
406
|
+
errors: list[str] = []
|
|
407
|
+
prefix = f"Route {index} ('{route.name}')"
|
|
408
|
+
|
|
409
|
+
# Check required fields
|
|
410
|
+
if not route.name:
|
|
411
|
+
errors.append(f"{prefix}: Missing required field 'name'")
|
|
412
|
+
|
|
413
|
+
if not route.rules:
|
|
414
|
+
errors.append(f"{prefix}: Missing required field 'rules'")
|
|
415
|
+
else:
|
|
416
|
+
# Validate each rule in the rules list
|
|
417
|
+
for rule_idx, rule_data in enumerate(route.rules):
|
|
418
|
+
rule_errors = cls._validate_rule(rule_data, f"{prefix}/rules[{rule_idx}]", registry)
|
|
419
|
+
errors.extend(rule_errors)
|
|
420
|
+
|
|
421
|
+
if not route.actions:
|
|
422
|
+
errors.append(f"{prefix}: Missing required field 'actions'")
|
|
423
|
+
elif not isinstance(route.actions, list):
|
|
424
|
+
errors.append(f"{prefix}: 'actions' must be a list")
|
|
425
|
+
|
|
426
|
+
return errors
|
|
427
|
+
|
|
428
|
+
@classmethod
|
|
429
|
+
def _validate_rule(cls, data: dict[str, Any], prefix: str, registry: type) -> list[str]:
|
|
430
|
+
"""Validate a rule configuration recursively.
|
|
431
|
+
|
|
432
|
+
Handles nested rules for combinators (all_of, any_of, not).
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
data: Rule configuration dictionary.
|
|
436
|
+
prefix: Error message prefix.
|
|
437
|
+
registry: RuleRegistry class.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
List of validation errors.
|
|
441
|
+
"""
|
|
442
|
+
errors: list[str] = []
|
|
443
|
+
|
|
444
|
+
if not isinstance(data, dict):
|
|
445
|
+
errors.append(f"{prefix}: Rule must be an object")
|
|
446
|
+
return errors
|
|
447
|
+
|
|
448
|
+
rule_type = data.get("type")
|
|
449
|
+
if not rule_type:
|
|
450
|
+
errors.append(f"{prefix}: Rule missing required field 'type'")
|
|
451
|
+
return errors
|
|
452
|
+
|
|
453
|
+
# Check if rule type exists in registry
|
|
454
|
+
registered_types = registry.list_types()
|
|
455
|
+
if rule_type not in registered_types:
|
|
456
|
+
errors.append(f"{prefix}: Unknown rule type '{rule_type}'. Available: {registered_types}")
|
|
457
|
+
return errors
|
|
458
|
+
|
|
459
|
+
# Validate params if present
|
|
460
|
+
params = data.get("params", {})
|
|
461
|
+
if params and not isinstance(params, dict):
|
|
462
|
+
errors.append(f"{prefix}: 'params' must be an object")
|
|
463
|
+
|
|
464
|
+
# Validate nested rules for combinators
|
|
465
|
+
if rule_type in ("all_of", "any_of"):
|
|
466
|
+
nested_rules = data.get("rules", params.get("rules", []))
|
|
467
|
+
if not isinstance(nested_rules, list):
|
|
468
|
+
errors.append(f"{prefix}: '{rule_type}' requires 'rules' to be a list")
|
|
469
|
+
else:
|
|
470
|
+
for i, nested_data in enumerate(nested_rules):
|
|
471
|
+
nested_errors = cls._validate_rule(nested_data, f"{prefix}/{rule_type}[{i}]", registry)
|
|
472
|
+
errors.extend(nested_errors)
|
|
473
|
+
|
|
474
|
+
elif rule_type == "not":
|
|
475
|
+
nested_rule = data.get("rule", params.get("rule"))
|
|
476
|
+
if not nested_rule:
|
|
477
|
+
errors.append(f"{prefix}: 'not' rule requires nested 'rule' field")
|
|
478
|
+
elif isinstance(nested_rule, dict):
|
|
479
|
+
nested_errors = cls._validate_rule(nested_rule, f"{prefix}/not", registry)
|
|
480
|
+
errors.extend(nested_errors)
|
|
481
|
+
|
|
482
|
+
return errors
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def to_action_router(cls, config: RoutingConfig) -> "ActionRouter":
|
|
486
|
+
"""Convert RoutingConfig to ActionRouter.
|
|
487
|
+
|
|
488
|
+
Converts the parsed configuration into an ActionRouter
|
|
489
|
+
instance ready for event matching.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
config: RoutingConfig to convert.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Configured ActionRouter.
|
|
496
|
+
"""
|
|
497
|
+
from .combinators import AllOf, AnyOf, NotRule
|
|
498
|
+
from .engine import ActionRouter, Route
|
|
499
|
+
from .rules import BaseRule, RuleRegistry
|
|
500
|
+
|
|
501
|
+
routes: list[Route] = []
|
|
502
|
+
|
|
503
|
+
for route_config in config.routes:
|
|
504
|
+
# Build the rule from rules list
|
|
505
|
+
rule = cls._build_rule_from_list(route_config.rules)
|
|
506
|
+
if rule is None:
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
route = Route(
|
|
510
|
+
name=route_config.name,
|
|
511
|
+
rule=rule,
|
|
512
|
+
actions=route_config.actions,
|
|
513
|
+
priority=route_config.priority,
|
|
514
|
+
is_active=True,
|
|
515
|
+
stop_on_match=route_config.stop_on_match,
|
|
516
|
+
metadata=route_config.metadata,
|
|
517
|
+
)
|
|
518
|
+
routes.append(route)
|
|
519
|
+
|
|
520
|
+
return ActionRouter(routes=routes)
|
|
521
|
+
|
|
522
|
+
@classmethod
|
|
523
|
+
def _build_rule_from_list(cls, rules_list: list[dict[str, Any]]) -> "BaseRule | None":
|
|
524
|
+
"""Build a rule from a list of rule configurations.
|
|
525
|
+
|
|
526
|
+
If multiple rules, wraps them in AllOf combinator.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
rules_list: List of rule configurations.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
BaseRule instance or None if empty/invalid.
|
|
533
|
+
"""
|
|
534
|
+
from .combinators import AllOf
|
|
535
|
+
|
|
536
|
+
if not rules_list:
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
if len(rules_list) == 1:
|
|
540
|
+
return cls._build_rule(rules_list[0])
|
|
541
|
+
|
|
542
|
+
# Multiple rules -> combine with AllOf
|
|
543
|
+
rules = []
|
|
544
|
+
for rule_data in rules_list:
|
|
545
|
+
rule = cls._build_rule(rule_data)
|
|
546
|
+
if rule:
|
|
547
|
+
rules.append(rule)
|
|
548
|
+
|
|
549
|
+
if not rules:
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
return AllOf(rules=rules)
|
|
553
|
+
|
|
554
|
+
@classmethod
|
|
555
|
+
def _build_rule(cls, data: dict[str, Any]) -> "BaseRule | None":
|
|
556
|
+
"""Build a single rule from configuration.
|
|
557
|
+
|
|
558
|
+
Handles nested rules for combinators.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
data: Rule configuration dictionary.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
BaseRule instance or None if invalid.
|
|
565
|
+
"""
|
|
566
|
+
from .combinators import AllOf, AnyOf, NotRule
|
|
567
|
+
from .rules import BaseRule, RuleRegistry
|
|
568
|
+
|
|
569
|
+
rule_type = data.get("type")
|
|
570
|
+
if not rule_type:
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
# Get params (support both top-level and nested params)
|
|
574
|
+
params = data.get("params", {})
|
|
575
|
+
|
|
576
|
+
# Handle combinator rules with nested rules
|
|
577
|
+
if rule_type == "all_of":
|
|
578
|
+
nested_rules_data = data.get("rules", params.get("rules", []))
|
|
579
|
+
nested_rules = []
|
|
580
|
+
for nested_data in nested_rules_data:
|
|
581
|
+
nested_rule = cls._build_rule(nested_data)
|
|
582
|
+
if nested_rule:
|
|
583
|
+
nested_rules.append(nested_rule)
|
|
584
|
+
return AllOf(rules=nested_rules)
|
|
585
|
+
|
|
586
|
+
elif rule_type == "any_of":
|
|
587
|
+
nested_rules_data = data.get("rules", params.get("rules", []))
|
|
588
|
+
nested_rules = []
|
|
589
|
+
for nested_data in nested_rules_data:
|
|
590
|
+
nested_rule = cls._build_rule(nested_data)
|
|
591
|
+
if nested_rule:
|
|
592
|
+
nested_rules.append(nested_rule)
|
|
593
|
+
return AnyOf(rules=nested_rules)
|
|
594
|
+
|
|
595
|
+
elif rule_type == "not":
|
|
596
|
+
nested_data = data.get("rule", params.get("rule"))
|
|
597
|
+
if nested_data:
|
|
598
|
+
nested_rule = cls._build_rule(nested_data)
|
|
599
|
+
if nested_rule:
|
|
600
|
+
return NotRule(rule=nested_rule)
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
# Simple rules - merge top-level params with nested params
|
|
604
|
+
# Support both formats:
|
|
605
|
+
# 1. { type: "severity", min_severity: "critical" }
|
|
606
|
+
# 2. { type: "severity", params: { min_severity: "critical" } }
|
|
607
|
+
merged_params = {k: v for k, v in data.items() if k not in ("type", "params")}
|
|
608
|
+
merged_params.update(params)
|
|
609
|
+
|
|
610
|
+
return RuleRegistry.create(rule_type, **merged_params)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class ConfigBuilder:
|
|
614
|
+
"""Builder for programmatically creating routing configurations.
|
|
615
|
+
|
|
616
|
+
Provides a fluent interface for constructing RoutingConfig
|
|
617
|
+
instances without manual dictionary manipulation.
|
|
618
|
+
|
|
619
|
+
Example:
|
|
620
|
+
config = (
|
|
621
|
+
ConfigBuilder()
|
|
622
|
+
.with_version("1.0")
|
|
623
|
+
.with_defaults(priority=0, stop_on_match=False)
|
|
624
|
+
.add_route(
|
|
625
|
+
name="critical_alerts",
|
|
626
|
+
rules=[
|
|
627
|
+
{"type": "severity", "params": {"min_severity": "critical"}}
|
|
628
|
+
],
|
|
629
|
+
actions=["pagerduty"],
|
|
630
|
+
priority=100,
|
|
631
|
+
)
|
|
632
|
+
.add_route(
|
|
633
|
+
name="production_errors",
|
|
634
|
+
rules=[
|
|
635
|
+
{
|
|
636
|
+
"type": "all_of",
|
|
637
|
+
"rules": [
|
|
638
|
+
{"type": "tag", "params": {"tags": ["production"]}},
|
|
639
|
+
{"type": "status", "params": {"statuses": ["error"]}},
|
|
640
|
+
],
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
actions=["slack-alerts"],
|
|
644
|
+
priority=50,
|
|
645
|
+
)
|
|
646
|
+
.build()
|
|
647
|
+
)
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
def __init__(self) -> None:
|
|
651
|
+
"""Initialize the builder."""
|
|
652
|
+
self._version: str = "1.0"
|
|
653
|
+
self._defaults: dict[str, Any] = {}
|
|
654
|
+
self._routes: list[RouteConfig] = []
|
|
655
|
+
|
|
656
|
+
def with_version(self, version: str) -> Self:
|
|
657
|
+
"""Set the configuration version.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
version: Version string.
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
Self for method chaining.
|
|
664
|
+
"""
|
|
665
|
+
self._version = version
|
|
666
|
+
return self
|
|
667
|
+
|
|
668
|
+
def with_defaults(self, **kwargs: Any) -> Self:
|
|
669
|
+
"""Set default values for routes.
|
|
670
|
+
|
|
671
|
+
Supported defaults:
|
|
672
|
+
- priority: Default route priority
|
|
673
|
+
- stop_on_match: Default stop_on_match behavior
|
|
674
|
+
- metadata: Default metadata to apply
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
**kwargs: Default values.
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
Self for method chaining.
|
|
681
|
+
"""
|
|
682
|
+
self._defaults.update(kwargs)
|
|
683
|
+
return self
|
|
684
|
+
|
|
685
|
+
def add_route(
|
|
686
|
+
self,
|
|
687
|
+
name: str,
|
|
688
|
+
rules: list[dict[str, Any]],
|
|
689
|
+
actions: list[str],
|
|
690
|
+
priority: int | None = None,
|
|
691
|
+
stop_on_match: bool | None = None,
|
|
692
|
+
metadata: dict[str, Any] | None = None,
|
|
693
|
+
) -> Self:
|
|
694
|
+
"""Add a route to the configuration.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
name: Unique route name.
|
|
698
|
+
rules: List of rule configurations.
|
|
699
|
+
actions: List of action/channel identifiers.
|
|
700
|
+
priority: Route priority (uses default if None).
|
|
701
|
+
stop_on_match: Stop on match behavior (uses default if None).
|
|
702
|
+
metadata: Route metadata (uses default if None).
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
Self for method chaining.
|
|
706
|
+
"""
|
|
707
|
+
route = RouteConfig(
|
|
708
|
+
name=name,
|
|
709
|
+
rules=rules,
|
|
710
|
+
actions=actions,
|
|
711
|
+
priority=priority if priority is not None else self._defaults.get("priority", 0),
|
|
712
|
+
stop_on_match=(
|
|
713
|
+
stop_on_match if stop_on_match is not None else self._defaults.get("stop_on_match", False)
|
|
714
|
+
),
|
|
715
|
+
metadata=metadata if metadata is not None else dict(self._defaults.get("metadata", {})),
|
|
716
|
+
)
|
|
717
|
+
self._routes.append(route)
|
|
718
|
+
return self
|
|
719
|
+
|
|
720
|
+
def add_simple_route(
|
|
721
|
+
self,
|
|
722
|
+
name: str,
|
|
723
|
+
rule_type: str,
|
|
724
|
+
rule_params: dict[str, Any],
|
|
725
|
+
actions: list[str],
|
|
726
|
+
priority: int | None = None,
|
|
727
|
+
stop_on_match: bool | None = None,
|
|
728
|
+
) -> Self:
|
|
729
|
+
"""Add a simple route with a single rule.
|
|
730
|
+
|
|
731
|
+
Convenience method for routes with a single rule.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
name: Unique route name.
|
|
735
|
+
rule_type: Rule type (e.g., "severity", "tag").
|
|
736
|
+
rule_params: Rule parameters.
|
|
737
|
+
actions: List of action/channel identifiers.
|
|
738
|
+
priority: Route priority.
|
|
739
|
+
stop_on_match: Stop on match behavior.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
Self for method chaining.
|
|
743
|
+
"""
|
|
744
|
+
rules = [{"type": rule_type, "params": rule_params}]
|
|
745
|
+
return self.add_route(
|
|
746
|
+
name=name,
|
|
747
|
+
rules=rules,
|
|
748
|
+
actions=actions,
|
|
749
|
+
priority=priority,
|
|
750
|
+
stop_on_match=stop_on_match,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
def add_combined_route(
|
|
754
|
+
self,
|
|
755
|
+
name: str,
|
|
756
|
+
combinator: str,
|
|
757
|
+
rule_configs: list[tuple[str, dict[str, Any]]],
|
|
758
|
+
actions: list[str],
|
|
759
|
+
priority: int | None = None,
|
|
760
|
+
stop_on_match: bool | None = None,
|
|
761
|
+
) -> Self:
|
|
762
|
+
"""Add a route with combined rules.
|
|
763
|
+
|
|
764
|
+
Convenience method for routes with multiple rules combined
|
|
765
|
+
by a combinator (all_of, any_of).
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
name: Unique route name.
|
|
769
|
+
combinator: Combinator type ("all_of" or "any_of").
|
|
770
|
+
rule_configs: List of (rule_type, params) tuples.
|
|
771
|
+
actions: List of action/channel identifiers.
|
|
772
|
+
priority: Route priority.
|
|
773
|
+
stop_on_match: Stop on match behavior.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
Self for method chaining.
|
|
777
|
+
"""
|
|
778
|
+
nested_rules = [{"type": rtype, "params": params} for rtype, params in rule_configs]
|
|
779
|
+
rules = [{"type": combinator, "rules": nested_rules}]
|
|
780
|
+
return self.add_route(
|
|
781
|
+
name=name,
|
|
782
|
+
rules=rules,
|
|
783
|
+
actions=actions,
|
|
784
|
+
priority=priority,
|
|
785
|
+
stop_on_match=stop_on_match,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
def clear_routes(self) -> Self:
|
|
789
|
+
"""Clear all routes from the builder.
|
|
790
|
+
|
|
791
|
+
Returns:
|
|
792
|
+
Self for method chaining.
|
|
793
|
+
"""
|
|
794
|
+
self._routes.clear()
|
|
795
|
+
return self
|
|
796
|
+
|
|
797
|
+
def remove_route(self, name: str) -> Self:
|
|
798
|
+
"""Remove a route by name.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
name: Route name to remove.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
Self for method chaining.
|
|
805
|
+
"""
|
|
806
|
+
self._routes = [r for r in self._routes if r.name != name]
|
|
807
|
+
return self
|
|
808
|
+
|
|
809
|
+
def build(self) -> RoutingConfig:
|
|
810
|
+
"""Build the RoutingConfig.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Configured RoutingConfig instance.
|
|
814
|
+
"""
|
|
815
|
+
return RoutingConfig(
|
|
816
|
+
version=self._version,
|
|
817
|
+
routes=list(self._routes),
|
|
818
|
+
defaults=dict(self._defaults),
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
def build_router(self) -> "ActionRouter":
|
|
822
|
+
"""Build and convert to ActionRouter.
|
|
823
|
+
|
|
824
|
+
Convenience method that builds the config and
|
|
825
|
+
converts it to an ActionRouter in one step.
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
Configured ActionRouter instance.
|
|
829
|
+
"""
|
|
830
|
+
config = self.build()
|
|
831
|
+
return ConfigParser.to_action_router(config)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
# Convenience function for quick parsing
|
|
835
|
+
def parse_routing_config(source: str | Path | dict[str, Any]) -> RoutingConfig:
|
|
836
|
+
"""Parse routing configuration from various sources.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
source: Configuration source - can be:
|
|
840
|
+
- Path to a file
|
|
841
|
+
- YAML or JSON string
|
|
842
|
+
- Dictionary
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
Parsed RoutingConfig.
|
|
846
|
+
|
|
847
|
+
Raises:
|
|
848
|
+
ValueError: If source format cannot be determined.
|
|
849
|
+
"""
|
|
850
|
+
if isinstance(source, Path):
|
|
851
|
+
return ConfigParser.parse_file(source)
|
|
852
|
+
elif isinstance(source, str):
|
|
853
|
+
# Check if it's a file path
|
|
854
|
+
path = Path(source)
|
|
855
|
+
if path.exists():
|
|
856
|
+
return ConfigParser.parse_file(path)
|
|
857
|
+
|
|
858
|
+
# Try parsing as YAML/JSON
|
|
859
|
+
source = source.strip()
|
|
860
|
+
if source.startswith("{"):
|
|
861
|
+
return ConfigParser.parse_json(source)
|
|
862
|
+
else:
|
|
863
|
+
return ConfigParser.parse_yaml(source)
|
|
864
|
+
elif isinstance(source, dict):
|
|
865
|
+
return RoutingConfig.from_dict(source)
|
|
866
|
+
else:
|
|
867
|
+
raise ValueError(f"Unsupported source type: {type(source)}")
|