truthound-dashboard 1.4.3__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.3.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.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.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