truthound-dashboard 1.0.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 (62) hide show
  1. truthound_dashboard/__init__.py +11 -0
  2. truthound_dashboard/__main__.py +6 -0
  3. truthound_dashboard/api/__init__.py +15 -0
  4. truthound_dashboard/api/deps.py +153 -0
  5. truthound_dashboard/api/drift.py +179 -0
  6. truthound_dashboard/api/error_handlers.py +287 -0
  7. truthound_dashboard/api/health.py +78 -0
  8. truthound_dashboard/api/history.py +62 -0
  9. truthound_dashboard/api/middleware.py +626 -0
  10. truthound_dashboard/api/notifications.py +561 -0
  11. truthound_dashboard/api/profile.py +52 -0
  12. truthound_dashboard/api/router.py +83 -0
  13. truthound_dashboard/api/rules.py +277 -0
  14. truthound_dashboard/api/schedules.py +329 -0
  15. truthound_dashboard/api/schemas.py +136 -0
  16. truthound_dashboard/api/sources.py +229 -0
  17. truthound_dashboard/api/validations.py +125 -0
  18. truthound_dashboard/cli.py +226 -0
  19. truthound_dashboard/config.py +132 -0
  20. truthound_dashboard/core/__init__.py +264 -0
  21. truthound_dashboard/core/base.py +185 -0
  22. truthound_dashboard/core/cache.py +479 -0
  23. truthound_dashboard/core/connections.py +331 -0
  24. truthound_dashboard/core/encryption.py +409 -0
  25. truthound_dashboard/core/exceptions.py +627 -0
  26. truthound_dashboard/core/logging.py +488 -0
  27. truthound_dashboard/core/maintenance.py +542 -0
  28. truthound_dashboard/core/notifications/__init__.py +56 -0
  29. truthound_dashboard/core/notifications/base.py +390 -0
  30. truthound_dashboard/core/notifications/channels.py +557 -0
  31. truthound_dashboard/core/notifications/dispatcher.py +453 -0
  32. truthound_dashboard/core/notifications/events.py +155 -0
  33. truthound_dashboard/core/notifications/service.py +744 -0
  34. truthound_dashboard/core/sampling.py +626 -0
  35. truthound_dashboard/core/scheduler.py +311 -0
  36. truthound_dashboard/core/services.py +1531 -0
  37. truthound_dashboard/core/truthound_adapter.py +659 -0
  38. truthound_dashboard/db/__init__.py +67 -0
  39. truthound_dashboard/db/base.py +108 -0
  40. truthound_dashboard/db/database.py +196 -0
  41. truthound_dashboard/db/models.py +732 -0
  42. truthound_dashboard/db/repository.py +237 -0
  43. truthound_dashboard/main.py +309 -0
  44. truthound_dashboard/schemas/__init__.py +150 -0
  45. truthound_dashboard/schemas/base.py +96 -0
  46. truthound_dashboard/schemas/drift.py +118 -0
  47. truthound_dashboard/schemas/history.py +74 -0
  48. truthound_dashboard/schemas/profile.py +91 -0
  49. truthound_dashboard/schemas/rule.py +199 -0
  50. truthound_dashboard/schemas/schedule.py +88 -0
  51. truthound_dashboard/schemas/schema.py +121 -0
  52. truthound_dashboard/schemas/source.py +138 -0
  53. truthound_dashboard/schemas/validation.py +192 -0
  54. truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
  55. truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
  56. truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
  57. truthound_dashboard/static/index.html +15 -0
  58. truthound_dashboard/static/mockServiceWorker.js +349 -0
  59. truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
  60. truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
  61. truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
  62. truthound_dashboard-1.0.0.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,96 @@
