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.
- 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 +437 -10
- 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 +11 -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.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.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.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""Trigger interfaces for checkpoint-based validation pipelines.
|
|
2
|
+
|
|
3
|
+
Triggers initiate validation runs. They can be time-based, event-based,
|
|
4
|
+
or manually invoked.
|
|
5
|
+
|
|
6
|
+
This module defines abstract interfaces for triggers that are loosely
|
|
7
|
+
coupled from truthound's checkpoint.triggers module.
|
|
8
|
+
|
|
9
|
+
Supported trigger types:
|
|
10
|
+
- Schedule/Cron: Time-based triggers
|
|
11
|
+
- FileWatch: File system event triggers
|
|
12
|
+
- Webhook: HTTP webhook triggers
|
|
13
|
+
- Event: Pub/sub event triggers
|
|
14
|
+
- Manual: Manual invocation
|
|
15
|
+
- Pipeline: Integration with data pipelines (Airflow, Dagster, etc.)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TriggerStatus(str, Enum):
|
|
31
|
+
"""Status of a trigger execution."""
|
|
32
|
+
|
|
33
|
+
PENDING = "pending"
|
|
34
|
+
RUNNING = "running"
|
|
35
|
+
SUCCESS = "success"
|
|
36
|
+
FAILURE = "failure"
|
|
37
|
+
SKIPPED = "skipped"
|
|
38
|
+
DISABLED = "disabled"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TriggerType(str, Enum):
|
|
42
|
+
"""Types of triggers."""
|
|
43
|
+
|
|
44
|
+
CRON = "cron"
|
|
45
|
+
SCHEDULE = "schedule"
|
|
46
|
+
FILE_WATCH = "file_watch"
|
|
47
|
+
WEBHOOK = "webhook"
|
|
48
|
+
EVENT = "event"
|
|
49
|
+
MANUAL = "manual"
|
|
50
|
+
PIPELINE = "pipeline"
|
|
51
|
+
DATA_ARRIVAL = "data_arrival"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class TriggerConfig:
|
|
56
|
+
"""Base configuration for all triggers.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
name: Trigger name for identification.
|
|
60
|
+
trigger_type: Type of trigger.
|
|
61
|
+
enabled: Whether this trigger is enabled.
|
|
62
|
+
description: Human-readable description.
|
|
63
|
+
metadata: Additional metadata.
|
|
64
|
+
checkpoint_id: ID of checkpoint to run.
|
|
65
|
+
checkpoint_name: Name of checkpoint to run.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name: str = ""
|
|
69
|
+
trigger_type: TriggerType = TriggerType.MANUAL
|
|
70
|
+
enabled: bool = True
|
|
71
|
+
description: str = ""
|
|
72
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
checkpoint_id: str | None = None
|
|
74
|
+
checkpoint_name: str | None = None
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, Any]:
|
|
77
|
+
"""Convert to dictionary."""
|
|
78
|
+
return {
|
|
79
|
+
"name": self.name,
|
|
80
|
+
"trigger_type": self.trigger_type.value,
|
|
81
|
+
"enabled": self.enabled,
|
|
82
|
+
"description": self.description,
|
|
83
|
+
"metadata": self.metadata,
|
|
84
|
+
"checkpoint_id": self.checkpoint_id,
|
|
85
|
+
"checkpoint_name": self.checkpoint_name,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_dict(cls, data: dict[str, Any]) -> "TriggerConfig":
|
|
90
|
+
"""Create from dictionary."""
|
|
91
|
+
trigger_type = data.get("trigger_type", "manual")
|
|
92
|
+
if isinstance(trigger_type, str):
|
|
93
|
+
trigger_type = TriggerType(trigger_type)
|
|
94
|
+
return cls(
|
|
95
|
+
name=data.get("name", ""),
|
|
96
|
+
trigger_type=trigger_type,
|
|
97
|
+
enabled=data.get("enabled", True),
|
|
98
|
+
description=data.get("description", ""),
|
|
99
|
+
metadata=data.get("metadata", {}),
|
|
100
|
+
checkpoint_id=data.get("checkpoint_id"),
|
|
101
|
+
checkpoint_name=data.get("checkpoint_name"),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class CronTriggerConfig(TriggerConfig):
|
|
107
|
+
"""Configuration for cron-based triggers.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
cron_expression: Cron expression (e.g., "0 * * * *" for hourly).
|
|
111
|
+
timezone: Timezone for cron evaluation.
|
|
112
|
+
start_date: Earliest date to start running.
|
|
113
|
+
end_date: Latest date to stop running.
|
|
114
|
+
max_runs: Maximum number of runs (None for unlimited).
|
|
115
|
+
catchup: Whether to catch up missed runs.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
cron_expression: str = "0 * * * *" # Hourly
|
|
119
|
+
timezone: str = "UTC"
|
|
120
|
+
start_date: datetime | None = None
|
|
121
|
+
end_date: datetime | None = None
|
|
122
|
+
max_runs: int | None = None
|
|
123
|
+
catchup: bool = False
|
|
124
|
+
|
|
125
|
+
def __post_init__(self):
|
|
126
|
+
self.trigger_type = TriggerType.CRON
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> dict[str, Any]:
|
|
129
|
+
"""Convert to dictionary."""
|
|
130
|
+
data = super().to_dict()
|
|
131
|
+
data.update({
|
|
132
|
+
"cron_expression": self.cron_expression,
|
|
133
|
+
"timezone": self.timezone,
|
|
134
|
+
"start_date": self.start_date.isoformat() if self.start_date else None,
|
|
135
|
+
"end_date": self.end_date.isoformat() if self.end_date else None,
|
|
136
|
+
"max_runs": self.max_runs,
|
|
137
|
+
"catchup": self.catchup,
|
|
138
|
+
})
|
|
139
|
+
return data
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class FileWatchTriggerConfig(TriggerConfig):
|
|
144
|
+
"""Configuration for file watch triggers.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
path: Path to watch (can be glob pattern).
|
|
148
|
+
events: File events to watch for.
|
|
149
|
+
debounce_seconds: Debounce time to avoid rapid triggers.
|
|
150
|
+
recursive: Whether to watch subdirectories.
|
|
151
|
+
include_patterns: Patterns to include.
|
|
152
|
+
exclude_patterns: Patterns to exclude.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
path: str = ""
|
|
156
|
+
events: list[str] = field(default_factory=lambda: ["created", "modified"])
|
|
157
|
+
debounce_seconds: float = 5.0
|
|
158
|
+
recursive: bool = False
|
|
159
|
+
include_patterns: list[str] = field(default_factory=list)
|
|
160
|
+
exclude_patterns: list[str] = field(default_factory=list)
|
|
161
|
+
|
|
162
|
+
def __post_init__(self):
|
|
163
|
+
self.trigger_type = TriggerType.FILE_WATCH
|
|
164
|
+
|
|
165
|
+
def to_dict(self) -> dict[str, Any]:
|
|
166
|
+
"""Convert to dictionary."""
|
|
167
|
+
data = super().to_dict()
|
|
168
|
+
data.update({
|
|
169
|
+
"path": self.path,
|
|
170
|
+
"events": self.events,
|
|
171
|
+
"debounce_seconds": self.debounce_seconds,
|
|
172
|
+
"recursive": self.recursive,
|
|
173
|
+
"include_patterns": self.include_patterns,
|
|
174
|
+
"exclude_patterns": self.exclude_patterns,
|
|
175
|
+
})
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class WebhookTriggerConfig(TriggerConfig):
|
|
181
|
+
"""Configuration for webhook triggers.
|
|
182
|
+
|
|
183
|
+
Attributes:
|
|
184
|
+
path: Webhook endpoint path.
|
|
185
|
+
methods: HTTP methods to accept.
|
|
186
|
+
require_auth: Whether authentication is required.
|
|
187
|
+
auth_header: Header name for authentication.
|
|
188
|
+
secret: Secret for HMAC validation.
|
|
189
|
+
payload_schema: JSON schema for payload validation.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
path: str = "/triggers/webhook"
|
|
193
|
+
methods: list[str] = field(default_factory=lambda: ["POST"])
|
|
194
|
+
require_auth: bool = False
|
|
195
|
+
auth_header: str = "X-Trigger-Secret"
|
|
196
|
+
secret: str = ""
|
|
197
|
+
payload_schema: dict[str, Any] | None = None
|
|
198
|
+
|
|
199
|
+
def __post_init__(self):
|
|
200
|
+
self.trigger_type = TriggerType.WEBHOOK
|
|
201
|
+
|
|
202
|
+
def to_dict(self) -> dict[str, Any]:
|
|
203
|
+
"""Convert to dictionary."""
|
|
204
|
+
data = super().to_dict()
|
|
205
|
+
data.update({
|
|
206
|
+
"path": self.path,
|
|
207
|
+
"methods": self.methods,
|
|
208
|
+
"require_auth": self.require_auth,
|
|
209
|
+
"auth_header": self.auth_header,
|
|
210
|
+
# Don't expose secret
|
|
211
|
+
"payload_schema": self.payload_schema,
|
|
212
|
+
})
|
|
213
|
+
return data
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass
|
|
217
|
+
class EventTriggerConfig(TriggerConfig):
|
|
218
|
+
"""Configuration for event-based triggers (pub/sub).
|
|
219
|
+
|
|
220
|
+
Attributes:
|
|
221
|
+
event_type: Type of event to listen for.
|
|
222
|
+
event_source: Source of events.
|
|
223
|
+
filter_expression: Expression to filter events.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
event_type: str = ""
|
|
227
|
+
event_source: str = ""
|
|
228
|
+
filter_expression: str = ""
|
|
229
|
+
|
|
230
|
+
def __post_init__(self):
|
|
231
|
+
self.trigger_type = TriggerType.EVENT
|
|
232
|
+
|
|
233
|
+
def to_dict(self) -> dict[str, Any]:
|
|
234
|
+
"""Convert to dictionary."""
|
|
235
|
+
data = super().to_dict()
|
|
236
|
+
data.update({
|
|
237
|
+
"event_type": self.event_type,
|
|
238
|
+
"event_source": self.event_source,
|
|
239
|
+
"filter_expression": self.filter_expression,
|
|
240
|
+
})
|
|
241
|
+
return data
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class DataArrivalTriggerConfig(TriggerConfig):
|
|
246
|
+
"""Configuration for data arrival triggers.
|
|
247
|
+
|
|
248
|
+
Triggers when new data arrives at a source.
|
|
249
|
+
|
|
250
|
+
Attributes:
|
|
251
|
+
source_id: Data source to monitor.
|
|
252
|
+
check_interval_seconds: How often to check for new data.
|
|
253
|
+
min_rows: Minimum rows to trigger.
|
|
254
|
+
max_wait_seconds: Maximum time to wait for data.
|
|
255
|
+
watermark_column: Column to track data arrival.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
source_id: str = ""
|
|
259
|
+
check_interval_seconds: int = 60
|
|
260
|
+
min_rows: int = 1
|
|
261
|
+
max_wait_seconds: int = 3600
|
|
262
|
+
watermark_column: str | None = None
|
|
263
|
+
|
|
264
|
+
def __post_init__(self):
|
|
265
|
+
self.trigger_type = TriggerType.DATA_ARRIVAL
|
|
266
|
+
|
|
267
|
+
def to_dict(self) -> dict[str, Any]:
|
|
268
|
+
"""Convert to dictionary."""
|
|
269
|
+
data = super().to_dict()
|
|
270
|
+
data.update({
|
|
271
|
+
"source_id": self.source_id,
|
|
272
|
+
"check_interval_seconds": self.check_interval_seconds,
|
|
273
|
+
"min_rows": self.min_rows,
|
|
274
|
+
"max_wait_seconds": self.max_wait_seconds,
|
|
275
|
+
"watermark_column": self.watermark_column,
|
|
276
|
+
})
|
|
277
|
+
return data
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass
|
|
281
|
+
class TriggerResult:
|
|
282
|
+
"""Result of a trigger execution.
|
|
283
|
+
|
|
284
|
+
Attributes:
|
|
285
|
+
trigger_name: Name of the trigger.
|
|
286
|
+
trigger_type: Type of trigger.
|
|
287
|
+
status: Execution status.
|
|
288
|
+
message: Human-readable message.
|
|
289
|
+
triggered_at: When the trigger fired.
|
|
290
|
+
run_id: ID of the initiated run (if any).
|
|
291
|
+
context: Additional context from the trigger.
|
|
292
|
+
error: Error message if failed.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
trigger_name: str
|
|
296
|
+
trigger_type: str
|
|
297
|
+
status: TriggerStatus
|
|
298
|
+
message: str = ""
|
|
299
|
+
triggered_at: datetime | None = None
|
|
300
|
+
run_id: str | None = None
|
|
301
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
302
|
+
error: str | None = None
|
|
303
|
+
|
|
304
|
+
def to_dict(self) -> dict[str, Any]:
|
|
305
|
+
"""Convert to dictionary."""
|
|
306
|
+
return {
|
|
307
|
+
"trigger_name": self.trigger_name,
|
|
308
|
+
"trigger_type": self.trigger_type,
|
|
309
|
+
"status": self.status.value,
|
|
310
|
+
"message": self.message,
|
|
311
|
+
"triggered_at": self.triggered_at.isoformat() if self.triggered_at else None,
|
|
312
|
+
"run_id": self.run_id,
|
|
313
|
+
"context": self.context,
|
|
314
|
+
"error": self.error,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@runtime_checkable
|
|
319
|
+
class TriggerProtocol(Protocol):
|
|
320
|
+
"""Protocol for trigger implementations.
|
|
321
|
+
|
|
322
|
+
Triggers monitor for conditions and initiate checkpoint runs
|
|
323
|
+
when conditions are met.
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
class CronTrigger:
|
|
327
|
+
def is_due(self) -> bool:
|
|
328
|
+
return cron_is_due(self.expression)
|
|
329
|
+
|
|
330
|
+
def trigger(self) -> TriggerResult:
|
|
331
|
+
if self.is_due():
|
|
332
|
+
run_id = run_checkpoint(self.checkpoint_id)
|
|
333
|
+
return TriggerResult(status=TriggerStatus.SUCCESS, run_id=run_id)
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def name(self) -> str:
|
|
338
|
+
"""Get trigger name."""
|
|
339
|
+
...
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def trigger_type(self) -> str:
|
|
343
|
+
"""Get trigger type."""
|
|
344
|
+
...
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def config(self) -> TriggerConfig:
|
|
348
|
+
"""Get trigger configuration."""
|
|
349
|
+
...
|
|
350
|
+
|
|
351
|
+
def is_enabled(self) -> bool:
|
|
352
|
+
"""Check if trigger is enabled."""
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
def is_due(self) -> bool:
|
|
356
|
+
"""Check if trigger should fire.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
True if trigger conditions are met.
|
|
360
|
+
"""
|
|
361
|
+
...
|
|
362
|
+
|
|
363
|
+
def trigger(self, context: dict[str, Any] | None = None) -> TriggerResult:
|
|
364
|
+
"""Fire the trigger and initiate a checkpoint run.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
context: Optional context to pass to the checkpoint.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Trigger result.
|
|
371
|
+
"""
|
|
372
|
+
...
|
|
373
|
+
|
|
374
|
+
def get_next_run_time(self) -> datetime | None:
|
|
375
|
+
"""Get the next scheduled run time.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Next run time or None if not applicable.
|
|
379
|
+
"""
|
|
380
|
+
...
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class BaseTrigger(ABC):
|
|
384
|
+
"""Abstract base class for triggers.
|
|
385
|
+
|
|
386
|
+
Provides common functionality for all triggers.
|
|
387
|
+
Subclasses must implement is_due and _do_trigger methods.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def __init__(self, config: TriggerConfig | dict[str, Any] | None = None) -> None:
|
|
391
|
+
"""Initialize trigger.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
config: Trigger configuration or dict.
|
|
395
|
+
"""
|
|
396
|
+
if config is None:
|
|
397
|
+
self._config = TriggerConfig()
|
|
398
|
+
elif isinstance(config, dict):
|
|
399
|
+
self._config = TriggerConfig.from_dict(config)
|
|
400
|
+
else:
|
|
401
|
+
self._config = config
|
|
402
|
+
|
|
403
|
+
self._last_triggered: datetime | None = None
|
|
404
|
+
self._trigger_count: int = 0
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def name(self) -> str:
|
|
408
|
+
"""Get trigger name."""
|
|
409
|
+
return self._config.name or self.__class__.__name__
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
@abstractmethod
|
|
413
|
+
def trigger_type(self) -> str:
|
|
414
|
+
"""Get trigger type."""
|
|
415
|
+
...
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def config(self) -> TriggerConfig:
|
|
419
|
+
"""Get trigger configuration."""
|
|
420
|
+
return self._config
|
|
421
|
+
|
|
422
|
+
def is_enabled(self) -> bool:
|
|
423
|
+
"""Check if trigger is enabled."""
|
|
424
|
+
return self._config.enabled
|
|
425
|
+
|
|
426
|
+
@abstractmethod
|
|
427
|
+
def is_due(self) -> bool:
|
|
428
|
+
"""Check if trigger should fire."""
|
|
429
|
+
...
|
|
430
|
+
|
|
431
|
+
def trigger(self, context: dict[str, Any] | None = None) -> TriggerResult:
|
|
432
|
+
"""Fire the trigger.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
context: Optional context.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Trigger result.
|
|
439
|
+
"""
|
|
440
|
+
if not self.is_enabled():
|
|
441
|
+
return TriggerResult(
|
|
442
|
+
trigger_name=self.name,
|
|
443
|
+
trigger_type=self.trigger_type,
|
|
444
|
+
status=TriggerStatus.DISABLED,
|
|
445
|
+
message="Trigger is disabled",
|
|
446
|
+
triggered_at=datetime.now(),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if not self.is_due():
|
|
450
|
+
return TriggerResult(
|
|
451
|
+
trigger_name=self.name,
|
|
452
|
+
trigger_type=self.trigger_type,
|
|
453
|
+
status=TriggerStatus.SKIPPED,
|
|
454
|
+
message="Trigger conditions not met",
|
|
455
|
+
triggered_at=datetime.now(),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
result = self._do_trigger(context)
|
|
460
|
+
self._last_triggered = datetime.now()
|
|
461
|
+
self._trigger_count += 1
|
|
462
|
+
return result
|
|
463
|
+
except Exception as e:
|
|
464
|
+
return TriggerResult(
|
|
465
|
+
trigger_name=self.name,
|
|
466
|
+
trigger_type=self.trigger_type,
|
|
467
|
+
status=TriggerStatus.FAILURE,
|
|
468
|
+
message=f"Trigger failed: {str(e)}",
|
|
469
|
+
triggered_at=datetime.now(),
|
|
470
|
+
error=str(e),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
@abstractmethod
|
|
474
|
+
def _do_trigger(self, context: dict[str, Any] | None) -> TriggerResult:
|
|
475
|
+
"""Perform the actual trigger execution.
|
|
476
|
+
|
|
477
|
+
Subclasses must implement this method.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
context: Optional context.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Trigger result.
|
|
484
|
+
"""
|
|
485
|
+
...
|
|
486
|
+
|
|
487
|
+
def get_next_run_time(self) -> datetime | None:
|
|
488
|
+
"""Get the next scheduled run time.
|
|
489
|
+
|
|
490
|
+
Override in subclasses for scheduled triggers.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Next run time or None.
|
|
494
|
+
"""
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# =============================================================================
|
|
499
|
+
# Trigger Registry
|
|
500
|
+
# =============================================================================
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class TriggerRegistry:
|
|
504
|
+
"""Registry for trigger types.
|
|
505
|
+
|
|
506
|
+
Allows registering and retrieving trigger implementations by name.
|
|
507
|
+
|
|
508
|
+
Example:
|
|
509
|
+
registry = TriggerRegistry()
|
|
510
|
+
registry.register("cron", CronTrigger)
|
|
511
|
+
registry.register("file_watch", FileWatchTrigger)
|
|
512
|
+
|
|
513
|
+
trigger = registry.create("cron", config={"cron_expression": "0 * * * *"})
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
def __init__(self) -> None:
|
|
517
|
+
"""Initialize registry."""
|
|
518
|
+
self._triggers: dict[str, type[BaseTrigger]] = {}
|
|
519
|
+
self._factories: dict[str, Callable[..., BaseTrigger]] = {}
|
|
520
|
+
|
|
521
|
+
def register(self, name: str, trigger_class: type[BaseTrigger]) -> None:
|
|
522
|
+
"""Register a trigger class.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
name: Trigger type name.
|
|
526
|
+
trigger_class: Trigger class to register.
|
|
527
|
+
"""
|
|
528
|
+
self._triggers[name] = trigger_class
|
|
529
|
+
|
|
530
|
+
def register_factory(
|
|
531
|
+
self,
|
|
532
|
+
name: str,
|
|
533
|
+
factory: Callable[..., BaseTrigger],
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Register a trigger factory function.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
name: Trigger type name.
|
|
539
|
+
factory: Factory function that creates triggers.
|
|
540
|
+
"""
|
|
541
|
+
self._factories[name] = factory
|
|
542
|
+
|
|
543
|
+
def create(
|
|
544
|
+
self,
|
|
545
|
+
name: str,
|
|
546
|
+
config: TriggerConfig | dict[str, Any] | None = None,
|
|
547
|
+
**kwargs: Any,
|
|
548
|
+
) -> BaseTrigger:
|
|
549
|
+
"""Create a trigger instance.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
name: Trigger type name.
|
|
553
|
+
config: Trigger configuration.
|
|
554
|
+
**kwargs: Additional arguments for the trigger.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Trigger instance.
|
|
558
|
+
|
|
559
|
+
Raises:
|
|
560
|
+
KeyError: If trigger type is not registered.
|
|
561
|
+
"""
|
|
562
|
+
if name in self._factories:
|
|
563
|
+
return self._factories[name](config=config, **kwargs)
|
|
564
|
+
|
|
565
|
+
if name in self._triggers:
|
|
566
|
+
return self._triggers[name](config=config, **kwargs)
|
|
567
|
+
|
|
568
|
+
raise KeyError(f"Trigger type not found: {name}")
|
|
569
|
+
|
|
570
|
+
def list_triggers(self) -> list[str]:
|
|
571
|
+
"""List all registered trigger types.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
List of trigger type names.
|
|
575
|
+
"""
|
|
576
|
+
return list(set(self._triggers.keys()) | set(self._factories.keys()))
|
|
577
|
+
|
|
578
|
+
def has_trigger(self, name: str) -> bool:
|
|
579
|
+
"""Check if a trigger type is registered.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
name: Trigger type name.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
True if trigger type is registered.
|
|
586
|
+
"""
|
|
587
|
+
return name in self._triggers or name in self._factories
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# Global trigger registry
|
|
591
|
+
_trigger_registry: TriggerRegistry | None = None
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def get_trigger_registry() -> TriggerRegistry:
|
|
595
|
+
"""Get the global trigger registry.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Global TriggerRegistry instance.
|
|
599
|
+
"""
|
|
600
|
+
global _trigger_registry
|
|
601
|
+
if _trigger_registry is None:
|
|
602
|
+
_trigger_registry = TriggerRegistry()
|
|
603
|
+
return _trigger_registry
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def register_trigger(name: str) -> Callable[[type], type]:
|
|
607
|
+
"""Decorator to register a trigger class.
|
|
608
|
+
|
|
609
|
+
Example:
|
|
610
|
+
@register_trigger("my_custom")
|
|
611
|
+
class MyCustomTrigger(BaseTrigger):
|
|
612
|
+
...
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
def decorator(cls: type) -> type:
|
|
616
|
+
get_trigger_registry().register(name, cls)
|
|
617
|
+
return cls
|
|
618
|
+
|
|
619
|
+
return decorator
|