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,652 @@
|
|
|
1
|
+
"""Action interfaces for checkpoint-based validation pipelines.
|
|
2
|
+
|
|
3
|
+
Actions are executed after validation completes. They can store results,
|
|
4
|
+
send notifications, or integrate with external systems.
|
|
5
|
+
|
|
6
|
+
This module defines abstract interfaces for actions that are loosely
|
|
7
|
+
coupled from truthound's checkpoint.actions module.
|
|
8
|
+
|
|
9
|
+
Supported action types:
|
|
10
|
+
- Storage: Store validation results to filesystem, S3, GCS
|
|
11
|
+
- Notifications: Slack, Email, Teams, Discord, Telegram, PagerDuty
|
|
12
|
+
- Webhook: Call any HTTP endpoint
|
|
13
|
+
- Custom: Execute Python callbacks or shell commands
|
|
14
|
+
- DataDocs: Generate HTML/Markdown documentation
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from truthound_dashboard.core.interfaces.checkpoint import CheckpointResult
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NotifyCondition(str, Enum):
|
|
30
|
+
"""Conditions under which actions are triggered.
|
|
31
|
+
|
|
32
|
+
Maps to truthound's notify_on parameter for actions.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
ALWAYS = "always" # Every run
|
|
36
|
+
SUCCESS = "success" # Validation passed
|
|
37
|
+
FAILURE = "failure" # Validation failed
|
|
38
|
+
ERROR = "error" # System error occurred
|
|
39
|
+
FAILURE_OR_ERROR = "failure_or_error" # Failure or error
|
|
40
|
+
NOT_SUCCESS = "not_success" # Any non-success status
|
|
41
|
+
WARNING = "warning" # Warning status
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ActionStatus(str, Enum):
|
|
45
|
+
"""Status of an action execution."""
|
|
46
|
+
|
|
47
|
+
PENDING = "pending"
|
|
48
|
+
RUNNING = "running"
|
|
49
|
+
SUCCESS = "success"
|
|
50
|
+
FAILURE = "failure"
|
|
51
|
+
SKIPPED = "skipped" # Skipped due to notify_on condition
|
|
52
|
+
COMPENSATED = "compensated" # Rolled back (for transactional actions)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ActionConfig:
|
|
57
|
+
"""Base configuration for all actions.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
name: Action name for identification.
|
|
61
|
+
notify_on: When to execute this action.
|
|
62
|
+
enabled: Whether this action is enabled.
|
|
63
|
+
timeout_seconds: Maximum execution time.
|
|
64
|
+
retry_count: Number of retries on failure.
|
|
65
|
+
retry_delay_seconds: Delay between retries.
|
|
66
|
+
metadata: Additional metadata for the action.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str = ""
|
|
70
|
+
notify_on: NotifyCondition = NotifyCondition.FAILURE
|
|
71
|
+
enabled: bool = True
|
|
72
|
+
timeout_seconds: int = 30
|
|
73
|
+
retry_count: int = 0
|
|
74
|
+
retry_delay_seconds: int = 5
|
|
75
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict[str, Any]:
|
|
78
|
+
"""Convert to dictionary."""
|
|
79
|
+
return {
|
|
80
|
+
"name": self.name,
|
|
81
|
+
"notify_on": self.notify_on.value,
|
|
82
|
+
"enabled": self.enabled,
|
|
83
|
+
"timeout_seconds": self.timeout_seconds,
|
|
84
|
+
"retry_count": self.retry_count,
|
|
85
|
+
"retry_delay_seconds": self.retry_delay_seconds,
|
|
86
|
+
"metadata": self.metadata,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_dict(cls, data: dict[str, Any]) -> "ActionConfig":
|
|
91
|
+
"""Create from dictionary."""
|
|
92
|
+
notify_on = data.get("notify_on", "failure")
|
|
93
|
+
if isinstance(notify_on, str):
|
|
94
|
+
notify_on = NotifyCondition(notify_on)
|
|
95
|
+
return cls(
|
|
96
|
+
name=data.get("name", ""),
|
|
97
|
+
notify_on=notify_on,
|
|
98
|
+
enabled=data.get("enabled", True),
|
|
99
|
+
timeout_seconds=data.get("timeout_seconds", 30),
|
|
100
|
+
retry_count=data.get("retry_count", 0),
|
|
101
|
+
retry_delay_seconds=data.get("retry_delay_seconds", 5),
|
|
102
|
+
metadata=data.get("metadata", {}),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ActionResult:
|
|
108
|
+
"""Result of an action execution.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
action_name: Name of the action.
|
|
112
|
+
action_type: Type of action (notification, storage, etc.).
|
|
113
|
+
status: Execution status.
|
|
114
|
+
message: Human-readable message.
|
|
115
|
+
started_at: When execution started.
|
|
116
|
+
completed_at: When execution completed.
|
|
117
|
+
duration_ms: Execution duration in milliseconds.
|
|
118
|
+
details: Additional result details.
|
|
119
|
+
error: Error message if failed.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
action_name: str
|
|
123
|
+
action_type: str
|
|
124
|
+
status: ActionStatus
|
|
125
|
+
message: str = ""
|
|
126
|
+
started_at: datetime | None = None
|
|
127
|
+
completed_at: datetime | None = None
|
|
128
|
+
duration_ms: float = 0.0
|
|
129
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
130
|
+
error: str | None = None
|
|
131
|
+
|
|
132
|
+
def to_dict(self) -> dict[str, Any]:
|
|
133
|
+
"""Convert to dictionary."""
|
|
134
|
+
return {
|
|
135
|
+
"action_name": self.action_name,
|
|
136
|
+
"action_type": self.action_type,
|
|
137
|
+
"status": self.status.value,
|
|
138
|
+
"message": self.message,
|
|
139
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
140
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
141
|
+
"duration_ms": self.duration_ms,
|
|
142
|
+
"details": self.details,
|
|
143
|
+
"error": self.error,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class ActionContext:
|
|
149
|
+
"""Context passed to action execution.
|
|
150
|
+
|
|
151
|
+
Contains all information needed for an action to execute,
|
|
152
|
+
including the checkpoint result and environment variables.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
checkpoint_result: The validation result.
|
|
156
|
+
run_id: Unique run identifier.
|
|
157
|
+
checkpoint_name: Name of the checkpoint.
|
|
158
|
+
tags: Tags from the checkpoint.
|
|
159
|
+
metadata: Additional metadata.
|
|
160
|
+
environment: Environment variables (may contain secrets).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
checkpoint_result: "CheckpointResult"
|
|
164
|
+
run_id: str
|
|
165
|
+
checkpoint_name: str
|
|
166
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
167
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
168
|
+
environment: dict[str, str] = field(default_factory=dict)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@runtime_checkable
|
|
172
|
+
class ActionProtocol(Protocol):
|
|
173
|
+
"""Protocol for synchronous action implementations.
|
|
174
|
+
|
|
175
|
+
Actions are executed after validation completes. Each action
|
|
176
|
+
must implement the execute method.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
class SlackNotification:
|
|
180
|
+
def execute(self, context: ActionContext) -> ActionResult:
|
|
181
|
+
# Send Slack message
|
|
182
|
+
return ActionResult(
|
|
183
|
+
action_name="slack",
|
|
184
|
+
action_type="notification",
|
|
185
|
+
status=ActionStatus.SUCCESS,
|
|
186
|
+
)
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def name(self) -> str:
|
|
191
|
+
"""Get action name."""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def action_type(self) -> str:
|
|
196
|
+
"""Get action type (notification, storage, webhook, custom)."""
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def config(self) -> ActionConfig:
|
|
201
|
+
"""Get action configuration."""
|
|
202
|
+
...
|
|
203
|
+
|
|
204
|
+
def should_execute(self, context: ActionContext) -> bool:
|
|
205
|
+
"""Check if action should execute based on notify_on condition.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
context: Execution context with checkpoint result.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if action should execute.
|
|
212
|
+
"""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
def execute(self, context: ActionContext) -> ActionResult:
|
|
216
|
+
"""Execute the action synchronously.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
context: Execution context.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Action result.
|
|
223
|
+
"""
|
|
224
|
+
...
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@runtime_checkable
|
|
228
|
+
class AsyncActionProtocol(Protocol):
|
|
229
|
+
"""Protocol for asynchronous action implementations.
|
|
230
|
+
|
|
231
|
+
For high-throughput scenarios, use async actions that don't
|
|
232
|
+
block the event loop.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
class AsyncSlackNotification:
|
|
236
|
+
async def execute_async(self, context: ActionContext) -> ActionResult:
|
|
237
|
+
async with aiohttp.ClientSession() as session:
|
|
238
|
+
await session.post(webhook_url, json=payload)
|
|
239
|
+
return ActionResult(...)
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def name(self) -> str:
|
|
244
|
+
"""Get action name."""
|
|
245
|
+
...
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def action_type(self) -> str:
|
|
249
|
+
"""Get action type."""
|
|
250
|
+
...
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def config(self) -> ActionConfig:
|
|
254
|
+
"""Get action configuration."""
|
|
255
|
+
...
|
|
256
|
+
|
|
257
|
+
def should_execute(self, context: ActionContext) -> bool:
|
|
258
|
+
"""Check if action should execute."""
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
async def execute_async(self, context: ActionContext) -> ActionResult:
|
|
262
|
+
"""Execute the action asynchronously.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
context: Execution context.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Action result.
|
|
269
|
+
"""
|
|
270
|
+
...
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class CompensatableActionProtocol(Protocol):
|
|
274
|
+
"""Protocol for actions that support rollback (compensation).
|
|
275
|
+
|
|
276
|
+
Compensatable actions can be rolled back if a subsequent action
|
|
277
|
+
fails, supporting the Saga pattern for distributed transactions.
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
class DatabaseUpdateAction:
|
|
281
|
+
def execute(self, context):
|
|
282
|
+
self.backup_id = create_backup()
|
|
283
|
+
update_database(context.checkpoint_result)
|
|
284
|
+
return ActionResult(status=ActionStatus.SUCCESS)
|
|
285
|
+
|
|
286
|
+
def compensate(self, context, execute_result):
|
|
287
|
+
restore_from_backup(self.backup_id)
|
|
288
|
+
return ActionResult(status=ActionStatus.COMPENSATED)
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
def execute(self, context: ActionContext) -> ActionResult:
|
|
292
|
+
"""Execute the forward action."""
|
|
293
|
+
...
|
|
294
|
+
|
|
295
|
+
def compensate(
|
|
296
|
+
self, context: ActionContext, execute_result: ActionResult
|
|
297
|
+
) -> ActionResult:
|
|
298
|
+
"""Compensate (rollback) the action.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
context: Original execution context.
|
|
302
|
+
execute_result: Result from the execute() call.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Compensation result.
|
|
306
|
+
"""
|
|
307
|
+
...
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class BaseAction(ABC):
|
|
311
|
+
"""Abstract base class for actions.
|
|
312
|
+
|
|
313
|
+
Provides common functionality for all actions including
|
|
314
|
+
notify_on condition checking and configuration management.
|
|
315
|
+
|
|
316
|
+
Subclasses must implement:
|
|
317
|
+
- action_type property
|
|
318
|
+
- _do_execute method
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
def __init__(self, config: ActionConfig | dict[str, Any] | None = None) -> None:
|
|
322
|
+
"""Initialize action.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
config: Action configuration or dict.
|
|
326
|
+
"""
|
|
327
|
+
if config is None:
|
|
328
|
+
self._config = ActionConfig()
|
|
329
|
+
elif isinstance(config, dict):
|
|
330
|
+
self._config = ActionConfig.from_dict(config)
|
|
331
|
+
else:
|
|
332
|
+
self._config = config
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def name(self) -> str:
|
|
336
|
+
"""Get action name."""
|
|
337
|
+
return self._config.name or self.__class__.__name__
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
@abstractmethod
|
|
341
|
+
def action_type(self) -> str:
|
|
342
|
+
"""Get action type."""
|
|
343
|
+
...
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def config(self) -> ActionConfig:
|
|
347
|
+
"""Get action configuration."""
|
|
348
|
+
return self._config
|
|
349
|
+
|
|
350
|
+
def should_execute(self, context: ActionContext) -> bool:
|
|
351
|
+
"""Check if action should execute based on notify_on condition.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
context: Execution context.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
True if action should execute.
|
|
358
|
+
"""
|
|
359
|
+
if not self._config.enabled:
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
result = context.checkpoint_result
|
|
363
|
+
condition = self._config.notify_on
|
|
364
|
+
|
|
365
|
+
if condition == NotifyCondition.ALWAYS:
|
|
366
|
+
return True
|
|
367
|
+
elif condition == NotifyCondition.SUCCESS:
|
|
368
|
+
return result.status.value == "success"
|
|
369
|
+
elif condition == NotifyCondition.FAILURE:
|
|
370
|
+
return result.status.value == "failure"
|
|
371
|
+
elif condition == NotifyCondition.ERROR:
|
|
372
|
+
return result.status.value == "error"
|
|
373
|
+
elif condition == NotifyCondition.FAILURE_OR_ERROR:
|
|
374
|
+
return result.status.value in ("failure", "error")
|
|
375
|
+
elif condition == NotifyCondition.NOT_SUCCESS:
|
|
376
|
+
return result.status.value != "success"
|
|
377
|
+
elif condition == NotifyCondition.WARNING:
|
|
378
|
+
return result.status.value == "warning"
|
|
379
|
+
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
def execute(self, context: ActionContext) -> ActionResult:
|
|
383
|
+
"""Execute the action.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
context: Execution context.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Action result.
|
|
390
|
+
"""
|
|
391
|
+
if not self.should_execute(context):
|
|
392
|
+
return ActionResult(
|
|
393
|
+
action_name=self.name,
|
|
394
|
+
action_type=self.action_type,
|
|
395
|
+
status=ActionStatus.SKIPPED,
|
|
396
|
+
message=f"Skipped due to notify_on={self._config.notify_on.value}",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
started_at = datetime.now()
|
|
400
|
+
try:
|
|
401
|
+
result = self._do_execute(context)
|
|
402
|
+
completed_at = datetime.now()
|
|
403
|
+
result.started_at = started_at
|
|
404
|
+
result.completed_at = completed_at
|
|
405
|
+
result.duration_ms = (completed_at - started_at).total_seconds() * 1000
|
|
406
|
+
return result
|
|
407
|
+
except Exception as e:
|
|
408
|
+
completed_at = datetime.now()
|
|
409
|
+
return ActionResult(
|
|
410
|
+
action_name=self.name,
|
|
411
|
+
action_type=self.action_type,
|
|
412
|
+
status=ActionStatus.FAILURE,
|
|
413
|
+
message=f"Action failed: {str(e)}",
|
|
414
|
+
started_at=started_at,
|
|
415
|
+
completed_at=completed_at,
|
|
416
|
+
duration_ms=(completed_at - started_at).total_seconds() * 1000,
|
|
417
|
+
error=str(e),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
@abstractmethod
|
|
421
|
+
def _do_execute(self, context: ActionContext) -> ActionResult:
|
|
422
|
+
"""Perform the actual action execution.
|
|
423
|
+
|
|
424
|
+
Subclasses must implement this method.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
context: Execution context.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Action result.
|
|
431
|
+
"""
|
|
432
|
+
...
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class AsyncBaseAction(ABC):
|
|
436
|
+
"""Abstract base class for async actions.
|
|
437
|
+
|
|
438
|
+
Similar to BaseAction but for asynchronous execution.
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
def __init__(self, config: ActionConfig | dict[str, Any] | None = None) -> None:
|
|
442
|
+
"""Initialize action."""
|
|
443
|
+
if config is None:
|
|
444
|
+
self._config = ActionConfig()
|
|
445
|
+
elif isinstance(config, dict):
|
|
446
|
+
self._config = ActionConfig.from_dict(config)
|
|
447
|
+
else:
|
|
448
|
+
self._config = config
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def name(self) -> str:
|
|
452
|
+
"""Get action name."""
|
|
453
|
+
return self._config.name or self.__class__.__name__
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
@abstractmethod
|
|
457
|
+
def action_type(self) -> str:
|
|
458
|
+
"""Get action type."""
|
|
459
|
+
...
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def config(self) -> ActionConfig:
|
|
463
|
+
"""Get action configuration."""
|
|
464
|
+
return self._config
|
|
465
|
+
|
|
466
|
+
def should_execute(self, context: ActionContext) -> bool:
|
|
467
|
+
"""Check if action should execute."""
|
|
468
|
+
if not self._config.enabled:
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
result = context.checkpoint_result
|
|
472
|
+
condition = self._config.notify_on
|
|
473
|
+
|
|
474
|
+
if condition == NotifyCondition.ALWAYS:
|
|
475
|
+
return True
|
|
476
|
+
elif condition == NotifyCondition.SUCCESS:
|
|
477
|
+
return result.status.value == "success"
|
|
478
|
+
elif condition == NotifyCondition.FAILURE:
|
|
479
|
+
return result.status.value == "failure"
|
|
480
|
+
elif condition == NotifyCondition.ERROR:
|
|
481
|
+
return result.status.value == "error"
|
|
482
|
+
elif condition == NotifyCondition.FAILURE_OR_ERROR:
|
|
483
|
+
return result.status.value in ("failure", "error")
|
|
484
|
+
elif condition == NotifyCondition.NOT_SUCCESS:
|
|
485
|
+
return result.status.value != "success"
|
|
486
|
+
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
async def execute_async(self, context: ActionContext) -> ActionResult:
|
|
490
|
+
"""Execute the action asynchronously."""
|
|
491
|
+
if not self.should_execute(context):
|
|
492
|
+
return ActionResult(
|
|
493
|
+
action_name=self.name,
|
|
494
|
+
action_type=self.action_type,
|
|
495
|
+
status=ActionStatus.SKIPPED,
|
|
496
|
+
message=f"Skipped due to notify_on={self._config.notify_on.value}",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
started_at = datetime.now()
|
|
500
|
+
try:
|
|
501
|
+
result = await self._do_execute_async(context)
|
|
502
|
+
completed_at = datetime.now()
|
|
503
|
+
result.started_at = started_at
|
|
504
|
+
result.completed_at = completed_at
|
|
505
|
+
result.duration_ms = (completed_at - started_at).total_seconds() * 1000
|
|
506
|
+
return result
|
|
507
|
+
except Exception as e:
|
|
508
|
+
completed_at = datetime.now()
|
|
509
|
+
return ActionResult(
|
|
510
|
+
action_name=self.name,
|
|
511
|
+
action_type=self.action_type,
|
|
512
|
+
status=ActionStatus.FAILURE,
|
|
513
|
+
message=f"Action failed: {str(e)}",
|
|
514
|
+
started_at=started_at,
|
|
515
|
+
completed_at=completed_at,
|
|
516
|
+
duration_ms=(completed_at - started_at).total_seconds() * 1000,
|
|
517
|
+
error=str(e),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
@abstractmethod
|
|
521
|
+
async def _do_execute_async(self, context: ActionContext) -> ActionResult:
|
|
522
|
+
"""Perform the actual async action execution."""
|
|
523
|
+
...
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# =============================================================================
|
|
527
|
+
# Action Registry
|
|
528
|
+
# =============================================================================
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class ActionRegistry:
|
|
532
|
+
"""Registry for action types.
|
|
533
|
+
|
|
534
|
+
Allows registering and retrieving action implementations by name.
|
|
535
|
+
Supports both built-in and custom action types.
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
registry = ActionRegistry()
|
|
539
|
+
registry.register("slack", SlackNotificationAction)
|
|
540
|
+
registry.register("email", EmailNotificationAction)
|
|
541
|
+
|
|
542
|
+
action = registry.create("slack", config={"webhook_url": "..."})
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
def __init__(self) -> None:
|
|
546
|
+
"""Initialize registry."""
|
|
547
|
+
self._actions: dict[str, type[BaseAction | AsyncBaseAction]] = {}
|
|
548
|
+
self._factories: dict[str, Callable[..., BaseAction | AsyncBaseAction]] = {}
|
|
549
|
+
|
|
550
|
+
def register(
|
|
551
|
+
self,
|
|
552
|
+
name: str,
|
|
553
|
+
action_class: type[BaseAction | AsyncBaseAction],
|
|
554
|
+
) -> None:
|
|
555
|
+
"""Register an action class.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
name: Action type name.
|
|
559
|
+
action_class: Action class to register.
|
|
560
|
+
"""
|
|
561
|
+
self._actions[name] = action_class
|
|
562
|
+
|
|
563
|
+
def register_factory(
|
|
564
|
+
self,
|
|
565
|
+
name: str,
|
|
566
|
+
factory: Callable[..., BaseAction | AsyncBaseAction],
|
|
567
|
+
) -> None:
|
|
568
|
+
"""Register an action factory function.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
name: Action type name.
|
|
572
|
+
factory: Factory function that creates actions.
|
|
573
|
+
"""
|
|
574
|
+
self._factories[name] = factory
|
|
575
|
+
|
|
576
|
+
def create(
|
|
577
|
+
self,
|
|
578
|
+
name: str,
|
|
579
|
+
config: ActionConfig | dict[str, Any] | None = None,
|
|
580
|
+
**kwargs: Any,
|
|
581
|
+
) -> BaseAction | AsyncBaseAction:
|
|
582
|
+
"""Create an action instance.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
name: Action type name.
|
|
586
|
+
config: Action configuration.
|
|
587
|
+
**kwargs: Additional arguments for the action.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Action instance.
|
|
591
|
+
|
|
592
|
+
Raises:
|
|
593
|
+
KeyError: If action type is not registered.
|
|
594
|
+
"""
|
|
595
|
+
if name in self._factories:
|
|
596
|
+
return self._factories[name](config=config, **kwargs)
|
|
597
|
+
|
|
598
|
+
if name in self._actions:
|
|
599
|
+
return self._actions[name](config=config, **kwargs)
|
|
600
|
+
|
|
601
|
+
raise KeyError(f"Action type not found: {name}")
|
|
602
|
+
|
|
603
|
+
def list_actions(self) -> list[str]:
|
|
604
|
+
"""List all registered action types.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
List of action type names.
|
|
608
|
+
"""
|
|
609
|
+
return list(set(self._actions.keys()) | set(self._factories.keys()))
|
|
610
|
+
|
|
611
|
+
def has_action(self, name: str) -> bool:
|
|
612
|
+
"""Check if an action type is registered.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
name: Action type name.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
True if action type is registered.
|
|
619
|
+
"""
|
|
620
|
+
return name in self._actions or name in self._factories
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# Global action registry
|
|
624
|
+
_action_registry: ActionRegistry | None = None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def get_action_registry() -> ActionRegistry:
|
|
628
|
+
"""Get the global action registry.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Global ActionRegistry instance.
|
|
632
|
+
"""
|
|
633
|
+
global _action_registry
|
|
634
|
+
if _action_registry is None:
|
|
635
|
+
_action_registry = ActionRegistry()
|
|
636
|
+
return _action_registry
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def register_action(name: str) -> Callable[[type], type]:
|
|
640
|
+
"""Decorator to register an action class.
|
|
641
|
+
|
|
642
|
+
Example:
|
|
643
|
+
@register_action("my_custom")
|
|
644
|
+
class MyCustomAction(BaseAction):
|
|
645
|
+
...
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
def decorator(cls: type) -> type:
|
|
649
|
+
get_action_registry().register(name, cls)
|
|
650
|
+
return cls
|
|
651
|
+
|
|
652
|
+
return decorator
|