truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -12,8 +12,26 @@ from pydantic import Field, field_validator
12
12
 
13
13
  from .base import BaseSchema, IDMixin, ListResponseWrapper, TimestampMixin
14
14
 
15
- # Supported source types
16
- SourceType = Literal["file", "postgresql", "mysql", "snowflake", "bigquery"]
15
+ # Supported source types - must match SourceType enum in connections.py
16
+ SourceType = Literal[
17
+ "file",
18
+ "postgresql",
19
+ "mysql",
20
+ "sqlite",
21
+ "snowflake",
22
+ "bigquery",
23
+ "redshift",
24
+ "databricks",
25
+ "oracle",
26
+ "sqlserver",
27
+ "spark",
28
+ ]
29
+
30
+ # Source type categories for UI grouping
31
+ SourceCategory = Literal["file", "database", "warehouse", "bigdata"]
32
+
33
+ # Field types for dynamic form rendering
34
+ FieldType = Literal["text", "password", "number", "select", "boolean", "file_path", "textarea"]
17
35
 
18
36
 
19
37
  class SourceBase(BaseSchema):
@@ -136,3 +154,100 @@ class SourceSummary(BaseSchema):
136
154
  type: SourceType
137
155
  is_active: bool
138
156
  last_validated_at: datetime | None = None
