truthound-dashboard 1.4.3__py3-none-any.whl → 1.5.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.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,676 @@
1
+ """Checkpoint interfaces for validation pipeline orchestration.
2
+
3
+ A Checkpoint represents a complete data validation pipeline that
4
+ combines data sources, validators, actions, triggers, and routing.
5
+
6
+ This module defines abstract interfaces for checkpoints that are
7
+ loosely coupled from truthound's checkpoint module.
8
+
9
+ Checkpoint features:
10
+ - Data source binding
11
+ - Validator configuration
12
+ - Action orchestration
13
+ - Trigger management
14
+ - Result routing
15
+ - Run history tracking
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from abc import ABC, abstractmethod
21
+ from dataclasses import dataclass, field
22
+ from datetime import datetime
23
+ from enum import Enum
24
+ from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
25
+
26
+ if TYPE_CHECKING:
27
+ from truthound_dashboard.core.interfaces.actions import (
28
+ ActionConfig,
29
+ ActionContext,
30
+ ActionResult,
31
+ )
32
+ from truthound_dashboard.core.interfaces.routing import Route, RouteContext, Router
33
+ from truthound_dashboard.core.interfaces.triggers import TriggerConfig, TriggerResult
34
+
35
+
36
+ class CheckpointStatus(str, Enum):
37
+ """Status of a checkpoint run."""
38
+
39
+ PENDING = "pending" # Not yet started
40
+ RUNNING = "running" # Currently executing
41
+ SUCCESS = "success" # Validation passed
42
+ FAILURE = "failure" # Validation failed (issues found)
43
+ ERROR = "error" # System error occurred
44
+ WARNING = "warning" # Passed with warnings
45
+ SKIPPED = "skipped" # Skipped (e.g., no data)
46
+ TIMEOUT = "timeout" # Execution timed out
47
+
48
+
49
+ @dataclass
50
+ class CheckpointConfig:
51
+ """Configuration for a checkpoint.
52
+
53
+ Attributes:
54
+ name: Checkpoint name for identification.
55
+ description: Human-readable description.
56
+ source_id: ID of the data source to validate.
57
+ source_name: Name of the data source.
58
+ validators: List of validator names to run.
59
+ validator_config: Per-validator configuration.
60
+ schema_path: Path to schema file for validation.
61
+ auto_schema: Auto-learn schema for validation.
62
+ tags: Tags for categorization and routing.
63
+ enabled: Whether this checkpoint is enabled.
64
+ timeout_seconds: Maximum execution time.
65
+ retry_on_error: Retry on system errors.
66
+ retry_count: Number of retries.
67
+ metadata: Additional metadata.
68
+ success_threshold: Minimum pass rate for success.
69
+ warning_threshold: Pass rate below which is warning.
70
+ """
71
+
72
+ name: str = ""
73
+ description: str = ""
74
+ source_id: str | None = None
75
+ source_name: str | None = None
76
+ validators: list[str] = field(default_factory=list)
77
+ validator_config: dict[str, dict[str, Any]] = field(default_factory=dict)
78
+ schema_path: str | None = None
79
+ auto_schema: bool = False
80
+ tags: dict[str, str] = field(default_factory=dict)
81
+ enabled: bool = True
82
+ timeout_seconds: int = 300
83
+ retry_on_error: bool = False
84
+ retry_count: int = 0
85
+ metadata: dict[str, Any] = field(default_factory=dict)
86
+ success_threshold: float = 1.0 # 100% pass = success
87
+ warning_threshold: float = 0.9 # 90% pass = warning
88
+
89
+ def to_dict(self) -> dict[str, Any]:
90
+ """Convert to dictionary."""
91
+ return {
92
+ "name": self.name,
93
+ "description": self.description,
94
+ "source_id": self.source_id,
95
+ "source_name": self.source_name,
96
+ "validators": self.validators,
97
+ "validator_config": self.validator_config,
98
+ "schema_path": self.schema_path,
99
+ "auto_schema": self.auto_schema,
100
+ "tags": self.tags,
101
+ "enabled": self.enabled,
102
+ "timeout_seconds": self.timeout_seconds,
103
+ "retry_on_error": self.retry_on_error,
104
+ "retry_count": self.retry_count,
105
+ "metadata": self.metadata,
106
+ "success_threshold": self.success_threshold,
107
+ "warning_threshold": self.warning_threshold,
108
+ }
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: dict[str, Any]) -> "CheckpointConfig":
112
+ """Create from dictionary."""
113
+ return cls(
114
+ name=data.get("name", ""),
115
+ description=data.get("description", ""),
116
+ source_id=data.get("source_id"),
117
+ source_name=data.get("source_name"),
118
+ validators=data.get("validators", []),
119
+ validator_config=data.get("validator_config", {}),
120
+ schema_path=data.get("schema_path"),
121
+ auto_schema=data.get("auto_schema", False),
122
+ tags=data.get("tags", {}),
123
+ enabled=data.get("enabled", True),
124
+ timeout_seconds=data.get("timeout_seconds", 300),
125
+ retry_on_error=data.get("retry_on_error", False),
126
+ retry_count=data.get("retry_count", 0),
127
+ metadata=data.get("metadata", {}),
128
+ success_threshold=data.get("success_threshold", 1.0),
129
+ warning_threshold=data.get("warning_threshold", 0.9),
130
+ )
131
+
132
+
133
+ @dataclass
134
+ class CheckpointResult:
135
+ """Result of a checkpoint run.
136
+
137
+ Attributes:
138
+ checkpoint_name: Name of the checkpoint.
139
+ run_id: Unique run identifier.
140
+ status: Execution status.
141
+ started_at: When execution started.
142
+ completed_at: When execution completed.
143
+ duration_ms: Execution duration in milliseconds.
144
+ source_name: Data source name.
145
+ row_count: Number of rows validated.
146
+ column_count: Number of columns.
147
+ issue_count: Total number of issues.
148
+ critical_count: Number of critical issues.
149
+ high_count: Number of high severity issues.
150
+ medium_count: Number of medium severity issues.
151
+ low_count: Number of low severity issues.
152
+ has_critical: Whether critical issues were found.
153
+ has_high: Whether high severity issues were found.
154
+ issues: List of validation issues.
155
+ action_results: Results from action execution.
156
+ trigger_context: Context from the trigger (if any).
157
+ error_message: Error message if status is ERROR.
158
+ metadata: Additional result metadata.
159
+ """
160
+
161
+ checkpoint_name: str
162
+ run_id: str
163
+ status: CheckpointStatus
164
+ started_at: datetime | None = None
165
+ completed_at: datetime | None = None
166
+ duration_ms: float = 0.0
167
+ source_name: str = ""
168
+ row_count: int = 0
169
+ column_count: int = 0
170
+ issue_count: int = 0
171
+ critical_count: int = 0
172
+ high_count: int = 0
173
+ medium_count: int = 0
174
+ low_count: int = 0
175
+ has_critical: bool = False
176
+ has_high: bool = False
177
+ issues: list[dict[str, Any]] = field(default_factory=list)
178
+ action_results: list["ActionResult"] = field(default_factory=list)
179
+ trigger_context: dict[str, Any] = field(default_factory=dict)
180
+ error_message: str | None = None
181
+ metadata: dict[str, Any] = field(default_factory=dict)
182
+
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """Convert to dictionary."""
185
+ return {
186
+ "checkpoint_name": self.checkpoint_name,
187
+ "run_id": self.run_id,
188
+ "status": self.status.value,
189
+ "started_at": self.started_at.isoformat() if self.started_at else None,
190
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
191
+ "duration_ms": self.duration_ms,
192
+ "source_name": self.source_name,
193
+ "row_count": self.row_count,
194
+ "column_count": self.column_count,
195
+ "issue_count": self.issue_count,
196
+ "critical_count": self.critical_count,
197
+ "high_count": self.high_count,
198
+ "medium_count": self.medium_count,
199
+ "low_count": self.low_count,
200
+ "has_critical": self.has_critical,
201
+ "has_high": self.has_high,
202
+ "issues": self.issues,
203
+ "action_results": [r.to_dict() for r in self.action_results],
204
+ "trigger_context": self.trigger_context,
205
+ "error_message": self.error_message,
206
+ "metadata": self.metadata,
207
+ }
208
+
209
+ @classmethod
210
+ def from_check_result(
211
+ cls,
212
+ check_result: Any,
213
+ checkpoint_name: str,
214
+ run_id: str,
215
+ config: CheckpointConfig,
216
+ ) -> "CheckpointResult":
217
+ """Create from a check result (validation result).
218
+
219
+ Args:
220
+ check_result: Validation result from backend.
221
+ checkpoint_name: Checkpoint name.
222
+ run_id: Run identifier.
223
+ config: Checkpoint configuration.
224
+
225
+ Returns:
226
+ CheckpointResult instance.
227
+ """
228
+ # Determine status based on result
229
+ passed = getattr(check_result, "passed", True)
230
+ has_critical = getattr(check_result, "has_critical", False)
231
+ has_high = getattr(check_result, "has_high", False)
232
+ issue_count = getattr(check_result, "total_issues", 0)
233
+
234
+ if has_critical or has_high:
235
+ status = CheckpointStatus.FAILURE
236
+ elif not passed:
237
+ # Check if warning threshold
238
+ row_count = getattr(check_result, "row_count", 0)
239
+ if row_count > 0:
240
+ pass_rate = 1 - (issue_count / row_count)
241
+ if pass_rate >= config.warning_threshold:
242
+ status = CheckpointStatus.WARNING
243
+ else:
244
+ status = CheckpointStatus.FAILURE
245
+ else:
246
+ status = CheckpointStatus.FAILURE
247
+ else:
248
+ status = CheckpointStatus.SUCCESS
249
+
250
+ # Extract issue counts
251
+ critical_count = getattr(check_result, "critical_issues", 0)
252
+ high_count = getattr(check_result, "high_issues", 0)
253
+ medium_count = getattr(check_result, "medium_issues", 0)
254
+ low_count = getattr(check_result, "low_issues", 0)
255
+
256
+ # Get issues list
257
+ issues = []
258
+ if hasattr(check_result, "issues"):
259
+ issues = check_result.issues
260
+ if not isinstance(issues, list):
261
+ issues = list(issues)
262
+
263
+ return cls(
264
+ checkpoint_name=checkpoint_name,
265
+ run_id=run_id,
266
+ status=status,
267
+ source_name=getattr(check_result, "source", ""),
268
+ row_count=getattr(check_result, "row_count", 0),
269
+ column_count=getattr(check_result, "column_count", 0),
270
+ issue_count=issue_count,
271
+ critical_count=critical_count,
272
+ high_count=high_count,
273
+ medium_count=medium_count,
274
+ low_count=low_count,
275
+ has_critical=has_critical,
276
+ has_high=has_high,
277
+ issues=issues,
278
+ )
279
+
280
+
281
+ @runtime_checkable
282
+ class CheckpointProtocol(Protocol):
283
+ """Protocol for checkpoint implementations.
284
+
285
+ A checkpoint orchestrates the complete validation pipeline:
286
+ 1. Load data from source
287
+ 2. Run validators
288
+ 3. Evaluate routing rules
289
+ 4. Execute matched actions
290
+ 5. Record results
291
+
292
+ Example:
293
+ checkpoint = Checkpoint(
294
+ config=CheckpointConfig(
295
+ name="daily_orders",
296
+ source_id="orders_db",
297
+ validators=["null", "uniqueness", "range"],
298
+ ),
299
+ actions=[slack_action, email_action],
300
+ routes=[critical_route, daily_summary_route],
301
+ )
302
+
303
+ result = await checkpoint.run()
304
+ """
305
+
306
+ @property
307
+ def name(self) -> str:
308
+ """Get checkpoint name."""
309
+ ...
310
+
311
+ @property
312
+ def config(self) -> CheckpointConfig:
313
+ """Get checkpoint configuration."""
314
+ ...
315
+
316
+ @property
317
+ def actions(self) -> list[Any]:
318
+ """Get configured actions."""
319
+ ...
320
+
321
+ @property
322
+ def triggers(self) -> list[Any]:
323
+ """Get configured triggers."""
324
+ ...
325
+
326
+ @property
327
+ def router(self) -> "Router | None":
328
+ """Get the router for action routing."""
329
+ ...
330
+
331
+ async def run(
332
+ self,
333
+ trigger_context: dict[str, Any] | None = None,
334
+ ) -> CheckpointResult:
335
+ """Run the checkpoint.
336
+
337
+ Args:
338
+ trigger_context: Optional context from the trigger.
339
+
340
+ Returns:
341
+ Checkpoint result.
342
+ """
343
+ ...
344
+
345
+ def add_action(self, action: Any) -> None:
346
+ """Add an action to the checkpoint.
347
+
348
+ Args:
349
+ action: Action to add.
350
+ """
351
+ ...
352
+
353
+ def remove_action(self, name: str) -> bool:
354
+ """Remove an action by name.
355
+
356
+ Args:
357
+ name: Action name.
358
+
359
+ Returns:
360
+ True if action was removed.
361
+ """
362
+ ...
363
+
364
+ def add_trigger(self, trigger: Any) -> None:
365
+ """Add a trigger to the checkpoint.
366
+
367
+ Args:
368
+ trigger: Trigger to add.
369
+ """
370
+ ...
371
+
372
+ def set_router(self, router: "Router") -> None:
373
+ """Set the router for action routing.
374
+
375
+ Args:
376
+ router: Router to use.
377
+ """
378
+ ...
379
+
380
+
381
+ class CheckpointRunnerProtocol(Protocol):
382
+ """Protocol for checkpoint runners.
383
+
384
+ Runners execute checkpoints, handling lifecycle, error recovery,
385
+ and result persistence.
386
+
387
+ Example:
388
+ runner = CheckpointRunner()
389
+ runner.register(checkpoint)
390
+
391
+ # Run by name
392
+ result = await runner.run("daily_orders")
393
+
394
+ # Run all enabled checkpoints
395
+ results = await runner.run_all()
396
+ """
397
+
398
+ def register(self, checkpoint: CheckpointProtocol) -> None:
399
+ """Register a checkpoint.
400
+
401
+ Args:
402
+ checkpoint: Checkpoint to register.
403
+ """
404
+ ...
405
+
406
+ def unregister(self, name: str) -> bool:
407
+ """Unregister a checkpoint by name.
408
+
409
+ Args:
410
+ name: Checkpoint name.
411
+
412
+ Returns:
413
+ True if checkpoint was unregistered.
414
+ """
415
+ ...
416
+
417
+ def get(self, name: str) -> CheckpointProtocol | None:
418
+ """Get a checkpoint by name.
419
+
420
+ Args:
421
+ name: Checkpoint name.
422
+
423
+ Returns:
424
+ Checkpoint or None if not found.
425
+ """
426
+ ...
427
+
428
+ def list_checkpoints(self) -> list[str]:
429
+ """List all registered checkpoint names.
430
+
431
+ Returns:
432
+ List of checkpoint names.
433
+ """
434
+ ...
435
+
436
+ async def run(
437
+ self,
438
+ name: str,
439
+ trigger_context: dict[str, Any] | None = None,
440
+ ) -> CheckpointResult:
441
+ """Run a checkpoint by name.
442
+
443
+ Args:
444
+ name: Checkpoint name.
445
+ trigger_context: Optional trigger context.
446
+
447
+ Returns:
448
+ Checkpoint result.
449
+
450
+ Raises:
451
+ KeyError: If checkpoint not found.
452
+ """
453
+ ...
454
+
455
+ async def run_all(
456
+ self,
457
+ parallel: bool = False,
458
+ max_workers: int = 4,
459
+ ) -> list[CheckpointResult]:
460
+ """Run all enabled checkpoints.
461
+
462
+ Args:
463
+ parallel: Run checkpoints in parallel.
464
+ max_workers: Max parallel workers.
465
+
466
+ Returns:
467
+ List of checkpoint results.
468
+ """
469
+ ...
470
+
471
+
472
+ # =============================================================================
473
+ # Checkpoint Registry
474
+ # =============================================================================
475
+
476
+
477
+ class CheckpointRegistry:
478
+ """Registry for checkpoint types and instances.
479
+
480
+ Manages checkpoint registration, lifecycle, and access.
481
+
482
+ Example:
483
+ registry = CheckpointRegistry()
484
+ registry.register(checkpoint)
485
+
486
+ result = await registry.run("daily_orders")
487
+ """
488
+
489
+ def __init__(self) -> None:
490
+ """Initialize registry."""
491
+ self._checkpoints: dict[str, CheckpointProtocol] = {}
492
+ self._factories: dict[str, Callable[..., CheckpointProtocol]] = {}
493
+
494
+ def register(self, checkpoint: CheckpointProtocol) -> None:
495
+ """Register a checkpoint instance.
496
+
497
+ Args:
498
+ checkpoint: Checkpoint to register.
499
+ """
500
+ self._checkpoints[checkpoint.name] = checkpoint
501
+
502
+ def register_factory(
503
+ self,
504
+ name: str,
505
+ factory: Callable[..., CheckpointProtocol],
506
+ ) -> None:
507
+ """Register a checkpoint factory.
508
+
509
+ Args:
510
+ name: Checkpoint type name.
511
+ factory: Factory function.
512
+ """
513
+ self._factories[name] = factory
514
+
515
+ def unregister(self, name: str) -> bool:
516
+ """Unregister a checkpoint by name.
517
+
518
+ Args:
519
+ name: Checkpoint name.
520
+
521
+ Returns:
522
+ True if checkpoint was unregistered.
523
+ """
524
+ if name in self._checkpoints:
525
+ del self._checkpoints[name]
526
+ return True
527
+ return False
528
+
529
+ def get(self, name: str) -> CheckpointProtocol | None:
530
+ """Get a checkpoint by name.
531
+
532
+ Args:
533
+ name: Checkpoint name.
534
+
535
+ Returns:
536
+ Checkpoint or None if not found.
537
+ """
538
+ return self._checkpoints.get(name)
539
+
540
+ def create(
541
+ self,
542
+ type_name: str,
543
+ config: CheckpointConfig | dict[str, Any] | None = None,
544
+ **kwargs: Any,
545
+ ) -> CheckpointProtocol:
546
+ """Create a checkpoint using a factory.
547
+
548
+ Args:
549
+ type_name: Checkpoint type name.
550
+ config: Checkpoint configuration.
551
+ **kwargs: Additional arguments.
552
+
553
+ Returns:
554
+ Checkpoint instance.
555
+
556
+ Raises:
557
+ KeyError: If factory not found.
558
+ """
559
+ if type_name not in self._factories:
560
+ raise KeyError(f"Checkpoint factory not found: {type_name}")
561
+ return self._factories[type_name](config=config, **kwargs)
562
+
563
+ def list_checkpoints(self) -> list[str]:
564
+ """List all registered checkpoint names.
565
+
566
+ Returns:
567
+ List of checkpoint names.
568
+ """
569
+ return list(self._checkpoints.keys())
570
+
571
+ def list_factories(self) -> list[str]:
572
+ """List all registered factory names.
573
+
574
+ Returns:
575
+ List of factory names.
576
+ """
577
+ return list(self._factories.keys())
578
+
579
+ async def run(
580
+ self,
581
+ name: str,
582
+ trigger_context: dict[str, Any] | None = None,
583
+ ) -> CheckpointResult:
584
+ """Run a checkpoint by name.
585
+
586
+ Args:
587
+ name: Checkpoint name.
588
+ trigger_context: Optional trigger context.
589
+
590
+ Returns:
591
+ Checkpoint result.
592
+
593
+ Raises:
594
+ KeyError: If checkpoint not found.
595
+ """
596
+ checkpoint = self.get(name)
597
+ if checkpoint is None:
598
+ raise KeyError(f"Checkpoint not found: {name}")
599
+ return await checkpoint.run(trigger_context=trigger_context)
600
+
601
+ async def run_all(
602
+ self,
603
+ parallel: bool = False,
604
+ max_workers: int = 4,
605
+ ) -> list[CheckpointResult]:
606
+ """Run all enabled checkpoints.
607
+
608
+ Args:
609
+ parallel: Run in parallel.
610
+ max_workers: Max parallel workers.
611
+
612
+ Returns:
613
+ List of checkpoint results.
614
+ """
615
+ import asyncio
616
+
617
+ results: list[CheckpointResult] = []
618
+ enabled_checkpoints = [
619
+ cp for cp in self._checkpoints.values()
620
+ if cp.config.enabled
621
+ ]
622
+
623
+ if not enabled_checkpoints:
624
+ return results
625
+
626
+ if parallel:
627
+ # Run in parallel with semaphore
628
+ semaphore = asyncio.Semaphore(max_workers)
629
+
630
+ async def run_with_semaphore(cp: CheckpointProtocol) -> CheckpointResult:
631
+ async with semaphore:
632
+ return await cp.run()
633
+
634
+ results = await asyncio.gather(
635
+ *[run_with_semaphore(cp) for cp in enabled_checkpoints]
636
+ )
637
+ else:
638
+ # Run sequentially
639
+ for checkpoint in enabled_checkpoints:
640
+ result = await checkpoint.run()
641
+ results.append(result)
642
+
643
+ return results
644
+
645
+
646
+ # Global checkpoint registry
647
+ _checkpoint_registry: CheckpointRegistry | None = None
648
+
649
+
650
+ def get_checkpoint_registry() -> CheckpointRegistry:
651
+ """Get the global checkpoint registry.
652
+
653
+ Returns:
654
+ Global CheckpointRegistry instance.
655
+ """
656
+ global _checkpoint_registry
657
+ if _checkpoint_registry is None:
658
+ _checkpoint_registry = CheckpointRegistry()
659
+ return _checkpoint_registry
660
+
661
+
662
+ def register_checkpoint(name: str | None = None) -> Callable[[type], type]:
663
+ """Decorator to register a checkpoint factory.
664
+
665
+ Example:
666
+ @register_checkpoint("custom")
667
+ class CustomCheckpoint:
668
+ ...
669
+ """
670
+
671
+ def decorator(cls: type) -> type:
672
+ factory_name = name or cls.__name__
673
+ get_checkpoint_registry().register_factory(factory_name, cls)
674
+ return cls
675
+
676
+ return decorator