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.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.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
+ )