1
+ """Base Pydantic schemas and mixins.
2
+
3
+ This module provides reusable base classes and mixins for Pydantic schemas,
4
+ enabling consistent patterns across all API schemas.
5
+
6
+ The schema classes follow a consistent naming convention:
7
+ - *Base: Common fields shared by create/update/response
8
+ - *Create: Fields for creation (POST)
9
+ - *Update: Fields for updates (PUT/PATCH)
10
+ - *Response: Fields returned in responses
11
+ - *ListResponse: Paginated list response wrapper
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from datetime import datetime
17
+ from typing import Any, Generic, TypeVar
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field
20
+
21
+ # Type variable for generic response wrappers
22
+ T = TypeVar("T")
23
+
24
+
25
+ class BaseSchema(BaseModel):
26
+ """Base schema with common configuration.
27
+
28
+ All schemas should inherit from this base class to ensure
29
+ consistent behavior and serialization.
30
+ """
31
+
32
+ model_config = ConfigDict(
33
+ from_attributes=True,
34
+ populate_by_name=True,
35
+ json_schema_extra={"example": {}},
36
+ )
37
+
38
+
39
+ class TimestampMixin:
40
+ """Mixin for timestamp fields in responses."""
41
+
42
+ created_at: datetime = Field(..., description="Creation timestamp")
43
+ updated_at: datetime = Field(..., description="Last update timestamp")
44
+
45
+
46
+ class IDMixin:
47
+ """Mixin for ID field in responses."""
48
+
49
+ id: str = Field(..., description="Unique identifier")
50
+
51
+
52
+ class ResponseWrapper(BaseSchema, Generic[T]):
53
+ """Generic wrapper for single item responses.
54
+
55
+ Provides consistent structure for API responses.
56
+ """
57
+
58
+ success: bool = Field(default=True, description="Whether request succeeded")
59
+ data: T = Field(..., description="Response data")
60
+ message: str | None = Field(default=None, description="Optional message")
61
+
62
+
63
+ class ListResponseWrapper(BaseSchema, Generic[T]):
64
+ """Generic wrapper for list responses with pagination.
65
+
66
+ Provides consistent structure for paginated API responses.
67
+ """
68
+
69
+ success: bool = Field(default=True, description="Whether request succeeded")
70
+ data: list[T] = Field(default_factory=list, description="List of items")
71
+ total: int = Field(default=0, description="Total count of items")
72
+ offset: int = Field(default=0, description="Offset for pagination")
73
+ limit: int = Field(default=100, description="Limit for pagination")
74
+
75
+ @property
76
+ def has_more(self) -> bool:
77
+ """Check if there are more items."""
78
+ return self.offset + len(self.data) < self.total
79
+
80
+
81
+ class ErrorResponse(BaseSchema):
82
+ """Standard error response schema."""
83
+
84
+ success: bool = Field(default=False)
85
+ detail: str = Field(..., description="Error description")
86
+ code: str | None = Field(default=None, description="Error code")
87
+ errors: list[dict[str, Any]] | None = Field(
88
+ default=None, description="Validation errors"
89
+ )
90
+
91
+
92
+ class MessageResponse(BaseSchema):
93
+ """Simple message response schema."""
94
+
95
+ success: bool = Field(default=True)
96
+ message: str = Field(..., description="Response message")
@@ -0,0 +1,118 @@
1
+ """Drift detection schemas.
2
+
3
+ Schemas for drift comparison request/response.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Literal
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from .base import IDMixin, TimestampMixin
13
+
14
+
15
+ class DriftCompareRequest(BaseModel):
16
+ """Request body for drift comparison."""
17
+
18
+ baseline_source_id: str = Field(..., description="Baseline source ID")
19
+ current_source_id: str = Field(..., description="Current source ID to compare")
20
+ columns: list[str] | None = Field(
21
+ None, description="Columns to compare (None = all)"
22
+ )
23
+ method: Literal["auto", "ks", "psi", "chi2", "js"] = Field(
24
+ "auto", description="Drift detection method"
25
+ )
26
+ threshold: float | None = Field(None, ge=0, le=1, description="Custom threshold")
27
+ sample_size: int | None = Field(
28
+ None, ge=100, description="Sample size for large datasets"
29
+ )
30
+
31
+
32
+ class ColumnDriftResult(BaseModel):
33
+ """Drift result for a single column."""
34
+
35
+ column: str = Field(..., description="Column name")
36
+ dtype: str = Field(..., description="Data type")
37
+ drifted: bool = Field(..., description="Whether drift was detected")
38
+ level: str = Field(..., description="Drift level (high, medium, low, none)")
39
+ method: str = Field(..., description="Detection method used")
40
+ statistic: float | None = Field(None, description="Test statistic value")
41
+ p_value: float | None = Field(None, description="P-value")
42
+ baseline_stats: dict[str, Any] = Field(
43
+ default_factory=dict, description="Baseline statistics"
44
+ )
45
+ current_stats: dict[str, Any] = Field(
46
+ default_factory=dict, description="Current statistics"
47
+ )
48
+
49
+
50
+ class DriftResult(BaseModel):
51
+ """Full drift comparison result."""
52
+
53
+ baseline_source: str = Field(..., description="Baseline source path")
54
+ current_source: str = Field(..., description="Current source path")
55
+ baseline_rows: int = Field(..., description="Number of baseline rows")
56
+ current_rows: int = Field(..., description="Number of current rows")
57
+ has_drift: bool = Field(..., description="Whether any drift was detected")
58
+ has_high_drift: bool = Field(
59
+ ..., description="Whether high-severity drift was detected"
60
+ )
61
+ total_columns: int = Field(..., description="Total columns compared")
62
+ drifted_columns: list[str] = Field(
63
+ default_factory=list, description="Columns with drift"
64
+ )
65
+ columns: list[ColumnDriftResult] = Field(
66
+ default_factory=list, description="Per-column results"
67
+ )
68
+
69
+
70
+ class DriftSourceSummary(BaseModel):
71
+ """Summary of a source in drift comparison."""
72
+
73
+ id: str = Field(..., description="Source ID")
74
+ name: str = Field(..., description="Source name")
75
+
76
+
77
+ class DriftComparisonResponse(BaseModel, IDMixin, TimestampMixin):
78
+ """Response for drift comparison."""
79
+
80
+ baseline_source_id: str = Field(..., description="Baseline source ID")
81
+ current_source_id: str = Field(..., description="Current source ID")
82
+ has_drift: bool = Field(..., description="Whether drift was detected")
83
+ has_high_drift: bool = Field(
84
+ ..., description="Whether high-severity drift was detected"
85
+ )
86
+ total_columns: int | None = Field(None, description="Total columns compared")
87
+ drifted_columns: int | None = Field(
88
+ None, description="Number of columns with drift"
89
+ )
90
+ drift_percentage: float = Field(0, description="Percentage of columns with drift")
91
+ result: DriftResult | None = Field(None, description="Full drift result")
92
+ config: dict[str, Any] | None = Field(None, description="Comparison configuration")
93
+
94
+ # Optional source details
95
+ baseline: DriftSourceSummary | None = Field(
96
+ None, description="Baseline source info"
97
+ )
98
+ current: DriftSourceSummary | None = Field(None, description="Current source info")
99
+
100
+
101
+ class DriftComparisonListItem(BaseModel, IDMixin, TimestampMixin):
102
+ """List item for drift comparisons."""
103
+
104
+ baseline_source_id: str
105
+ current_source_id: str
106
+ has_drift: bool
107
+ has_high_drift: bool
108
+ total_columns: int | None = None
109
+ drifted_columns: int | None = None
110
+ drift_percentage: float = 0
111
+
112
+
113
+ class DriftComparisonListResponse(BaseModel):
114
+ """List response for drift comparisons."""
115
+
116
+ success: bool = True
117
+ data: list[DriftComparisonListItem] = Field(default_factory=list)
118
+ total: int = 0
@@ -0,0 +1,74 @@
1
+ """History and analytics schemas.
2
+
3
+ Schemas for validation history and trend analysis.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Literal
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class TrendDataPoint(BaseModel):
14
+ """Single data point in trend analysis."""
15
+
16
+ date: str = Field(..., description="Date string (format depends on granularity)")
17
+ success_rate: float = Field(
18
+ ..., ge=0, le=100, description="Success rate percentage"
19
+ )
20
+ run_count: int = Field(..., ge=0, description="Number of validation runs")
21
+ passed_count: int = Field(..., ge=0, description="Number of passed validations")
22
+ failed_count: int = Field(..., ge=0, description="Number of failed validations")
23
+
24
+
25
+ class FailureFrequencyItem(BaseModel):
26
+ """Failure frequency for an issue type."""
27
+
28
+ issue: str = Field(..., description="Issue identifier (column.issue_type)")
29
+ count: int = Field(..., ge=0, description="Total occurrence count")
30
+
31
+
32
+ class RecentValidation(BaseModel):
33
+ """Summary of a recent validation run."""
34
+
35
+ id: str = Field(..., description="Validation ID")
36
+ status: str = Field(..., description="Validation status")
37
+ passed: bool | None = Field(None, description="Whether validation passed")
38
+ has_critical: bool | None = Field(None, description="Has critical issues")
39
+ has_high: bool | None = Field(None, description="Has high severity issues")
40
+ total_issues: int | None = Field(None, description="Total issue count")
41
+ created_at: str = Field(..., description="ISO timestamp")
42
+
43
+
44
+ class HistorySummary(BaseModel):
45
+ """Summary statistics for the history period."""
46
+
47
+ total_runs: int = Field(..., ge=0, description="Total validation runs")
48
+ passed_runs: int = Field(..., ge=0, description="Number of passed runs")
49
+ failed_runs: int = Field(..., ge=0, description="Number of failed runs")
50
+ success_rate: float = Field(
51
+ ..., ge=0, le=100, description="Success rate percentage"
52
+ )
53
+
54
+
55
+ class HistoryResponse(BaseModel):
56
+ """Validation history response with trends and analytics."""
57
+
58
+ summary: HistorySummary = Field(..., description="Summary statistics")
59
+ trend: list[TrendDataPoint] = Field(default_factory=list, description="Trend data")
60
+ failure_frequency: list[FailureFrequencyItem] = Field(
61
+ default_factory=list, description="Top failure types"
62
+ )
63
+ recent_validations: list[RecentValidation] = Field(
64
+ default_factory=list, description="Recent validation runs"
65
+ )
66
+
67
+
68
+ class HistoryQueryParams(BaseModel):
69
+ """Query parameters for history endpoint."""
70
+
71
+ period: Literal["7d", "30d", "90d"] = Field("30d", description="Time period")
72
+ granularity: Literal["hourly", "daily", "weekly"] = Field(
73
+ "daily", description="Aggregation granularity"
74
+ )
@@ -0,0 +1,91 @@
1
+ """Profile-related Pydantic schemas.
2
+
3
+ This module defines schemas for data profiling API operations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import Field
11
+
12
+ from .base import BaseSchema
13
+
14
+
15
+ class ColumnProfile(BaseSchema):
16
+ """Profile information for a single column."""
17
+
18
+ name: str = Field(..., description="Column name")
19
+ dtype: str = Field(..., description="Data type")
20
+ null_pct: str = Field(default="0%", description="Percentage of null values")
21
+ unique_pct: str = Field(default="0%", description="Percentage of unique values")
22
+ min: Any | None = Field(default=None, description="Minimum value")
23
+ max: Any | None = Field(default=None, description="Maximum value")
24
+ mean: float | None = Field(default=None, description="Mean value (numeric columns)")
25
+ std: float | None = Field(default=None, description="Standard deviation (numeric)")
26
+
27
+ # Additional statistics (optional)
28
+ distinct_count: int | None = Field(
29
+ default=None,
30
+ description="Count of distinct values",
31
+ )
32
+ most_common: list[dict[str, Any]] | None = Field(
33
+ default=None,
34
+ description="Most common values with counts",
35
+ )
36
+
37
+
38
+ class ProfileResponse(BaseSchema):
39
+ """Data profiling response."""
40
+
41
+ source: str = Field(..., description="Source path/identifier")
42
+ row_count: int = Field(..., ge=0, description="Total number of rows")
43
+ column_count: int = Field(..., ge=0, description="Total number of columns")
44
+ size_bytes: int = Field(..., ge=0, description="Data size in bytes")
45
+ columns: list[ColumnProfile] = Field(
46
+ default_factory=list,
47
+ description="Profile for each column",
48
+ )
49
+
50
+ # Computed properties
51
+ @property
52
+ def size_human(self) -> str:
53
+ """Get human-readable size."""
54
+ size = self.size_bytes
55
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
56
+ if size < 1024:
57
+ return f"{size:.1f} {unit}"
58
+ size /= 1024
59
+ return f"{size:.1f} PB"
60
+
61
+ @classmethod
62
+ def from_result(cls, result: Any) -> ProfileResponse:
63
+ """Create response from adapter result.
64
+
65
+ Args:
66
+ result: ProfileResult from adapter.
67
+
68
+ Returns:
69
+ ProfileResponse instance.
70
+ """
71
+ columns = [
72
+ ColumnProfile(
73
+ name=col["name"],
74
+ dtype=col["dtype"],
75
+ null_pct=col.get("null_pct", "0%"),
76
+ unique_pct=col.get("unique_pct", "0%"),
77
+ min=col.get("min"),
78
+ max=col.get("max"),
79
+ mean=col.get("mean"),
80
+ std=col.get("std"),
81
+ )
82
+ for col in result.columns
83
+ ]
84
+
85
+ return cls(
86
+ source=result.source,
87
+ row_count=result.row_count,
88
+ column_count=result.column_count,
89
+ size_bytes=result.size_bytes,
90
+ columns=columns,
91
+ )
@@ -0,0 +1,199 @@
1
+ """Rule-related Pydantic schemas.
2
+
3
+ This module defines schemas for custom validation rules API operations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import yaml
11
+ from pydantic import Field, field_validator
12
+
13
+ from .base import BaseSchema, IDMixin, ListResponseWrapper, TimestampMixin
14
+
15
+
16
+ class RuleBase(BaseSchema):
17
+ """Base rule schema with common fields."""
18
+
19
+ name: str = Field(
20
+ default="Default Rules",
21
+ min_length=1,
22
+ max_length=255,
23
+ description="Human-readable rule name",
24
+ examples=["Email Validation Rules", "User Data Constraints"],
25
+ )
26
+ description: str | None = Field(
27
+ default=None,
28
+ max_length=1000,
29
+ description="Optional description of the rules",
30
+ )
31
+ rules_yaml: str = Field(
32
+ ...,
33
+ min_length=1,
34
+ description="YAML content defining validation rules",
35
+ examples=[
36
+ """columns:
37
+ user_id:
38
+ not_null: true
39
+ unique: true
40
+ email:
41
+ pattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"
42
+ """
43
+ ],
44
+ )
45
+
46
+
47
+ class RuleCreate(RuleBase):
48
+ """Schema for creating a new rule."""
49
+
50
+ @field_validator("rules_yaml")
51
+ @classmethod
52
+ def validate_yaml(cls, v: str) -> str:
53
+ """Validate YAML syntax."""
54
+ try:
55
+ yaml.safe_load(v)
56
+ except yaml.YAMLError as e:
57
+ raise ValueError(f"Invalid YAML syntax: {e}")
58
+ return v
59
+
60
+
61
+ class RuleUpdate(BaseSchema):
62
+ """Schema for updating an existing rule."""
63
+
64
+ name: str | None = Field(
65
+ default=None,
66
+ min_length=1,
67
+ max_length=255,
68
+ description="New rule name",
69
+ )
70
+ description: str | None = Field(
71
+ default=None,
72
+ max_length=1000,
73
+ description="New description",
74
+ )
75
+ rules_yaml: str | None = Field(
76
+ default=None,
77
+ min_length=1,
78
+ description="New YAML rules content",
79
+ )
80
+ is_active: bool | None = Field(
81
+ default=None,
82
+ description="Whether rule is active",
83
+ )
84
+ version: str | None = Field(
85
+ default=None,
86
+ max_length=50,
87
+ description="Version string for tracking changes",
88
+ )
89
+
90
+ @field_validator("rules_yaml")
91
+ @classmethod
92
+ def validate_yaml(cls, v: str | None) -> str | None:
93
+ """Validate YAML syntax if provided."""
94
+ if v is not None:
95
+ try:
96
+ yaml.safe_load(v)
97
+ except yaml.YAMLError as e:
98
+ raise ValueError(f"Invalid YAML syntax: {e}")
99
+ return v
100
+
101
+
102
+ class ColumnRuleInfo(BaseSchema):
103
+ """Information about a single column's rules."""
104
+
105
+ name: str = Field(..., description="Column name")
106
+ constraints: dict[str, Any] = Field(
107
+ default_factory=dict,
108
+ description="Constraints applied to this column",
109
+ )
110
+
111
+
112
+ class RuleResponse(RuleBase, IDMixin, TimestampMixin):
113
+ """Schema for rule responses."""
114
+
115
+ source_id: str = Field(..., description="Source this rule belongs to")
116
+ is_active: bool = Field(default=True, description="Whether rule is active")
117
+ version: str | None = Field(default=None, description="Rule version")
118
+
119
+ # Computed from rules_json
120
+ rules_json: dict[str, Any] | None = Field(
121
+ default=None,
122
+ description="Parsed rules as JSON",
123
+ )
124
+ column_count: int = Field(
125
+ default=0,
126
+ description="Number of columns with rules",
127
+ )
128
+
129
+ @classmethod
130
+ def from_model(cls, rule: Any) -> RuleResponse:
131
+ """Create response from model with computed fields.
132
+
133
+ Args:
134
+ rule: Rule model instance.
135
+
136
+ Returns:
137
+ RuleResponse with computed fields.
138
+ """
139
+ return cls(
140
+ id=rule.id,
141
+ source_id=rule.source_id,
142
+ name=rule.name,
143
+ description=rule.description,
144
+ rules_yaml=rule.rules_yaml,
145
+ rules_json=rule.rules_json,
146
+ is_active=rule.is_active,
147
+ version=rule.version,
148
+ column_count=rule.column_count,
149
+ created_at=rule.created_at,
150
+ updated_at=rule.updated_at,
151
+ )
152
+
153
+
154
+ class RuleListItem(BaseSchema, IDMixin, TimestampMixin):
155
+ """Rule list item (without full YAML content)."""
156
+
157
+ source_id: str
158
+ name: str
159
+ description: str | None = None
160
+ is_active: bool = True
161
+ version: str | None = None
162
+ column_count: int = 0
163
+
164
+ @classmethod
165
+ def from_model(cls, rule: Any) -> RuleListItem:
166
+ """Create list item from model.
167
+
168
+ Args:
169
+ rule: Rule model instance.
170
+
171
+ Returns:
172
+ RuleListItem instance.
173
+ """
174
+ return cls(
175
+ id=rule.id,
176
+ source_id=rule.source_id,
177
+ name=rule.name,
178
+ description=rule.description,
179
+ is_active=rule.is_active,
180
+ version=rule.version,
181
+ column_count=rule.column_count,
182
+ created_at=rule.created_at,
183
+ updated_at=rule.updated_at,
184
+ )
185
+
186
+
187
+ class RuleListResponse(ListResponseWrapper[RuleListItem]):
188
+ """Paginated rule list response."""
189
+
190
+ pass
191
+
192
+
193
+ class RuleSummary(BaseSchema):
194
+ """Minimal rule summary for embedded responses."""
195
+
196
+ id: str
197
+ name: str
198
+ is_active: bool = True
199
+ column_count: int = 0
@@ -0,0 +1,88 @@
1
+ """Schedule management schemas.
2
+
3
+ Schemas for validation schedule request/response.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from .base import IDMixin, TimestampMixin
13
+
14
+
15
+ class ScheduleBase(BaseModel):
16
+ """Base schedule fields."""
17
+
18
+ name: str = Field(..., min_length=1, max_length=255, description="Schedule name")
19
+ cron_expression: str = Field(
20
+ ...,
21
+ description="Cron expression (5 fields: min hour day month weekday)",
22
+ examples=["0 8 * * *", "*/30 * * * *", "0 0 * * 0"],
23
+ )
24
+ notify_on_failure: bool = Field(True, description="Send notification on failure")
25
+
26
+
27
+ class ScheduleCreate(ScheduleBase):
28
+ """Request body for creating a schedule."""
29
+
30
+ source_id: str = Field(..., description="Source ID to schedule")
31
+ config: dict[str, Any] | None = Field(
32
+ None,
33
+ description="Additional configuration (validators, schema_path, etc.)",
34
+ )
35
+
36
+
37
+ class ScheduleUpdate(BaseModel):
38
+ """Request body for updating a schedule."""
39
+
40
+ name: str | None = Field(None, min_length=1, max_length=255)
41
+ cron_expression: str | None = Field(None)
42
+ notify_on_failure: bool | None = Field(None)
43
+ config: dict[str, Any] | None = Field(None)
44
+
45
+
46
+ class ScheduleResponse(BaseModel, IDMixin, TimestampMixin):
47
+ """Response for a schedule."""
48
+
49
+ name: str = Field(..., description="Schedule name")
50
+ source_id: str = Field(..., description="Source ID")
51
+ cron_expression: str = Field(..., description="Cron expression")
52
+ is_active: bool = Field(..., description="Whether schedule is active")
53
+ notify_on_failure: bool = Field(..., description="Notify on failure")
54
+ last_run_at: str | None = Field(None, description="Last run timestamp (ISO)")
55
+ next_run_at: str | None = Field(None, description="Next scheduled run (ISO)")
56
+ config: dict[str, Any] | None = Field(None, description="Configuration")
57
+
58
+ # Optional source info
59
+ source_name: str | None = Field(None, description="Source name")
60
+
61
+
62
+ class ScheduleListItem(BaseModel, IDMixin, TimestampMixin):
63
+ """List item for schedules."""
64
+
65
+ name: str
66
+ source_id: str
67
+ cron_expression: str
68
+ is_active: bool
69
+ notify_on_failure: bool
70
+ last_run_at: str | None = None
71
+ next_run_at: str | None = None
72
+ source_name: str | None = None
73
+
74
+
75
+ class ScheduleListResponse(BaseModel):
76
+ """List response for schedules."""
77
+
78
+ success: bool = True
79
+ data: list[ScheduleListItem] = Field(default_factory=list)
80
+ total: int = 0
81
+
82
+
83
+ class ScheduleActionResponse(BaseModel):
84
+ """Response for schedule actions (pause, resume, run)."""
85
+
86
+ success: bool = True
87
+ message: str = Field(..., description="Action result message")
88
+ schedule: ScheduleResponse | None = Field(None, description="Updated schedule")