157
+
158
+
159
+ # =============================================================================
160
+ # Source Type Definition Schemas (for dynamic form rendering)
161
+ # =============================================================================
162
+
163
+
164
+ class FieldOption(BaseSchema):
165
+ """Option for select/multi-select fields."""
166
+
167
+ value: str
168
+ label: str
169
+
170
+
171
+ class FieldDefinitionSchema(BaseSchema):
172
+ """Definition of a configuration field for dynamic form rendering."""
173
+
174
+ name: str = Field(..., description="Field identifier")
175
+ label: str = Field(..., description="Display label")
176
+ type: FieldType = Field(default="text", description="Input field type")
177
+ required: bool = Field(default=False, description="Whether field is required")
178
+ placeholder: str = Field(default="", description="Input placeholder text")
179
+ description: str = Field(default="", description="Help text for the field")
180
+ default: Any = Field(default=None, description="Default value")
181
+ options: list[FieldOption] | None = Field(
182
+ default=None,
183
+ description="Options for select fields",
184
+ )
185
+ min_value: int | None = Field(default=None, description="Minimum value for numbers")
186
+ max_value: int | None = Field(default=None, description="Maximum value for numbers")
187
+ depends_on: str | None = Field(
188
+ default=None,
189
+ description="Field this depends on for conditional rendering",
190
+ )
191
+ depends_value: Any = Field(
192
+ default=None,
193
+ description="Value of depends_on field that enables this field",
194
+ )
195
+
196
+
197
+ class SourceTypeDefinitionSchema(BaseSchema):
198
+ """Complete definition of a source type for dynamic form rendering."""
199
+
200
+ type: SourceType = Field(..., description="Source type identifier")
201
+ name: str = Field(..., description="Display name")
202
+ description: str = Field(..., description="Type description")
203
+ icon: str = Field(..., description="Icon identifier for UI")
204
+ category: SourceCategory = Field(..., description="Source category")
205
+ fields: list[FieldDefinitionSchema] = Field(
206
+ ...,
207
+ description="Configuration fields",
208
+ )
209
+ required_fields: list[str] = Field(
210
+ default_factory=list,
211
+ description="List of required field names",
212
+ )
213
+ optional_fields: list[str] = Field(
214
+ default_factory=list,
215
+ description="List of optional field names",
216
+ )
217
+ docs_url: str = Field(default="", description="Documentation URL")
218
+
219
+
220
+ class SourceTypeCategorySchema(BaseSchema):
221
+ """Category for grouping source types."""
222
+
223
+ value: str = Field(..., description="Category identifier")
224
+ label: str = Field(..., description="Display label")
225
+ description: str = Field(default="", description="Category description")
226
+
227
+
228
+ class SourceTypesResponse(BaseSchema):
229
+ """Response containing all source types and categories."""
230
+
231
+ types: list[SourceTypeDefinitionSchema] = Field(
232
+ ...,
233
+ description="All available source types",
234
+ )
235
+ categories: list[SourceTypeCategorySchema] = Field(
236
+ ...,
237
+ description="Source type categories",
238
+ )
239
+
240
+
241
+ class TestConnectionRequest(BaseSchema):
242
+ """Request to test a source connection before creating."""
243
+
244
+ type: SourceType = Field(..., description="Source type")
245
+ config: dict[str, Any] = Field(..., description="Connection configuration")
246
+
247
+
248
+ class TestConnectionResponse(BaseSchema):
249
+ """Response from connection test."""
250
+
251
+ success: bool = Field(..., description="Whether connection was successful")
252
+ message: str | None = Field(default=None, description="Success message")
253
+ error: str | None = Field(default=None, description="Error message if failed")
@@ -0,0 +1,511 @@
1
+ """Trigger system schemas for advanced scheduling.
2
+
3
+ This module provides schemas for different trigger types:
4
+ - Cron: Traditional cron-based scheduling
5
+ - Interval: Fixed time interval scheduling
6
+ - DataChange: Trigger when data changes by threshold
7
+ - Composite: Combine multiple triggers with AND/OR logic
8
+
9
+ Following truthound library's scheduling capabilities from Phase 7.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import datetime
15
+ from enum import Enum
16
+ from typing import Any, Literal
17
+
18
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
19
+
20
+
21
+ class TriggerType(str, Enum):
22
+ """Supported trigger types."""
23
+
24
+ CRON = "cron"
25
+ INTERVAL = "interval"
26
+ DATA_CHANGE = "data_change"
27
+ COMPOSITE = "composite"
28
+ EVENT = "event"
29
+ MANUAL = "manual"
30
+ WEBHOOK = "webhook" # External webhook triggers
31
+
32
+
33
+ class TriggerOperator(str, Enum):
34
+ """Operator for composite triggers."""
35
+
36
+ AND = "and"
37
+ OR = "or"
38
+
39
+
40
+ class BaseTriggerConfig(BaseModel):
41
+ """Base configuration for all trigger types."""
42
+
43
+ model_config = ConfigDict(extra="forbid")
44
+
45
+ type: TriggerType = Field(..., description="Trigger type")
46
+ enabled: bool = Field(default=True, description="Whether trigger is enabled")
47
+ description: str | None = Field(default=None, description="Optional description")
48
+
49
+
50
+ class CronTriggerConfig(BaseTriggerConfig):
51
+ """Cron-based trigger configuration.
52
+
53
+ Uses standard cron expression format: minute hour day month weekday
54
+
55
+ Example:
56
+ "0 0 * * *" - Daily at midnight
57
+ "0 */6 * * *" - Every 6 hours
58
+ "0 8 * * 1-5" - Weekdays at 8am
59
+ """
60
+
61
+ type: Literal[TriggerType.CRON] = TriggerType.CRON
62
+ expression: str = Field(
63
+ ...,
64
+ description="Cron expression (minute hour day month weekday)",
65
+ examples=["0 0 * * *", "0 */6 * * *", "0 8 * * 1-5"],
66
+ )
67
+ timezone: str = Field(default="UTC", description="Timezone for cron schedule")
68
+
69
+ @field_validator("expression")
70
+ @classmethod
71
+ def validate_cron_expression(cls, v: str) -> str:
72
+ """Validate cron expression format."""
73
+ parts = v.split()
74
+ if len(parts) != 5:
75
+ raise ValueError(
76
+ "Cron expression must have 5 parts: minute hour day month weekday"
77
+ )
78
+ return v
79
+
80
+
81
+ class IntervalTriggerConfig(BaseTriggerConfig):
82
+ """Interval-based trigger configuration.
83
+
84
+ Triggers at fixed time intervals.
85
+
86
+ Example:
87
+ seconds=3600 - Every hour
88
+ minutes=30 - Every 30 minutes
89
+ hours=6 - Every 6 hours
90
+ """
91
+
92
+ type: Literal[TriggerType.INTERVAL] = TriggerType.INTERVAL
93
+ seconds: int | None = Field(default=None, ge=1, description="Interval in seconds")
94
+ minutes: int | None = Field(default=None, ge=1, description="Interval in minutes")
95
+ hours: int | None = Field(default=None, ge=1, description="Interval in hours")
96
+ days: int | None = Field(default=None, ge=1, description="Interval in days")
97
+
98
+ @field_validator("seconds", "minutes", "hours", "days", mode="after")
99
+ @classmethod
100
+ def at_least_one_interval(cls, v: int | None, info) -> int | None:
101
+ """Ensure at least one interval is specified."""
102
+ return v
103
+
104
+ def get_total_seconds(self) -> int:
105
+ """Calculate total interval in seconds."""
106
+ total = 0
107
+ if self.seconds:
108
+ total += self.seconds
109
+ if self.minutes:
110
+ total += self.minutes * 60
111
+ if self.hours:
112
+ total += self.hours * 3600
113
+ if self.days:
114
+ total += self.days * 86400
115
+ return total or 3600 # Default to 1 hour
116
+
117
+
118
+ class DataChangeTriggerConfig(BaseTriggerConfig):
119
+ """Data change trigger configuration.
120
+
121
+ Triggers when profile changes exceed a threshold percentage.
122
+ Monitors row count, column statistics, or schema changes.
123
+
124
+ Example:
125
+ change_threshold=0.05 - Trigger when >= 5% change detected
126
+ metrics=["row_count", "null_percentage"] - What to monitor
127
+ """
128
+
129
+ type: Literal[TriggerType.DATA_CHANGE] = TriggerType.DATA_CHANGE
130
+ change_threshold: float = Field(
131
+ default=0.05,
132
+ ge=0.0,
133
+ le=1.0,
134
+ description="Minimum change percentage to trigger (0.0-1.0)",
135
+ )
136
+ metrics: list[str] = Field(
137
+ default_factory=lambda: ["row_count", "null_percentage", "distinct_count"],
138
+ description="Metrics to monitor for changes",
139
+ )
140
+ baseline_profile_id: str | None = Field(
141
+ default=None, description="Specific baseline profile ID to compare against"
142
+ )
143
+ use_latest_baseline: bool = Field(
144
+ default=True, description="Use the most recent profile as baseline"
145
+ )
146
+ check_interval_minutes: int = Field(
147
+ default=60, ge=1, description="How often to check for changes (minutes)"
148
+ )
149
+ priority: int = Field(
150
+ default=5,
151
+ ge=1,
152
+ le=10,
153
+ description="Evaluation priority (1=highest, 10=lowest)",
154
+ )
155
+ auto_profile: bool = Field(
156
+ default=True,
157
+ description="Automatically run profile before comparison",
158
+ )
159
+ cooldown_minutes: int = Field(
160
+ default=15,
161
+ ge=0,
162
+ description="Minimum time between triggers (prevents rapid re-triggering)",
163
+ )
164
+
165
+ @field_validator("metrics")
166
+ @classmethod
167
+ def validate_metrics(cls, v: list[str]) -> list[str]:
168
+ """Validate metric names."""
169
+ valid_metrics = {
170
+ "row_count",
171
+ "column_count",
172
+ "null_percentage",
173
+ "distinct_count",
174
+ "mean",
175
+ "std",
176
+ "min",
177
+ "max",
178
+ "schema_hash",
179
+ }
180
+ for metric in v:
181
+ if metric not in valid_metrics:
182
+ raise ValueError(
183
+ f"Invalid metric: {metric}. Valid: {valid_metrics}"
184
+ )
185
+ return v
186
+
187
+
188
+ class EventTriggerConfig(BaseTriggerConfig):
189
+ """Event-based trigger configuration.
190
+
191
+ Triggers in response to specific system events.
192
+
193
+ Example:
194
+ event_types=["schema_changed", "drift_detected"]
195
+ """
196
+
197
+ type: Literal[TriggerType.EVENT] = TriggerType.EVENT
198
+ event_types: list[str] = Field(
199
+ ...,
200
+ min_length=1,
201
+ description="Event types that trigger execution",
202
+ )
203
+ source_filter: list[str] | None = Field(
204
+ default=None, description="Optional source IDs to filter events"
205
+ )
206
+
207
+ @field_validator("event_types")
208
+ @classmethod
209
+ def validate_event_types(cls, v: list[str]) -> list[str]:
210
+ """Validate event type names."""
211
+ valid_events = {
212
+ "validation_completed",
213
+ "validation_failed",
214
+ "schema_changed",
215
+ "drift_detected",
216
+ "profile_updated",
217
+ "source_created",
218
+ "source_updated",
219
+ }
220
+ for event in v:
221
+ if event not in valid_events:
222
+ raise ValueError(f"Invalid event type: {event}. Valid: {valid_events}")
223
+ return v
224
+
225
+
226
+ class CompositeTriggerConfig(BaseTriggerConfig):
227
+ """Composite trigger configuration.
228
+
229
+ Combines multiple triggers with AND/OR logic.
230
+
231
+ Example (AND):
232
+ triggers=[CronTrigger, DataChangeTrigger]
233
+ operator="and"
234
+ -> Triggers only when both conditions are met
235
+
236
+ Example (OR):
237
+ triggers=[IntervalTrigger, EventTrigger]
238
+ operator="or"
239
+ -> Triggers when any condition is met
240
+ """
241
+
242
+ type: Literal[TriggerType.COMPOSITE] = TriggerType.COMPOSITE
243
+ operator: TriggerOperator = Field(
244
+ default=TriggerOperator.AND,
245
+ description="How to combine triggers (and/or)",
246
+ )
247
+ triggers: list[dict[str, Any]] = Field(
248
+ ...,
249
+ min_length=2,
250
+ description="List of trigger configurations to combine",
251
+ )
252
+
253
+ @field_validator("triggers")
254
+ @classmethod
255
+ def validate_triggers(cls, v: list[dict[str, Any]]) -> list[dict[str, Any]]:
256
+ """Validate nested trigger configurations."""
257
+ if len(v) < 2:
258
+ raise ValueError("Composite trigger requires at least 2 triggers")
259
+
260
+ for trigger in v:
261
+ if "type" not in trigger:
262
+ raise ValueError("Each trigger must have a 'type' field")
263
+ # Prevent deeply nested composites for simplicity
264
+ if trigger.get("type") == TriggerType.COMPOSITE:
265
+ raise ValueError("Nested composite triggers are not supported")
266
+
267
+ return v
268
+
269
+
270
+ class ManualTriggerConfig(BaseTriggerConfig):
271
+ """Manual trigger configuration.
272
+
273
+ Only triggers when explicitly invoked via API.
274
+ """
275
+
276
+ type: Literal[TriggerType.MANUAL] = TriggerType.MANUAL
277
+
278
+
279
+ class WebhookTriggerConfig(BaseTriggerConfig):
280
+ """Webhook trigger configuration.
281
+
282
+ Triggers when an external system sends a webhook request.
283
+ Supports secret-based authentication and payload validation.
284
+
285
+ Example:
286
+ webhook_secret="my_secret_key"
287
+ allowed_sources=["airflow", "dagster", "prefect"]
288
+ """
289
+
290
+ type: Literal[TriggerType.WEBHOOK] = TriggerType.WEBHOOK
291
+ webhook_secret: str | None = Field(
292
+ default=None,
293
+ description="Secret key for webhook authentication (HMAC-SHA256)",
294
+ )
295
+ allowed_sources: list[str] | None = Field(
296
+ default=None,
297
+ description="Optional list of allowed source identifiers",
298
+ )
299
+ payload_filters: dict[str, Any] | None = Field(
300
+ default=None,
301
+ description="JSON path filters to match against payload",
302
+ )
303
+ require_signature: bool = Field(
304
+ default=False,
305
+ description="Require X-Webhook-Signature header validation",
306
+ )
307
+
308
+
309
+ # Union type for all trigger configurations
310
+ TriggerConfig = (
311
+ CronTriggerConfig
312
+ | IntervalTriggerConfig
313
+ | DataChangeTriggerConfig
314
+ | EventTriggerConfig
315
+ | CompositeTriggerConfig
316
+ | ManualTriggerConfig
317
+ | WebhookTriggerConfig
318
+ )
319
+
320
+
321
+ class TriggerState(BaseModel):
322
+ """Current state of a trigger."""
323
+
324
+ model_config = ConfigDict(from_attributes=True)
325
+
326
+ type: TriggerType
327
+ enabled: bool
328
+ last_triggered_at: datetime | None = None
329
+ next_trigger_at: datetime | None = None
330
+ trigger_count: int = 0
331
+ last_check_result: dict[str, Any] | None = None
332
+ error: str | None = None
333
+
334
+
335
+ class TriggerEvaluationResult(BaseModel):
336
+ """Result of evaluating a trigger condition."""
337
+
338
+ should_trigger: bool = Field(..., description="Whether trigger condition is met")
339
+ reason: str = Field(..., description="Explanation of the evaluation result")
340
+ details: dict[str, Any] = Field(
341
+ default_factory=dict, description="Additional details about the evaluation"
342
+ )
343
+ next_check_at: datetime | None = Field(
344
+ default=None, description="Suggested next check time"
345
+ )
346
+
347
+
348
+ # =============================================================================
349
+ # Request/Response schemas
350
+ # =============================================================================
351
+
352
+
353
+ class TriggerConfigCreate(BaseModel):
354
+ """Schema for creating trigger configuration."""
355
+
356
+ model_config = ConfigDict(extra="forbid")
357
+
358
+ type: TriggerType
359
+ config: dict[str, Any] = Field(..., description="Trigger-specific configuration")
360
+
361
+ def to_trigger_config(self) -> TriggerConfig:
362
+ """Convert to specific trigger config type."""
363
+ config_with_type = {"type": self.type, **self.config}
364
+
365
+ match self.type:
366
+ case TriggerType.CRON:
367
+ return CronTriggerConfig(**config_with_type)
368
+ case TriggerType.INTERVAL:
369
+ return IntervalTriggerConfig(**config_with_type)
370
+ case TriggerType.DATA_CHANGE:
371
+ return DataChangeTriggerConfig(**config_with_type)
372
+ case TriggerType.EVENT:
373
+ return EventTriggerConfig(**config_with_type)
374
+ case TriggerType.COMPOSITE:
375
+ return CompositeTriggerConfig(**config_with_type)
376
+ case TriggerType.MANUAL:
377
+ return ManualTriggerConfig(**config_with_type)
378
+ case TriggerType.WEBHOOK:
379
+ return WebhookTriggerConfig(**config_with_type)
380
+ case _:
381
+ raise ValueError(f"Unknown trigger type: {self.type}")
382
+
383
+
384
+ class TriggerResponse(BaseModel):
385
+ """Response schema for trigger information."""
386
+
387
+ model_config = ConfigDict(from_attributes=True)
388
+
389
+ type: TriggerType
390
+ config: dict[str, Any]
391
+ state: TriggerState | None = None
392
+
393
+
394
+ def parse_trigger_config(data: dict[str, Any]) -> TriggerConfig:
395
+ """Parse trigger configuration from dict.
396
+
397
+ Args:
398
+ data: Dictionary containing trigger configuration.
399
+
400
+ Returns:
401
+ Appropriate TriggerConfig subclass instance.
402
+
403
+ Raises:
404
+ ValueError: If trigger type is invalid or config is malformed.
405
+ """
406
+ trigger_type = data.get("type")
407
+ if not trigger_type:
408
+ raise ValueError("Trigger config must have 'type' field")
409
+
410
+ try:
411
+ trigger_type_enum = TriggerType(trigger_type)
412
+ except ValueError:
413
+ raise ValueError(f"Invalid trigger type: {trigger_type}")
414
+
415
+ match trigger_type_enum:
416
+ case TriggerType.CRON:
417
+ return CronTriggerConfig(**data)
418
+ case TriggerType.INTERVAL:
419
+ return IntervalTriggerConfig(**data)
420
+ case TriggerType.DATA_CHANGE:
421
+ return DataChangeTriggerConfig(**data)
422
+ case TriggerType.EVENT:
423
+ return EventTriggerConfig(**data)
424
+ case TriggerType.COMPOSITE:
425
+ return CompositeTriggerConfig(**data)
426
+ case TriggerType.MANUAL:
427
+ return ManualTriggerConfig(**data)
428
+ case TriggerType.WEBHOOK:
429
+ return WebhookTriggerConfig(**data)
430
+ case _:
431
+ raise ValueError(f"Unknown trigger type: {trigger_type}")
432
+
433
+
434
+ # =============================================================================
435
+ # Trigger Monitoring Schemas
436
+ # =============================================================================
437
+
438
+
439
+ class TriggerCheckStatus(BaseModel):
440
+ """Status of a single trigger check."""
441
+
442
+ schedule_id: str
443
+ schedule_name: str
444
+ trigger_type: TriggerType
445
+ last_check_at: datetime | None = None
446
+ next_check_at: datetime | None = None
447
+ last_triggered_at: datetime | None = None
448
+ check_count: int = 0
449
+ trigger_count: int = 0
450
+ last_evaluation: TriggerEvaluationResult | None = None
451
+ is_due_for_check: bool = False
452
+ priority: int = 5
453
+ cooldown_remaining_seconds: int = 0
454
+
455
+
456
+ class TriggerMonitoringStats(BaseModel):
457
+ """Aggregated statistics for trigger monitoring."""
458
+
459
+ total_schedules: int = 0
460
+ active_data_change_triggers: int = 0
461
+ active_webhook_triggers: int = 0
462
+ active_composite_triggers: int = 0
463
+ total_checks_last_hour: int = 0
464
+ total_triggers_last_hour: int = 0
465
+ average_check_interval_seconds: float = 0.0
466
+ next_scheduled_check_at: datetime | None = None
467
+
468
+
469
+ class TriggerMonitoringResponse(BaseModel):
470
+ """Response for trigger monitoring status endpoint."""
471
+
472
+ stats: TriggerMonitoringStats
473
+ schedules: list[TriggerCheckStatus] = Field(default_factory=list)
474
+ checker_running: bool = False
475
+ checker_interval_seconds: int = 300
476
+ last_checker_run_at: datetime | None = None
477
+
478
+
479
+ class WebhookTriggerRequest(BaseModel):
480
+ """Request schema for incoming webhook triggers."""
481
+
482
+ source: str = Field(..., description="Source identifier (e.g., 'airflow', 'dagster')")
483
+ event_type: str = Field(
484
+ default="data_updated",
485
+ description="Type of event (data_updated, job_completed, etc.)",
486
+ )
487
+ payload: dict[str, Any] = Field(
488
+ default_factory=dict,
489
+ description="Additional event payload data",
490
+ )
491
+ schedule_id: str | None = Field(
492
+ default=None,
493
+ description="Specific schedule to trigger (optional)",
494
+ )
495
+ source_id: str | None = Field(
496
+ default=None,
497
+ description="Data source ID to trigger (optional)",
498
+ )
499
+ timestamp: datetime | None = Field(
500
+ default=None,
501
+ description="Event timestamp (defaults to now)",
502
+ )
503
+
504
+
505
+ class WebhookTriggerResponse(BaseModel):
506
+ """Response schema for webhook trigger endpoint."""
507
+
508
+ accepted: bool
509
+ triggered_schedules: list[str] = Field(default_factory=list)
510
+ message: str
511
+ request_id: str