truthound-dashboard 1.4.4__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.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +437 -10
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +11 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.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
|