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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Custom Validator Executor.
|
|
2
|
+
|
|
3
|
+
This module provides execution of custom validators in a safe,
|
|
4
|
+
sandboxed environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
16
|
+
|
|
17
|
+
from truthound_dashboard.db.models import CustomValidator, PluginExecutionLog
|
|
18
|
+
|
|
19
|
+
from .sandbox import create_sandbox, SandboxConfig, SandboxResult
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ValidatorResult:
|
|
26
|
+
"""Result of custom validator execution.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
passed: Whether validation passed.
|
|
30
|
+
issues: List of validation issues found.
|
|
31
|
+
message: Summary message.
|
|
32
|
+
details: Additional details (JSON-serializable).
|
|
33
|
+
execution_time_ms: Execution time in milliseconds.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
passed: bool
|
|
37
|
+
issues: list[dict[str, Any]] = field(default_factory=list)
|
|
38
|
+
message: str = ""
|
|
39
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
execution_time_ms: float = 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ValidatorContext:
|
|
45
|
+
"""Context provided to custom validators.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
column_name: Name of the column being validated.
|
|
49
|
+
column_values: List of values in the column.
|
|
50
|
+
parameters: Validator parameters.
|
|
51
|
+
schema: Column schema information.
|
|
52
|
+
row_count: Total number of rows.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
column_name: str
|
|
56
|
+
column_values: list[Any]
|
|
57
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
schema: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
row_count: int = 0
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict[str, Any]:
|
|
62
|
+
"""Convert to dictionary for sandbox execution."""
|
|
63
|
+
return {
|
|
64
|
+
"column_name": self.column_name,
|
|
65
|
+
"values": self.column_values,
|
|
66
|
+
"params": self.parameters,
|
|
67
|
+
"schema": self.schema,
|
|
68
|
+
"row_count": self.row_count,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Template code that wraps custom validator code
|
|
73
|
+
VALIDATOR_WRAPPER_CODE = '''
|
|
74
|
+
# Custom validator wrapper
|
|
75
|
+
import re
|
|
76
|
+
import math
|
|
77
|
+
import statistics
|
|
78
|
+
from datetime import datetime, date
|
|
79
|
+
from collections import Counter
|
|
80
|
+
|
|
81
|
+
# User-provided validator code
|
|
82
|
+
{user_code}
|
|
83
|
+
|
|
84
|
+
# Entry point
|
|
85
|
+
def _execute_validator(column_name, values, params, schema, row_count):
|
|
86
|
+
"""Execute the custom validator.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
column_name: Name of the column.
|
|
90
|
+
values: List of column values.
|
|
91
|
+
params: Validator parameters.
|
|
92
|
+
schema: Column schema.
|
|
93
|
+
row_count: Total row count.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary with 'passed', 'issues', 'message', 'details'.
|
|
97
|
+
"""
|
|
98
|
+
# Call the user-defined validate function
|
|
99
|
+
result = validate(column_name, values, params, schema, row_count)
|
|
100
|
+
|
|
101
|
+
# Ensure result is properly formatted
|
|
102
|
+
if isinstance(result, bool):
|
|
103
|
+
return {{
|
|
104
|
+
"passed": result,
|
|
105
|
+
"issues": [],
|
|
106
|
+
"message": "Validation passed" if result else "Validation failed",
|
|
107
|
+
"details": {{}}
|
|
108
|
+
}}
|
|
109
|
+
elif isinstance(result, dict):
|
|
110
|
+
return {{
|
|
111
|
+
"passed": result.get("passed", False),
|
|
112
|
+
"issues": result.get("issues", []),
|
|
113
|
+
"message": result.get("message", ""),
|
|
114
|
+
"details": result.get("details", {{}})
|
|
115
|
+
}}
|
|
116
|
+
else:
|
|
117
|
+
return {{
|
|
118
|
+
"passed": False,
|
|
119
|
+
"issues": [],
|
|
120
|
+
"message": f"Invalid result type: {{type(result).__name__}}",
|
|
121
|
+
"details": {{}}
|
|
122
|
+
}}
|
|
123
|
+
'''
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class CustomValidatorExecutor:
|
|
127
|
+
"""Executor for custom validators.
|
|
128
|
+
|
|
129
|
+
This class handles the execution of custom validators
|
|
130
|
+
in a sandboxed environment with logging and monitoring.
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
sandbox: Plugin sandbox for secure execution.
|
|
134
|
+
log_executions: Whether to log executions to database.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
sandbox_config: SandboxConfig | None = None,
|
|
140
|
+
log_executions: bool = True,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Initialize the executor.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
sandbox_config: Sandbox configuration.
|
|
146
|
+
log_executions: Whether to log executions.
|
|
147
|
+
"""
|
|
148
|
+
self.sandbox = create_sandbox(sandbox_config or SandboxConfig())
|
|
149
|
+
self.log_executions = log_executions
|
|
150
|
+
|
|
151
|
+
def validate_validator_code(self, code: str) -> tuple[bool, list[str]]:
|
|
152
|
+
"""Validate custom validator code before execution.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
code: Python code implementing the validator.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Tuple of (is_valid, list of issues).
|
|
159
|
+
"""
|
|
160
|
+
issues = []
|
|
161
|
+
|
|
162
|
+
# Check for required function
|
|
163
|
+
if "def validate(" not in code:
|
|
164
|
+
issues.append("Missing required 'validate' function")
|
|
165
|
+
|
|
166
|
+
# Check for dangerous patterns
|
|
167
|
+
dangerous = [
|
|
168
|
+
"os.system",
|
|
169
|
+
"subprocess",
|
|
170
|
+
"exec(",
|
|
171
|
+
"eval(",
|
|
172
|
+
"__import__",
|
|
173
|
+
"open(",
|
|
174
|
+
"file(",
|
|
175
|
+
]
|
|
176
|
+
for pattern in dangerous:
|
|
177
|
+
if pattern in code:
|
|
178
|
+
issues.append(f"Dangerous pattern detected: {pattern}")
|
|
179
|
+
|
|
180
|
+
# Analyze with sandbox
|
|
181
|
+
sandbox_issues, _ = self.sandbox.analyze_code(code)
|
|
182
|
+
issues.extend(sandbox_issues)
|
|
183
|
+
|
|
184
|
+
return len(issues) == 0, issues
|
|
185
|
+
|
|
186
|
+
async def execute(
|
|
187
|
+
self,
|
|
188
|
+
validator: CustomValidator,
|
|
189
|
+
context: ValidatorContext,
|
|
190
|
+
session: AsyncSession | None = None,
|
|
191
|
+
source_id: str | None = None,
|
|
192
|
+
) -> ValidatorResult:
|
|
193
|
+
"""Execute a custom validator.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
validator: CustomValidator model.
|
|
197
|
+
context: Validation context.
|
|
198
|
+
session: Optional database session for logging.
|
|
199
|
+
source_id: Optional source ID for logging.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
ValidatorResult with execution results.
|
|
203
|
+
"""
|
|
204
|
+
execution_id = str(uuid.uuid4())
|
|
205
|
+
start_time = datetime.utcnow()
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"Executing custom validator: {validator.name} "
|
|
209
|
+
f"on column: {context.column_name}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Create execution log if logging enabled
|
|
213
|
+
log_entry = None
|
|
214
|
+
if self.log_executions and session and validator.plugin_id:
|
|
215
|
+
log_entry = PluginExecutionLog(
|
|
216
|
+
plugin_id=validator.plugin_id,
|
|
217
|
+
validator_id=str(validator.id),
|
|
218
|
+
execution_id=execution_id,
|
|
219
|
+
source_id=source_id,
|
|
220
|
+
status="running",
|
|
221
|
+
)
|
|
222
|
+
session.add(log_entry)
|
|
223
|
+
await session.flush()
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
# Prepare the code
|
|
227
|
+
wrapped_code = VALIDATOR_WRAPPER_CODE.format(user_code=validator.code)
|
|
228
|
+
|
|
229
|
+
# Execute in sandbox
|
|
230
|
+
sandbox_result = self.sandbox.execute(
|
|
231
|
+
code=wrapped_code,
|
|
232
|
+
entry_point="_execute_validator",
|
|
233
|
+
entry_args=context.to_dict(),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if sandbox_result.success and sandbox_result.result:
|
|
237
|
+
result = ValidatorResult(
|
|
238
|
+
passed=sandbox_result.result.get("passed", False),
|
|
239
|
+
issues=sandbox_result.result.get("issues", []),
|
|
240
|
+
message=sandbox_result.result.get("message", ""),
|
|
241
|
+
details=sandbox_result.result.get("details", {}),
|
|
242
|
+
execution_time_ms=sandbox_result.execution_time_ms,
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
result = ValidatorResult(
|
|
246
|
+
passed=False,
|
|
247
|
+
message=sandbox_result.error or "Execution failed",
|
|
248
|
+
details={
|
|
249
|
+
"stdout": sandbox_result.stdout,
|
|
250
|
+
"stderr": sandbox_result.stderr,
|
|
251
|
+
},
|
|
252
|
+
execution_time_ms=sandbox_result.execution_time_ms,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Update validator usage stats
|
|
256
|
+
validator.increment_usage()
|
|
257
|
+
|
|
258
|
+
# Update execution log
|
|
259
|
+
if log_entry:
|
|
260
|
+
if result.passed:
|
|
261
|
+
log_entry.mark_completed(
|
|
262
|
+
result={"passed": result.passed, "issues_count": len(result.issues)},
|
|
263
|
+
memory_used_mb=sandbox_result.memory_used_mb,
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
log_entry.mark_failed(result.message)
|
|
267
|
+
|
|
268
|
+
if session:
|
|
269
|
+
await session.flush()
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"Custom validator execution failed: {e}")
|
|
275
|
+
|
|
276
|
+
if log_entry:
|
|
277
|
+
log_entry.mark_failed(str(e))
|
|
278
|
+
if session:
|
|
279
|
+
await session.flush()
|
|
280
|
+
|
|
281
|
+
return ValidatorResult(
|
|
282
|
+
passed=False,
|
|
283
|
+
message=f"Execution error: {e}",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def test_validator(
|
|
287
|
+
self,
|
|
288
|
+
code: str,
|
|
289
|
+
parameters: list[dict[str, Any]],
|
|
290
|
+
test_data: dict[str, Any],
|
|
291
|
+
param_values: dict[str, Any] | None = None,
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
"""Test a custom validator without saving.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
code: Validator code.
|
|
297
|
+
parameters: Parameter definitions.
|
|
298
|
+
test_data: Test data (column_name and values).
|
|
299
|
+
param_values: Parameter values to use.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Dictionary with test results.
|
|
303
|
+
"""
|
|
304
|
+
# Validate code first
|
|
305
|
+
is_valid, issues = self.validate_validator_code(code)
|
|
306
|
+
if not is_valid:
|
|
307
|
+
return {
|
|
308
|
+
"success": False,
|
|
309
|
+
"passed": None,
|
|
310
|
+
"error": f"Code validation failed: {'; '.join(issues)}",
|
|
311
|
+
"execution_time_ms": 0,
|
|
312
|
+
"warnings": issues,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Create context
|
|
316
|
+
context = ValidatorContext(
|
|
317
|
+
column_name=test_data.get("column_name", "test_column"),
|
|
318
|
+
column_values=test_data.get("values", []),
|
|
319
|
+
parameters=param_values or {},
|
|
320
|
+
schema=test_data.get("schema", {}),
|
|
321
|
+
row_count=len(test_data.get("values", [])),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Execute
|
|
325
|
+
wrapped_code = VALIDATOR_WRAPPER_CODE.format(user_code=code)
|
|
326
|
+
sandbox_result = self.sandbox.execute(
|
|
327
|
+
code=wrapped_code,
|
|
328
|
+
entry_point="_execute_validator",
|
|
329
|
+
entry_args=context.to_dict(),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if sandbox_result.success and sandbox_result.result:
|
|
333
|
+
return {
|
|
334
|
+
"success": True,
|
|
335
|
+
"passed": sandbox_result.result.get("passed", False),
|
|
336
|
+
"result": sandbox_result.result,
|
|
337
|
+
"execution_time_ms": sandbox_result.execution_time_ms,
|
|
338
|
+
"warnings": sandbox_result.warnings,
|
|
339
|
+
}
|
|
340
|
+
else:
|
|
341
|
+
return {
|
|
342
|
+
"success": False,
|
|
343
|
+
"passed": None,
|
|
344
|
+
"error": sandbox_result.error,
|
|
345
|
+
"execution_time_ms": sandbox_result.execution_time_ms,
|
|
346
|
+
"warnings": sandbox_result.warnings,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
def get_validator_template(self) -> str:
|
|
350
|
+
"""Get a template for creating custom validators.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Template code string.
|
|
354
|
+
"""
|
|
355
|
+
return '''
|
|
356
|
+
def validate(column_name, values, params, schema, row_count):
|
|
357
|
+
"""Custom validator function.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
column_name: Name of the column being validated.
|
|
361
|
+
values: List of values in the column.
|
|
362
|
+
params: Dictionary of parameter values.
|
|
363
|
+
schema: Column schema information.
|
|
364
|
+
row_count: Total number of rows.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dictionary with:
|
|
368
|
+
- passed: bool - Whether validation passed
|
|
369
|
+
- issues: list - List of issue dictionaries
|
|
370
|
+
- message: str - Summary message
|
|
371
|
+
- details: dict - Additional details
|
|
372
|
+
"""
|
|
373
|
+
issues = []
|
|
374
|
+
|
|
375
|
+
# Example: Check for null values
|
|
376
|
+
null_count = sum(1 for v in values if v is None)
|
|
377
|
+
if null_count > 0:
|
|
378
|
+
issues.append({
|
|
379
|
+
"row": None,
|
|
380
|
+
"message": f"Found {null_count} null values",
|
|
381
|
+
"severity": "warning"
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
# Example: Custom validation logic
|
|
385
|
+
# threshold = params.get("threshold", 0.1)
|
|
386
|
+
# ...
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"passed": len(issues) == 0,
|
|
390
|
+
"issues": issues,
|
|
391
|
+
"message": f"Validation completed with {len(issues)} issues",
|
|
392
|
+
"details": {
|
|
393
|
+
"null_count": null_count,
|
|
394
|
+
"total_values": len(values)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
'''
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# Default executor instance
|
|
401
|
+
validator_executor = CustomValidatorExecutor()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Plugin Versioning and Dependency Management Module.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Semantic versioning (semver) parsing and comparison
|
|
5
|
+
- Version constraint parsing (^, ~, >=, <, etc.)
|
|
6
|
+
- Dependency graph with cycle detection
|
|
7
|
+
- Topological sorting for load order
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .semver import (
|
|
13
|
+
Version,
|
|
14
|
+
parse_version,
|
|
15
|
+
compare_versions,
|
|
16
|
+
)
|
|
17
|
+
from .constraints import (
|
|
18
|
+
VersionConstraint,
|
|
19
|
+
VersionRange,
|
|
20
|
+
parse_constraint,
|
|
21
|
+
satisfies,
|
|
22
|
+
find_best_version,
|
|
23
|
+
)
|
|
24
|
+
from .dependencies import (
|
|
25
|
+
DependencyType,
|
|
26
|
+
Dependency,
|
|
27
|
+
DependencyGraph,
|
|
28
|
+
DependencyResolver,
|
|
29
|
+
DependencyResolutionError,
|
|
30
|
+
CyclicDependencyError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Semver
|
|
35
|
+
"Version",
|
|
36
|
+
"parse_version",
|
|
37
|
+
"compare_versions",
|
|
38
|
+
# Constraints
|
|
39
|
+
"VersionConstraint",
|
|
40
|
+
"VersionRange",
|
|
41
|
+
"parse_constraint",
|
|
42
|
+
"satisfies",
|
|
43
|
+
"find_best_version",
|
|
44
|
+
# Dependencies
|
|
45
|
+
"DependencyType",
|
|
46
|
+
"Dependency",
|
|
47
|
+
"DependencyGraph",
|
|
48
|
+
"DependencyResolver",
|
|
49
|
+
"DependencyResolutionError",
|
|
50
|
+
"CyclicDependencyError",
|
|
51
|
+
]
|