truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +645 -23
- 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 +15 -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.1.dist-info/METADATA +312 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.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.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Custom action implementations.
|
|
2
|
+
|
|
3
|
+
Provides flexible action types for custom integrations:
|
|
4
|
+
- Callback action: Execute Python callables
|
|
5
|
+
- Shell command action: Execute shell commands
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
from truthound_dashboard.core.interfaces.actions import (
|
|
18
|
+
ActionConfig,
|
|
19
|
+
ActionContext,
|
|
20
|
+
ActionResult,
|
|
21
|
+
ActionStatus,
|
|
22
|
+
AsyncBaseAction,
|
|
23
|
+
BaseAction,
|
|
24
|
+
NotifyCondition,
|
|
25
|
+
register_action,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Callback Action
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CallbackConfig(ActionConfig):
|
|
38
|
+
"""Configuration for callback action.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
callback: Python callable to execute.
|
|
42
|
+
pass_context: Pass full context to callback.
|
|
43
|
+
pass_result_only: Pass only checkpoint result to callback.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
callback: Callable[..., Any] | None = None
|
|
47
|
+
pass_context: bool = True
|
|
48
|
+
pass_result_only: bool = False
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
self.name = self.name or "callback"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@register_action("callback")
|
|
55
|
+
class CallbackAction(BaseAction):
|
|
56
|
+
"""Execute a Python callback function.
|
|
57
|
+
|
|
58
|
+
Provides maximum flexibility for custom actions by executing
|
|
59
|
+
arbitrary Python callables.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
def my_handler(context):
|
|
63
|
+
print(f"Validation completed: {context.checkpoint_result.status}")
|
|
64
|
+
return {"custom_key": "custom_value"}
|
|
65
|
+
|
|
66
|
+
action = CallbackAction(callback=my_handler)
|
|
67
|
+
|
|
68
|
+
The callback receives the ActionContext and should return:
|
|
69
|
+
- None: Uses default success result
|
|
70
|
+
- dict: Merged into result details
|
|
71
|
+
- ActionResult: Used directly as the result
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
callback: Callable[..., Any] | None = None,
|
|
77
|
+
notify_on: NotifyCondition = NotifyCondition.ALWAYS,
|
|
78
|
+
config: CallbackConfig | dict[str, Any] | None = None,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
) -> None:
|
|
81
|
+
if config is None:
|
|
82
|
+
config = CallbackConfig(
|
|
83
|
+
callback=callback,
|
|
84
|
+
notify_on=notify_on,
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
87
|
+
elif isinstance(config, dict):
|
|
88
|
+
callback = config.pop("callback", callback)
|
|
89
|
+
config = CallbackConfig(callback=callback, **config)
|
|
90
|
+
|
|
91
|
+
super().__init__(config)
|
|
92
|
+
self._callback_config: CallbackConfig = config
|
|
93
|
+
self._callback = callback or self._callback_config.callback
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def action_type(self) -> str:
|
|
97
|
+
return "custom"
|
|
98
|
+
|
|
99
|
+
def _do_execute(self, context: ActionContext) -> ActionResult:
|
|
100
|
+
"""Execute the callback."""
|
|
101
|
+
if self._callback is None:
|
|
102
|
+
return ActionResult(
|
|
103
|
+
action_name=self.name,
|
|
104
|
+
action_type=self.action_type,
|
|
105
|
+
status=ActionStatus.FAILURE,
|
|
106
|
+
message="No callback function provided",
|
|
107
|
+
error="callback is None",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Determine what to pass to callback
|
|
112
|
+
if self._callback_config.pass_result_only:
|
|
113
|
+
callback_result = self._callback(context.checkpoint_result)
|
|
114
|
+
elif self._callback_config.pass_context:
|
|
115
|
+
callback_result = self._callback(context)
|
|
116
|
+
else:
|
|
117
|
+
callback_result = self._callback()
|
|
118
|
+
|
|
119
|
+
# Process callback result
|
|
120
|
+
if callback_result is None:
|
|
121
|
+
return ActionResult(
|
|
122
|
+
action_name=self.name,
|
|
123
|
+
action_type=self.action_type,
|
|
124
|
+
status=ActionStatus.SUCCESS,
|
|
125
|
+
message="Callback executed successfully",
|
|
126
|
+
)
|
|
127
|
+
elif isinstance(callback_result, ActionResult):
|
|
128
|
+
return callback_result
|
|
129
|
+
elif isinstance(callback_result, dict):
|
|
130
|
+
return ActionResult(
|
|
131
|
+
action_name=self.name,
|
|
132
|
+
action_type=self.action_type,
|
|
133
|
+
status=ActionStatus.SUCCESS,
|
|
134
|
+
message="Callback executed successfully",
|
|
135
|
+
details=callback_result,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
return ActionResult(
|
|
139
|
+
action_name=self.name,
|
|
140
|
+
action_type=self.action_type,
|
|
141
|
+
status=ActionStatus.SUCCESS,
|
|
142
|
+
message="Callback executed successfully",
|
|
143
|
+
details={"return_value": str(callback_result)},
|
|
144
|
+
)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return ActionResult(
|
|
147
|
+
action_name=self.name,
|
|
148
|
+
action_type=self.action_type,
|
|
149
|
+
status=ActionStatus.FAILURE,
|
|
150
|
+
message=f"Callback failed: {str(e)}",
|
|
151
|
+
error=str(e),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# =============================================================================
|
|
156
|
+
# Async Callback Action
|
|
157
|
+
# =============================================================================
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class AsyncCallbackConfig(ActionConfig):
|
|
162
|
+
"""Configuration for async callback action."""
|
|
163
|
+
|
|
164
|
+
callback: Callable[..., Any] | None = None
|
|
165
|
+
pass_context: bool = True
|
|
166
|
+
|
|
167
|
+
def __post_init__(self):
|
|
168
|
+
self.name = self.name or "async_callback"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@register_action("async_callback")
|
|
172
|
+
class AsyncCallbackAction(AsyncBaseAction):
|
|
173
|
+
"""Execute an async Python callback function.
|
|
174
|
+
|
|
175
|
+
Similar to CallbackAction but for async/await patterns.
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
async def my_async_handler(context):
|
|
179
|
+
async with aiohttp.ClientSession() as session:
|
|
180
|
+
await session.post(url, json=data)
|
|
181
|
+
return {"status": "posted"}
|
|
182
|
+
|
|
183
|
+
action = AsyncCallbackAction(callback=my_async_handler)
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
callback: Callable[..., Any] | None = None,
|
|
189
|
+
notify_on: NotifyCondition = NotifyCondition.ALWAYS,
|
|
190
|
+
config: AsyncCallbackConfig | dict[str, Any] | None = None,
|
|
191
|
+
**kwargs: Any,
|
|
192
|
+
) -> None:
|
|
193
|
+
if config is None:
|
|
194
|
+
config = AsyncCallbackConfig(
|
|
195
|
+
callback=callback,
|
|
196
|
+
notify_on=notify_on,
|
|
197
|
+
**kwargs,
|
|
198
|
+
)
|
|
199
|
+
elif isinstance(config, dict):
|
|
200
|
+
callback = config.pop("callback", callback)
|
|
201
|
+
config = AsyncCallbackConfig(callback=callback, **config)
|
|
202
|
+
|
|
203
|
+
super().__init__(config)
|
|
204
|
+
self._callback_config: AsyncCallbackConfig = config
|
|
205
|
+
self._callback = callback or self._callback_config.callback
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def action_type(self) -> str:
|
|
209
|
+
return "custom"
|
|
210
|
+
|
|
211
|
+
async def _do_execute_async(self, context: ActionContext) -> ActionResult:
|
|
212
|
+
"""Execute the async callback."""
|
|
213
|
+
if self._callback is None:
|
|
214
|
+
return ActionResult(
|
|
215
|
+
action_name=self.name,
|
|
216
|
+
action_type=self.action_type,
|
|
217
|
+
status=ActionStatus.FAILURE,
|
|
218
|
+
message="No callback function provided",
|
|
219
|
+
error="callback is None",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
if self._callback_config.pass_context:
|
|
224
|
+
callback_result = await self._callback(context)
|
|
225
|
+
else:
|
|
226
|
+
callback_result = await self._callback()
|
|
227
|
+
|
|
228
|
+
if callback_result is None:
|
|
229
|
+
return ActionResult(
|
|
230
|
+
action_name=self.name,
|
|
231
|
+
action_type=self.action_type,
|
|
232
|
+
status=ActionStatus.SUCCESS,
|
|
233
|
+
message="Async callback executed successfully",
|
|
234
|
+
)
|
|
235
|
+
elif isinstance(callback_result, ActionResult):
|
|
236
|
+
return callback_result
|
|
237
|
+
elif isinstance(callback_result, dict):
|
|
238
|
+
return ActionResult(
|
|
239
|
+
action_name=self.name,
|
|
240
|
+
action_type=self.action_type,
|
|
241
|
+
status=ActionStatus.SUCCESS,
|
|
242
|
+
message="Async callback executed successfully",
|
|
243
|
+
details=callback_result,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
return ActionResult(
|
|
247
|
+
action_name=self.name,
|
|
248
|
+
action_type=self.action_type,
|
|
249
|
+
status=ActionStatus.SUCCESS,
|
|
250
|
+
message="Async callback executed successfully",
|
|
251
|
+
details={"return_value": str(callback_result)},
|
|
252
|
+
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
return ActionResult(
|
|
255
|
+
action_name=self.name,
|
|
256
|
+
action_type=self.action_type,
|
|
257
|
+
status=ActionStatus.FAILURE,
|
|
258
|
+
message=f"Async callback failed: {str(e)}",
|
|
259
|
+
error=str(e),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# =============================================================================
|
|
264
|
+
# Shell Command Action
|
|
265
|
+
# =============================================================================
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass
|
|
269
|
+
class ShellCommandConfig(ActionConfig):
|
|
270
|
+
"""Configuration for shell command action.
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
command: Shell command to execute.
|
|
274
|
+
shell: Use shell execution.
|
|
275
|
+
cwd: Working directory.
|
|
276
|
+
env: Environment variables.
|
|
277
|
+
capture_output: Capture stdout/stderr.
|
|
278
|
+
check: Raise exception on non-zero exit.
|
|
279
|
+
pass_env_vars: Environment variable names to set from context.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
command: str = ""
|
|
283
|
+
shell: bool = True
|
|
284
|
+
cwd: str | None = None
|
|
285
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
286
|
+
capture_output: bool = True
|
|
287
|
+
check: bool = False
|
|
288
|
+
pass_env_vars: list[str] = field(default_factory=list)
|
|
289
|
+
|
|
290
|
+
def __post_init__(self):
|
|
291
|
+
self.name = self.name or "shell"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@register_action("shell")
|
|
295
|
+
class ShellCommandAction(BaseAction):
|
|
296
|
+
"""Execute a shell command.
|
|
297
|
+
|
|
298
|
+
Runs a shell command after validation completes.
|
|
299
|
+
Can pass validation context as environment variables.
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
action = ShellCommandAction(
|
|
303
|
+
command="./notify.sh",
|
|
304
|
+
pass_env_vars=["CHECKPOINT_NAME", "STATUS", "ISSUE_COUNT"],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
Available environment variables:
|
|
308
|
+
- TRUTHOUND_CHECKPOINT_NAME
|
|
309
|
+
- TRUTHOUND_RUN_ID
|
|
310
|
+
- TRUTHOUND_STATUS
|
|
311
|
+
- TRUTHOUND_SOURCE_NAME
|
|
312
|
+
- TRUTHOUND_ROW_COUNT
|
|
313
|
+
- TRUTHOUND_ISSUE_COUNT
|
|
314
|
+
- TRUTHOUND_CRITICAL_COUNT
|
|
315
|
+
- TRUTHOUND_HIGH_COUNT
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
def __init__(
|
|
319
|
+
self,
|
|
320
|
+
command: str = "",
|
|
321
|
+
notify_on: NotifyCondition = NotifyCondition.ALWAYS,
|
|
322
|
+
config: ShellCommandConfig | dict[str, Any] | None = None,
|
|
323
|
+
**kwargs: Any,
|
|
324
|
+
) -> None:
|
|
325
|
+
if config is None:
|
|
326
|
+
config = ShellCommandConfig(
|
|
327
|
+
command=command,
|
|
328
|
+
notify_on=notify_on,
|
|
329
|
+
**kwargs,
|
|
330
|
+
)
|
|
331
|
+
elif isinstance(config, dict):
|
|
332
|
+
config = ShellCommandConfig(**config)
|
|
333
|
+
|
|
334
|
+
super().__init__(config)
|
|
335
|
+
self._shell_config: ShellCommandConfig = config
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def action_type(self) -> str:
|
|
339
|
+
return "custom"
|
|
340
|
+
|
|
341
|
+
def _do_execute(self, context: ActionContext) -> ActionResult:
|
|
342
|
+
"""Execute shell command."""
|
|
343
|
+
if not self._shell_config.command:
|
|
344
|
+
return ActionResult(
|
|
345
|
+
action_name=self.name,
|
|
346
|
+
action_type=self.action_type,
|
|
347
|
+
status=ActionStatus.FAILURE,
|
|
348
|
+
message="No command provided",
|
|
349
|
+
error="command is empty",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Build environment with validation context
|
|
353
|
+
env = {**self._shell_config.env}
|
|
354
|
+
result = context.checkpoint_result
|
|
355
|
+
|
|
356
|
+
# Add standard environment variables
|
|
357
|
+
env.update({
|
|
358
|
+
"TRUTHOUND_CHECKPOINT_NAME": result.checkpoint_name,
|
|
359
|
+
"TRUTHOUND_RUN_ID": result.run_id,
|
|
360
|
+
"TRUTHOUND_STATUS": result.status.value,
|
|
361
|
+
"TRUTHOUND_SOURCE_NAME": result.source_name,
|
|
362
|
+
"TRUTHOUND_ROW_COUNT": str(result.row_count),
|
|
363
|
+
"TRUTHOUND_COLUMN_COUNT": str(result.column_count),
|
|
364
|
+
"TRUTHOUND_ISSUE_COUNT": str(result.issue_count),
|
|
365
|
+
"TRUTHOUND_CRITICAL_COUNT": str(result.critical_count),
|
|
366
|
+
"TRUTHOUND_HIGH_COUNT": str(result.high_count),
|
|
367
|
+
"TRUTHOUND_MEDIUM_COUNT": str(result.medium_count),
|
|
368
|
+
"TRUTHOUND_LOW_COUNT": str(result.low_count),
|
|
369
|
+
"TRUTHOUND_HAS_CRITICAL": str(result.has_critical).lower(),
|
|
370
|
+
"TRUTHOUND_HAS_HIGH": str(result.has_high).lower(),
|
|
371
|
+
"TRUTHOUND_DURATION_MS": str(result.duration_ms),
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
proc = subprocess.run(
|
|
376
|
+
self._shell_config.command,
|
|
377
|
+
shell=self._shell_config.shell,
|
|
378
|
+
cwd=self._shell_config.cwd,
|
|
379
|
+
env=env,
|
|
380
|
+
capture_output=self._shell_config.capture_output,
|
|
381
|
+
check=self._shell_config.check,
|
|
382
|
+
timeout=self._config.timeout_seconds,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
details: dict[str, Any] = {
|
|
386
|
+
"command": self._shell_config.command,
|
|
387
|
+
"exit_code": proc.returncode,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if self._shell_config.capture_output:
|
|
391
|
+
details["stdout"] = proc.stdout.decode("utf-8", errors="replace")
|
|
392
|
+
details["stderr"] = proc.stderr.decode("utf-8", errors="replace")
|
|
393
|
+
|
|
394
|
+
if proc.returncode == 0:
|
|
395
|
+
return ActionResult(
|
|
396
|
+
action_name=self.name,
|
|
397
|
+
action_type=self.action_type,
|
|
398
|
+
status=ActionStatus.SUCCESS,
|
|
399
|
+
message=f"Command executed successfully (exit code: {proc.returncode})",
|
|
400
|
+
details=details,
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
return ActionResult(
|
|
404
|
+
action_name=self.name,
|
|
405
|
+
action_type=self.action_type,
|
|
406
|
+
status=ActionStatus.FAILURE,
|
|
407
|
+
message=f"Command failed with exit code: {proc.returncode}",
|
|
408
|
+
details=details,
|
|
409
|
+
error=details.get("stderr", ""),
|
|
410
|
+
)
|
|
411
|
+
except subprocess.TimeoutExpired:
|
|
412
|
+
return ActionResult(
|
|
413
|
+
action_name=self.name,
|
|
414
|
+
action_type=self.action_type,
|
|
415
|
+
status=ActionStatus.FAILURE,
|
|
416
|
+
message=f"Command timed out after {self._config.timeout_seconds}s",
|
|
417
|
+
error="TimeoutExpired",
|
|
418
|
+
)
|
|
419
|
+
except Exception as e:
|
|
420
|
+
return ActionResult(
|
|
421
|
+
action_name=self.name,
|
|
422
|
+
action_type=self.action_type,
|
|
423
|
+
status=ActionStatus.FAILURE,
|
|
424
|
+
message=f"Command failed: {str(e)}",
|
|
425
|
+
error=str(e),
|
|
426
|
+
)
|