truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.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 +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -221,9 +221,53 @@ async def test_source_connection(
|
|
|
221
221
|
async def get_supported_types() -> dict:
|
|
222
222
|
"""Get list of supported source types.
|
|
223
223
|
|
|
224
|
+
Returns comprehensive information about each source type including
|
|
225
|
+
field definitions for dynamic form rendering.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of supported source types with field definitions.
|
|
229
|
+
"""
|
|
230
|
+
from truthound_dashboard.core.connections import (
|
|
231
|
+
get_source_type_categories,
|
|
232
|
+
get_supported_source_types,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
"success": True,
|
|
237
|
+
"data": {
|
|
238
|
+
"types": get_supported_source_types(),
|
|
239
|
+
"categories": get_source_type_categories(),
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@router.post(
|
|
245
|
+
"/test-connection",
|
|
246
|
+
response_model=dict,
|
|
247
|
+
summary="Test connection configuration",
|
|
248
|
+
description="Test a connection configuration before creating a source",
|
|
249
|
+
)
|
|
250
|
+
async def test_connection_config(
|
|
251
|
+
request: dict,
|
|
252
|
+
) -> dict:
|
|
253
|
+
"""Test connection configuration before creating a source.
|
|
254
|
+
|
|
255
|
+
This endpoint allows testing connection settings without
|
|
256
|
+
persisting them to the database.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
request: Dictionary with 'type' and 'config' keys.
|
|
260
|
+
|
|
224
261
|
Returns:
|
|
225
|
-
|
|
262
|
+
Connection test result with success status and message.
|
|
226
263
|
"""
|
|
227
|
-
from truthound_dashboard.core.connections import
|
|
264
|
+
from truthound_dashboard.core.connections import test_connection
|
|
265
|
+
|
|
266
|
+
source_type = request.get("type")
|
|
267
|
+
config = request.get("config", {})
|
|
228
268
|
|
|
229
|
-
|
|
269
|
+
if not source_type:
|
|
270
|
+
raise HTTPException(status_code=400, detail="Source type is required")
|
|
271
|
+
|
|
272
|
+
result = await test_connection(source_type, config)
|
|
273
|
+
return {"success": True, "data": result}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Trigger monitoring and webhook API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides endpoints for:
|
|
4
|
+
- Trigger monitoring status
|
|
5
|
+
- Webhook trigger reception
|
|
6
|
+
- Trigger status queries
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, Header, HTTPException
|
|
15
|
+
|
|
16
|
+
from truthound_dashboard.core.scheduler import get_scheduler
|
|
17
|
+
from truthound_dashboard.schemas.triggers import (
|
|
18
|
+
TriggerCheckStatus,
|
|
19
|
+
TriggerMonitoringResponse,
|
|
20
|
+
TriggerMonitoringStats,
|
|
21
|
+
WebhookTriggerRequest,
|
|
22
|
+
WebhookTriggerResponse,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
router = APIRouter(prefix="/triggers", tags=["triggers"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get(
|
|
31
|
+
"/monitoring",
|
|
32
|
+
response_model=TriggerMonitoringResponse,
|
|
33
|
+
summary="Get trigger monitoring status",
|
|
34
|
+
)
|
|
35
|
+
async def get_trigger_monitoring() -> TriggerMonitoringResponse:
|
|
36
|
+
"""Get current trigger monitoring status.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Trigger monitoring stats and schedule statuses.
|
|
40
|
+
"""
|
|
41
|
+
scheduler = get_scheduler()
|
|
42
|
+
status = scheduler.get_trigger_monitoring_status()
|
|
43
|
+
schedules = await scheduler.get_trigger_check_statuses()
|
|
44
|
+
|
|
45
|
+
# Calculate aggregate stats
|
|
46
|
+
active_data_change = sum(
|
|
47
|
+
1 for s in schedules if s["trigger_type"] == "data_change"
|
|
48
|
+
)
|
|
49
|
+
active_webhook = sum(
|
|
50
|
+
1 for s in schedules if s["trigger_type"] == "webhook"
|
|
51
|
+
)
|
|
52
|
+
active_composite = sum(
|
|
53
|
+
1 for s in schedules if s["trigger_type"] == "composite"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Calculate average check interval
|
|
57
|
+
check_intervals = [
|
|
58
|
+
s.get("check_interval_minutes", 5) * 60 for s in schedules
|
|
59
|
+
]
|
|
60
|
+
avg_interval = (
|
|
61
|
+
sum(check_intervals) / len(check_intervals)
|
|
62
|
+
if check_intervals
|
|
63
|
+
else 300.0
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Find next scheduled check
|
|
67
|
+
next_checks = [
|
|
68
|
+
s["next_check_at"] for s in schedules if s.get("next_check_at")
|
|
69
|
+
]
|
|
70
|
+
next_check = min(next_checks) if next_checks else None
|
|
71
|
+
|
|
72
|
+
stats = TriggerMonitoringStats(
|
|
73
|
+
total_schedules=len(schedules),
|
|
74
|
+
active_data_change_triggers=active_data_change,
|
|
75
|
+
active_webhook_triggers=active_webhook,
|
|
76
|
+
active_composite_triggers=active_composite,
|
|
77
|
+
total_checks_last_hour=status.get("checks_last_hour", 0),
|
|
78
|
+
total_triggers_last_hour=status.get("triggers_last_hour", 0),
|
|
79
|
+
average_check_interval_seconds=avg_interval,
|
|
80
|
+
next_scheduled_check_at=next_check,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
schedule_statuses = [
|
|
84
|
+
TriggerCheckStatus(**s) for s in schedules
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
return TriggerMonitoringResponse(
|
|
88
|
+
stats=stats,
|
|
89
|
+
schedules=schedule_statuses,
|
|
90
|
+
checker_running=status.get("checker_running", False),
|
|
91
|
+
checker_interval_seconds=status.get("checker_interval_seconds", 300),
|
|
92
|
+
last_checker_run_at=status.get("last_checker_run_at"),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.get(
|
|
97
|
+
"/schedules/{schedule_id}/status",
|
|
98
|
+
summary="Get trigger status for a specific schedule",
|
|
99
|
+
)
|
|
100
|
+
async def get_schedule_trigger_status(schedule_id: str) -> dict[str, Any]:
|
|
101
|
+
"""Get trigger status for a specific schedule.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
schedule_id: Schedule ID to query.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Trigger status for the schedule.
|
|
108
|
+
"""
|
|
109
|
+
scheduler = get_scheduler()
|
|
110
|
+
schedules = await scheduler.get_trigger_check_statuses()
|
|
111
|
+
|
|
112
|
+
for schedule in schedules:
|
|
113
|
+
if schedule["schedule_id"] == schedule_id:
|
|
114
|
+
return schedule
|
|
115
|
+
|
|
116
|
+
raise HTTPException(
|
|
117
|
+
status_code=404,
|
|
118
|
+
detail=f"Schedule {schedule_id} not found or not using monitored trigger type",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@router.post(
|
|
123
|
+
"/webhook",
|
|
124
|
+
response_model=WebhookTriggerResponse,
|
|
125
|
+
summary="Receive webhook trigger",
|
|
126
|
+
)
|
|
127
|
+
async def receive_webhook(
|
|
128
|
+
request: WebhookTriggerRequest,
|
|
129
|
+
x_webhook_signature: str | None = Header(
|
|
130
|
+
default=None, description="HMAC-SHA256 signature for verification"
|
|
131
|
+
),
|
|
132
|
+
) -> WebhookTriggerResponse:
|
|
133
|
+
"""Receive and process an incoming webhook trigger.
|
|
134
|
+
|
|
135
|
+
This endpoint is called by external systems (Airflow, Dagster, Prefect, etc.)
|
|
136
|
+
to trigger validations when data pipelines complete.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
request: Webhook trigger request.
|
|
140
|
+
x_webhook_signature: Optional signature for verification.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Webhook trigger response with triggered schedules.
|
|
144
|
+
"""
|
|
145
|
+
scheduler = get_scheduler()
|
|
146
|
+
|
|
147
|
+
result = await scheduler.trigger_webhook(
|
|
148
|
+
source=request.source,
|
|
149
|
+
event_type=request.event_type,
|
|
150
|
+
payload=request.payload,
|
|
151
|
+
schedule_id=request.schedule_id,
|
|
152
|
+
source_id=request.source_id,
|
|
153
|
+
signature=x_webhook_signature,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return WebhookTriggerResponse(
|
|
157
|
+
accepted=result["accepted"],
|
|
158
|
+
triggered_schedules=result["triggered_schedules"],
|
|
159
|
+
message=result["message"],
|
|
160
|
+
request_id=result["request_id"],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.post(
|
|
165
|
+
"/webhook/test",
|
|
166
|
+
summary="Test webhook configuration",
|
|
167
|
+
)
|
|
168
|
+
async def test_webhook(
|
|
169
|
+
source: str = "test",
|
|
170
|
+
event_type: str = "test_event",
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Test webhook endpoint without triggering any schedules.
|
|
173
|
+
|
|
174
|
+
Useful for verifying connectivity and configuration.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
source: Test source name.
|
|
178
|
+
event_type: Test event type.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Test result.
|
|
182
|
+
"""
|
|
183
|
+
return {
|
|
184
|
+
"success": True,
|
|
185
|
+
"message": "Webhook endpoint is accessible",
|
|
186
|
+
"received": {
|
|
187
|
+
"source": source,
|
|
188
|
+
"event_type": event_type,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
@@ -71,10 +71,23 @@ async def run_validation(
|
|
|
71
71
|
# Simple mode: use validator names list (backward compatible)
|
|
72
72
|
validators = request.validators
|
|
73
73
|
|
|
74
|
+
# Convert custom validators to internal format
|
|
75
|
+
custom_validators = None
|
|
76
|
+
if request.custom_validators:
|
|
77
|
+
custom_validators = [
|
|
78
|
+
{
|
|
79
|
+
"validator_id": cv.validator_id,
|
|
80
|
+
"column": cv.column,
|
|
81
|
+
"params": cv.params or {},
|
|
82
|
+
}
|
|
83
|
+
for cv in request.custom_validators
|
|
84
|
+
]
|
|
85
|
+
|
|
74
86
|
validation = await service.run_validation(
|
|
75
87
|
source_id,
|
|
76
88
|
validators=validators,
|
|
77
89
|
validator_params=validator_params,
|
|
90
|
+
custom_validators=custom_validators,
|
|
78
91
|
schema_path=request.schema_path,
|
|
79
92
|
auto_schema=request.auto_schema,
|
|
80
93
|
columns=request.columns,
|
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
"""Validators API endpoints.
|
|
2
2
|
|
|
3
3
|
This module provides API endpoints for validator discovery and configuration.
|
|
4
|
+
Includes both built-in truthound validators and user-defined custom validators.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
import logging
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from typing import Annotated, Any
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from truthound_dashboard.core.plugins import CustomValidatorExecutor
|
|
17
|
+
from truthound_dashboard.core.plugins.registry import plugin_registry
|
|
18
|
+
from truthound_dashboard.core.plugins.validator_executor import ValidatorContext
|
|
9
19
|
|
|
10
20
|
from ..schemas.validators import (
|
|
11
21
|
VALIDATOR_REGISTRY,
|
|
22
|
+
CustomValidatorExecuteRequest,
|
|
23
|
+
CustomValidatorExecuteResponse,
|
|
24
|
+
UnifiedValidatorDefinition,
|
|
25
|
+
UnifiedValidatorListResponse,
|
|
12
26
|
ValidatorCategory,
|
|
13
27
|
ValidatorDefinition,
|
|
28
|
+
ValidatorSource,
|
|
14
29
|
get_validator_by_name,
|
|
15
30
|
get_validators_by_category,
|
|
16
31
|
search_validators,
|
|
17
32
|
)
|
|
33
|
+
from .deps import SourceServiceDep, get_session
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
18
36
|
|
|
19
37
|
router = APIRouter()
|
|
20
38
|
|
|
39
|
+
# Dependencies
|
|
40
|
+
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
41
|
+
|
|
21
42
|
|
|
22
43
|
@router.get(
|
|
23
44
|
"/validators",
|
|
24
45
|
response_model=list[ValidatorDefinition],
|
|
25
|
-
summary="List
|
|
26
|
-
description="Returns all
|
|
46
|
+
summary="List built-in validators",
|
|
47
|
+
description="Returns all built-in validators with their parameter definitions.",
|
|
27
48
|
)
|
|
28
49
|
async def list_validators(
|
|
29
50
|
category: ValidatorCategory | None = Query(
|
|
@@ -33,7 +54,7 @@ async def list_validators(
|
|
|
33
54
|
default=None, description="Search by name, description, or tags"
|
|
34
55
|
),
|
|
35
56
|
) -> list[ValidatorDefinition]:
|
|
36
|
-
"""List
|
|
57
|
+
"""List built-in validators, optionally filtered.
|
|
37
58
|
|
|
38
59
|
Args:
|
|
39
60
|
category: Optional category filter.
|
|
@@ -49,6 +70,122 @@ async def list_validators(
|
|
|
49
70
|
return VALIDATOR_REGISTRY
|
|
50
71
|
|
|
51
72
|
|
|
73
|
+
@router.get(
|
|
74
|
+
"/validators/unified",
|
|
75
|
+
response_model=UnifiedValidatorListResponse,
|
|
76
|
+
summary="List all validators (built-in + custom)",
|
|
77
|
+
description="Returns unified list of both built-in and custom validators.",
|
|
78
|
+
)
|
|
79
|
+
async def list_unified_validators(
|
|
80
|
+
session: SessionDep,
|
|
81
|
+
category: str | None = Query(
|
|
82
|
+
default=None, description="Filter by category"
|
|
83
|
+
),
|
|
84
|
+
source: ValidatorSource | None = Query(
|
|
85
|
+
default=None, description="Filter by source (builtin or custom)"
|
|
86
|
+
),
|
|
87
|
+
search: str | None = Query(
|
|
88
|
+
default=None, description="Search by name, description, or tags"
|
|
89
|
+
),
|
|
90
|
+
enabled_only: bool = Query(
|
|
91
|
+
default=False, description="Only return enabled validators"
|
|
92
|
+
),
|
|
93
|
+
offset: int = Query(default=0, ge=0),
|
|
94
|
+
limit: int = Query(default=100, ge=1, le=500),
|
|
95
|
+
) -> UnifiedValidatorListResponse:
|
|
96
|
+
"""List all validators (built-in + custom).
|
|
97
|
+
|
|
98
|
+
This endpoint provides a unified view of all available validators,
|
|
99
|
+
combining truthound's built-in validators with user-defined custom validators.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
session: Database session.
|
|
103
|
+
category: Optional category filter.
|
|
104
|
+
source: Optional source filter (builtin or custom).
|
|
105
|
+
search: Optional search query.
|
|
106
|
+
enabled_only: Only return enabled validators.
|
|
107
|
+
offset: Pagination offset.
|
|
108
|
+
limit: Pagination limit.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Unified list of validators with metadata.
|
|
112
|
+
"""
|
|
113
|
+
unified_validators: list[UnifiedValidatorDefinition] = []
|
|
114
|
+
|
|
115
|
+
# 1. Get built-in validators
|
|
116
|
+
builtin_count = 0
|
|
117
|
+
if source is None or source == ValidatorSource.BUILTIN:
|
|
118
|
+
builtin_validators = VALIDATOR_REGISTRY
|
|
119
|
+
|
|
120
|
+
# Apply filters
|
|
121
|
+
if search:
|
|
122
|
+
search_lower = search.lower()
|
|
123
|
+
builtin_validators = [
|
|
124
|
+
v for v in builtin_validators
|
|
125
|
+
if (
|
|
126
|
+
search_lower in v.name.lower()
|
|
127
|
+
or search_lower in v.display_name.lower()
|
|
128
|
+
or search_lower in v.description.lower()
|
|
129
|
+
or any(search_lower in tag.lower() for tag in v.tags)
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
if category:
|
|
134
|
+
builtin_validators = [
|
|
135
|
+
v for v in builtin_validators
|
|
136
|
+
if v.category.value == category
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for v in builtin_validators:
|
|
140
|
+
unified_validators.append(UnifiedValidatorDefinition.from_builtin(v))
|
|
141
|
+
builtin_count = len(builtin_validators)
|
|
142
|
+
|
|
143
|
+
# 2. Get custom validators
|
|
144
|
+
custom_count = 0
|
|
145
|
+
if source is None or source == ValidatorSource.CUSTOM:
|
|
146
|
+
custom_validators, total_custom = await plugin_registry.list_validators(
|
|
147
|
+
session=session,
|
|
148
|
+
category=category,
|
|
149
|
+
enabled_only=enabled_only,
|
|
150
|
+
search=search,
|
|
151
|
+
offset=0,
|
|
152
|
+
limit=500, # Get all for now, apply pagination later
|
|
153
|
+
)
|
|
154
|
+
for cv in custom_validators:
|
|
155
|
+
unified_validators.append(UnifiedValidatorDefinition.from_custom(cv))
|
|
156
|
+
custom_count = len(custom_validators)
|
|
157
|
+
|
|
158
|
+
# Calculate category summary
|
|
159
|
+
category_counts: dict[str, dict[str, int]] = defaultdict(
|
|
160
|
+
lambda: {"builtin": 0, "custom": 0}
|
|
161
|
+
)
|
|
162
|
+
for v in unified_validators:
|
|
163
|
+
category_counts[v.category][v.source.value] += 1
|
|
164
|
+
|
|
165
|
+
categories = [
|
|
166
|
+
{
|
|
167
|
+
"name": cat,
|
|
168
|
+
"label": cat.replace("_", " ").title(),
|
|
169
|
+
"builtin_count": counts["builtin"],
|
|
170
|
+
"custom_count": counts["custom"],
|
|
171
|
+
"total": counts["builtin"] + counts["custom"],
|
|
172
|
+
}
|
|
173
|
+
for cat, counts in sorted(category_counts.items())
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
# Apply pagination
|
|
177
|
+
total = len(unified_validators)
|
|
178
|
+
paginated = unified_validators[offset : offset + limit]
|
|
179
|
+
|
|
180
|
+
return UnifiedValidatorListResponse(
|
|
181
|
+
data=paginated,
|
|
182
|
+
total=total,
|
|
183
|
+
builtin_count=builtin_count,
|
|
184
|
+
custom_count=custom_count,
|
|
185
|
+
categories=categories,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
52
189
|
@router.get(
|
|
53
190
|
"/validators/categories",
|
|
54
191
|
response_model=list[dict[str, str]],
|
|
@@ -83,3 +220,195 @@ async def get_validator(name: str) -> ValidatorDefinition | None:
|
|
|
83
220
|
Validator definition if found.
|
|
84
221
|
"""
|
|
85
222
|
return get_validator_by_name(name)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# =============================================================================
|
|
226
|
+
# Custom Validator Execution Endpoints
|
|
227
|
+
# =============================================================================
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@router.post(
|
|
231
|
+
"/validators/custom/{validator_id}/execute",
|
|
232
|
+
response_model=CustomValidatorExecuteResponse,
|
|
233
|
+
summary="Execute custom validator",
|
|
234
|
+
description="Execute a custom validator against a data source column.",
|
|
235
|
+
)
|
|
236
|
+
async def execute_custom_validator(
|
|
237
|
+
session: SessionDep,
|
|
238
|
+
source_service: SourceServiceDep,
|
|
239
|
+
validator_id: Annotated[str, Path(description="Custom validator ID")],
|
|
240
|
+
request: CustomValidatorExecuteRequest,
|
|
241
|
+
) -> CustomValidatorExecuteResponse:
|
|
242
|
+
"""Execute a custom validator on a specific data source and column.
|
|
243
|
+
|
|
244
|
+
This endpoint allows direct execution of a custom validator without
|
|
245
|
+
going through the full validation pipeline. Useful for testing and
|
|
246
|
+
one-off validations.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
session: Database session.
|
|
250
|
+
source_service: Source service for data access.
|
|
251
|
+
validator_id: ID of the custom validator.
|
|
252
|
+
request: Execution request with source_id, column_name, and params.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Execution result with validation status and issues.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
HTTPException: 404 if validator or source not found.
|
|
259
|
+
"""
|
|
260
|
+
# Get the custom validator
|
|
261
|
+
validator = await plugin_registry.get_validator(session, validator_id=validator_id)
|
|
262
|
+
if not validator:
|
|
263
|
+
raise HTTPException(
|
|
264
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
265
|
+
detail=f"Custom validator {validator_id} not found",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if not validator.is_enabled:
|
|
269
|
+
raise HTTPException(
|
|
270
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
271
|
+
detail=f"Custom validator {validator.name} is disabled",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Get the data source
|
|
275
|
+
source = await source_service.get_by_id(request.source_id)
|
|
276
|
+
if not source:
|
|
277
|
+
raise HTTPException(
|
|
278
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
279
|
+
detail=f"Data source {request.source_id} not found",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Load data from source
|
|
283
|
+
try:
|
|
284
|
+
import polars as pl
|
|
285
|
+
|
|
286
|
+
# Read data based on source type
|
|
287
|
+
if source.type == "csv":
|
|
288
|
+
df = pl.read_csv(source.path)
|
|
289
|
+
elif source.type == "parquet":
|
|
290
|
+
df = pl.read_parquet(source.path)
|
|
291
|
+
elif source.type == "json":
|
|
292
|
+
df = pl.read_json(source.path)
|
|
293
|
+
else:
|
|
294
|
+
raise HTTPException(
|
|
295
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
296
|
+
detail=f"Unsupported source type: {source.type}",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Check if column exists
|
|
300
|
+
if request.column_name not in df.columns:
|
|
301
|
+
raise HTTPException(
|
|
302
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
303
|
+
detail=f"Column '{request.column_name}' not found in source",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Apply sample size if specified
|
|
307
|
+
if request.sample_size and request.sample_size < len(df):
|
|
308
|
+
df = df.sample(request.sample_size)
|
|
309
|
+
|
|
310
|
+
# Get column values
|
|
311
|
+
column_values = df[request.column_name].to_list()
|
|
312
|
+
|
|
313
|
+
# Get column schema info
|
|
314
|
+
column_schema = {
|
|
315
|
+
"dtype": str(df[request.column_name].dtype),
|
|
316
|
+
"null_count": df[request.column_name].null_count(),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
except HTTPException:
|
|
320
|
+
raise
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"Failed to load data from source: {e}")
|
|
323
|
+
return CustomValidatorExecuteResponse(
|
|
324
|
+
success=False,
|
|
325
|
+
error=f"Failed to load data: {str(e)}",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Create execution context
|
|
329
|
+
context = ValidatorContext(
|
|
330
|
+
column_name=request.column_name,
|
|
331
|
+
column_values=column_values,
|
|
332
|
+
parameters=request.param_values,
|
|
333
|
+
schema=column_schema,
|
|
334
|
+
row_count=len(column_values),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Execute the validator
|
|
338
|
+
executor = CustomValidatorExecutor()
|
|
339
|
+
result = await executor.execute(
|
|
340
|
+
validator=validator,
|
|
341
|
+
context=context,
|
|
342
|
+
session=session,
|
|
343
|
+
source_id=request.source_id,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
await session.commit()
|
|
347
|
+
|
|
348
|
+
return CustomValidatorExecuteResponse(
|
|
349
|
+
success=True,
|
|
350
|
+
passed=result.passed,
|
|
351
|
+
execution_time_ms=result.execution_time_ms,
|
|
352
|
+
issues=result.issues,
|
|
353
|
+
message=result.message,
|
|
354
|
+
details=result.details,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@router.post(
|
|
359
|
+
"/validators/custom/{validator_id}/execute-preview",
|
|
360
|
+
response_model=CustomValidatorExecuteResponse,
|
|
361
|
+
summary="Preview custom validator execution",
|
|
362
|
+
description="Execute a custom validator on sample data for preview.",
|
|
363
|
+
)
|
|
364
|
+
async def preview_custom_validator_execution(
|
|
365
|
+
session: SessionDep,
|
|
366
|
+
validator_id: Annotated[str, Path(description="Custom validator ID")],
|
|
367
|
+
test_data: dict[str, Any],
|
|
368
|
+
) -> CustomValidatorExecuteResponse:
|
|
369
|
+
"""Preview custom validator execution with provided test data.
|
|
370
|
+
|
|
371
|
+
This endpoint allows testing a saved custom validator with arbitrary
|
|
372
|
+
test data without needing a data source.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
session: Database session.
|
|
376
|
+
validator_id: ID of the custom validator.
|
|
377
|
+
test_data: Test data containing column_name, values, and params.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Execution result with validation status and issues.
|
|
381
|
+
"""
|
|
382
|
+
# Get the custom validator
|
|
383
|
+
validator = await plugin_registry.get_validator(session, validator_id=validator_id)
|
|
384
|
+
if not validator:
|
|
385
|
+
raise HTTPException(
|
|
386
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
387
|
+
detail=f"Custom validator {validator_id} not found",
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Create context from test data
|
|
391
|
+
context = ValidatorContext(
|
|
392
|
+
column_name=test_data.get("column_name", "test_column"),
|
|
393
|
+
column_values=test_data.get("values", []),
|
|
394
|
+
parameters=test_data.get("params", {}),
|
|
395
|
+
schema=test_data.get("schema", {}),
|
|
396
|
+
row_count=len(test_data.get("values", [])),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Execute the validator (without logging)
|
|
400
|
+
executor = CustomValidatorExecutor(log_executions=False)
|
|
401
|
+
result = await executor.execute(
|
|
402
|
+
validator=validator,
|
|
403
|
+
context=context,
|
|
404
|
+
session=None,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return CustomValidatorExecuteResponse(
|
|
408
|
+
success=True,
|
|
409
|
+
passed=result.passed,
|
|
410
|
+
execution_time_ms=result.execution_time_ms,
|
|
411
|
+
issues=result.issues,
|
|
412
|
+
message=result.message,
|
|
413
|
+
details=result.details,
|
|
414
|
+
)
|