truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.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 +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.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[
|
|
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
|