truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,382 @@
1
+ """Routing engine for notification dispatch.
2
+
3
+ This module provides the core routing engine that evaluates events
4
+ against configured routes and determines target channels.
5
+
6
+ Components:
7
+ - RouteContext: Holds event data and metadata for rule evaluation
8
+ - Route: Defines a route with rule, actions, and priority
9
+ - RoutingResult: Result of route matching
10
+ - ActionRouter: Main routing engine
11
+
12
+ Example:
13
+ # Create routes
14
+ routes = [
15
+ Route(
16
+ name="critical_alerts",
17
+ rule=SeverityRule(min_severity="critical"),
18
+ actions=["pagerduty-channel"],
19
+ priority=100,
20
+ ),
21
+ Route(
22
+ name="default",
23
+ rule=AlwaysRule(),
24
+ actions=["slack-channel"],
25
+ priority=0,
26
+ ),
27
+ ]
28
+
29
+ # Create router
30
+ router = ActionRouter(routes=routes)
31
+
32
+ # Match event
33
+ context = RouteContext(event=validation_event)
34
+ result = await router.match(context)
35
+
36
+ for route in result.matched_routes:
37
+ print(f"Matched: {route.name} -> {route.actions}")
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ from dataclasses import dataclass, field
43
+ from datetime import datetime
44
+ from typing import TYPE_CHECKING, Any
45
+
46
+ from .rules import BaseRule
47
+
48
+ if TYPE_CHECKING:
49
+ from truthound_dashboard.core.notifications.base import NotificationEvent
50
+
51
+
52
+ @dataclass
53
+ class RouteContext:
54
+ """Context for route evaluation.
55
+
56
+ Holds the event data and additional metadata that rules
57
+ can use to make matching decisions.
58
+
59
+ Attributes:
60
+ event: The notification event being routed.
61
+ metadata: Additional context metadata.
62
+ timestamp: When routing is being evaluated.
63
+ """
64
+
65
+ event: "NotificationEvent"
66
+ metadata: dict[str, Any] = field(default_factory=dict)
67
+ timestamp: datetime = field(default_factory=datetime.utcnow)
68
+
69
+ def get_severity(self) -> str | None:
70
+ """Get event severity if available."""
71
+ # Try event data first
72
+ if hasattr(self.event, "severity"):
73
+ return self.event.severity
74
+ if hasattr(self.event, "has_critical") and self.event.has_critical:
75
+ return "critical"
76
+ if hasattr(self.event, "has_high") and self.event.has_high:
77
+ return "high"
78
+
79
+ # Try metadata
80
+ return self.metadata.get("severity")
81
+
82
+ def get_issue_count(self) -> int | None:
83
+ """Get issue count if available."""
84
+ if hasattr(self.event, "total_issues"):
85
+ return self.event.total_issues
86
+ return self.metadata.get("issue_count")
87
+
88
+ def get_pass_rate(self) -> float | None:
89
+ """Get validation pass rate if available."""
90
+ if hasattr(self.event, "pass_rate"):
91
+ return self.event.pass_rate
92
+ return self.metadata.get("pass_rate")
93
+
94
+ def get_tags(self) -> list[str]:
95
+ """Get context tags."""
96
+ tags = list(self.metadata.get("tags", []))
97
+
98
+ # Add event-derived tags
99
+ if self.event.source_name:
100
+ tags.append(f"source:{self.event.source_name}")
101
+ if self.event.event_type:
102
+ tags.append(f"type:{self.event.event_type}")
103
+
104
+ return tags
105
+
106
+ def get_data_asset(self) -> str | None:
107
+ """Get data asset name/path."""
108
+ if self.event.source_name:
109
+ return self.event.source_name
110
+ return self.metadata.get("data_asset")
111
+
112
+ def get_metadata(self, key: str) -> Any:
113
+ """Get metadata value by key."""
114
+ # Check event data first
115
+ if hasattr(self.event, "data") and key in self.event.data:
116
+ return self.event.data[key]
117
+ return self.metadata.get(key)
118
+
119
+ def get_status(self) -> str | None:
120
+ """Get validation status."""
121
+ if hasattr(self.event, "status"):
122
+ return self.event.status
123
+ # Infer from event type
124
+ if self.event.event_type in ("validation_failed", "schedule_failed"):
125
+ return "failure"
126
+ return self.metadata.get("status")
127
+
128
+ def get_error_message(self) -> str | None:
129
+ """Get error message if available."""
130
+ if hasattr(self.event, "error_message"):
131
+ return self.event.error_message
132
+ return self.metadata.get("error_message")
133
+
134
+
135
+ @dataclass
136
+ class Route:
137
+ """A routing rule with associated actions.
138
+
139
+ Attributes:
140
+ name: Unique route name.
141
+ rule: The rule to evaluate.
142
+ actions: List of channel IDs to notify.
143
+ priority: Route priority (higher = evaluated first).
144
+ is_active: Whether route is active.
145
+ escalation_policy_id: Optional escalation policy to trigger.
146
+ stop_on_match: If True, stop evaluating lower priority routes.
147
+ metadata: Additional route metadata.
148
+ """
149
+
150
+ name: str
151
+ rule: BaseRule
152
+ actions: list[str]
153
+ priority: int = 0
154
+ is_active: bool = True
155
+ escalation_policy_id: str | None = None
156
+ stop_on_match: bool = False
157
+ metadata: dict[str, Any] = field(default_factory=dict)
158
+
159
+ def to_dict(self) -> dict[str, Any]:
160
+ """Serialize route to dictionary."""
161
+ return {
162
+ "name": self.name,
163
+ "rule": self.rule.to_dict(),
164
+ "actions": self.actions,
165
+ "priority": self.priority,
166
+ "is_active": self.is_active,
167
+ "escalation_policy_id": self.escalation_policy_id,
168
+ "stop_on_match": self.stop_on_match,
169
+ "metadata": self.metadata,
170
+ }
171
+
172
+ @classmethod
173
+ def from_dict(cls, data: dict[str, Any]) -> "Route | None":
174
+ """Create Route from dictionary."""
175
+ rule_data = data.get("rule")
176
+ if not rule_data:
177
+ return None
178
+
179
+ rule = BaseRule.from_dict(rule_data)
180
+ if rule is None:
181
+ return None
182
+
183
+ return cls(
184
+ name=data.get("name", "unnamed"),
185
+ rule=rule,
186
+ actions=data.get("actions", []),
187
+ priority=data.get("priority", 0),
188
+ is_active=data.get("is_active", True),
189
+ escalation_policy_id=data.get("escalation_policy_id"),
190
+ stop_on_match=data.get("stop_on_match", False),
191
+ metadata=data.get("metadata", {}),
192
+ )
193
+
194
+
195
+ @dataclass
196
+ class RoutingResult:
197
+ """Result of route evaluation.
198
+
199
+ Attributes:
200
+ matched_routes: Routes that matched the context.
201
+ all_actions: Deduplicated list of all action channel IDs.
202
+ evaluation_time_ms: Time taken to evaluate routes.
203
+ context: The evaluated context.
204
+ """
205
+
206
+ matched_routes: list[Route]
207
+ all_actions: list[str]
208
+ evaluation_time_ms: float
209
+ context: RouteContext
210
+
211
+ @property
212
+ def has_matches(self) -> bool:
213
+ """Check if any routes matched."""
214
+ return len(self.matched_routes) > 0
215
+
216
+
217
+ class ActionRouter:
218
+ """Main routing engine.
219
+
220
+ Evaluates events against configured routes and returns
221
+ matching routes sorted by priority.
222
+
223
+ Routes are evaluated in priority order (highest first).
224
+ If a route has `stop_on_match=True`, lower priority routes
225
+ are skipped once it matches.
226
+
227
+ Attributes:
228
+ routes: List of configured routes.
229
+ default_route: Optional fallback route if nothing matches.
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ routes: list[Route] | None = None,
235
+ default_route: Route | None = None,
236
+ ) -> None:
237
+ """Initialize the router.
238
+
239
+ Args:
240
+ routes: List of routes to evaluate.
241
+ default_route: Fallback route if nothing matches.
242
+ """
243
+ self.routes = routes or []
244
+ self.default_route = default_route
245
+ self._sort_routes()
246
+
247
+ def _sort_routes(self) -> None:
248
+ """Sort routes by priority (highest first)."""
249
+ self.routes.sort(key=lambda r: r.priority, reverse=True)
250
+
251
+ def add_route(self, route: Route) -> None:
252
+ """Add a route to the router.
253
+
254
+ Args:
255
+ route: Route to add.
256
+ """
257
+ self.routes.append(route)
258
+ self._sort_routes()
259
+
260
+ def remove_route(self, name: str) -> bool:
261
+ """Remove a route by name.
262
+
263
+ Args:
264
+ name: Route name to remove.
265
+
266
+ Returns:
267
+ True if route was found and removed.
268
+ """
269
+ for i, route in enumerate(self.routes):
270
+ if route.name == name:
271
+ del self.routes[i]
272
+ return True
273
+ return False
274
+
275
+ def get_route(self, name: str) -> Route | None:
276
+ """Get a route by name.
277
+
278
+ Args:
279
+ name: Route name.
280
+
281
+ Returns:
282
+ Route if found, None otherwise.
283
+ """
284
+ for route in self.routes:
285
+ if route.name == name:
286
+ return route
287
+ return None
288
+
289
+ async def match(self, context: RouteContext) -> RoutingResult:
290
+ """Evaluate all routes against the context.
291
+
292
+ Args:
293
+ context: The routing context to evaluate.
294
+
295
+ Returns:
296
+ RoutingResult with matched routes and actions.
297
+ """
298
+ import time
299
+
300
+ start_time = time.perf_counter()
301
+ matched_routes: list[Route] = []
302
+ all_actions: set[str] = set()
303
+
304
+ for route in self.routes:
305
+ # Skip inactive routes
306
+ if not route.is_active:
307
+ continue
308
+
309
+ # Evaluate rule
310
+ try:
311
+ if await route.rule.matches(context):
312
+ matched_routes.append(route)
313
+ all_actions.update(route.actions)
314
+
315
+ # Stop if this route has stop_on_match
316
+ if route.stop_on_match:
317
+ break
318
+ except Exception:
319
+ # Log error but continue with other routes
320
+ continue
321
+
322
+ # Use default route if nothing matched
323
+ if not matched_routes and self.default_route:
324
+ if self.default_route.is_active:
325
+ try:
326
+ if await self.default_route.rule.matches(context):
327
+ matched_routes.append(self.default_route)
328
+ all_actions.update(self.default_route.actions)
329
+ except Exception:
330
+ pass
331
+
332
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
333
+
334
+ return RoutingResult(
335
+ matched_routes=matched_routes,
336
+ all_actions=list(all_actions),
337
+ evaluation_time_ms=elapsed_ms,
338
+ context=context,
339
+ )
340
+
341
+ async def get_channels_for_event(
342
+ self,
343
+ event: "NotificationEvent",
344
+ metadata: dict[str, Any] | None = None,
345
+ ) -> list[str]:
346
+ """Convenience method to get channel IDs for an event.
347
+
348
+ Args:
349
+ event: The notification event.
350
+ metadata: Optional additional metadata.
351
+
352
+ Returns:
353
+ List of channel IDs to notify.
354
+ """
355
+ context = RouteContext(
356
+ event=event,
357
+ metadata=metadata or {},
358
+ )
359
+ result = await self.match(context)
360
+ return result.all_actions
361
+
362
+ def to_dict(self) -> dict[str, Any]:
363
+ """Serialize router configuration."""
364
+ return {
365
+ "routes": [r.to_dict() for r in self.routes],
366
+ "default_route": self.default_route.to_dict() if self.default_route else None,
367
+ }
368
+
369
+ @classmethod
370
+ def from_dict(cls, data: dict[str, Any]) -> "ActionRouter":
371
+ """Create router from dictionary."""
372
+ routes = []
373
+ for route_data in data.get("routes", []):
374
+ route = Route.from_dict(route_data)
375
+ if route:
376
+ routes.append(route)
377
+
378
+ default_route = None
379
+ if data.get("default_route"):
380
+ default_route = Route.from_dict(data["default_route"])
381
+
382
+ return cls(routes=routes, default_route=default_route)