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.
- truthound_dashboard/__init__.py +11 -0
- truthound_dashboard/__main__.py +6 -0
- truthound_dashboard/api/__init__.py +15 -0
- truthound_dashboard/api/deps.py +153 -0
- truthound_dashboard/api/drift.py +179 -0
- truthound_dashboard/api/error_handlers.py +287 -0
- truthound_dashboard/api/health.py +78 -0
- truthound_dashboard/api/history.py +62 -0
- truthound_dashboard/api/middleware.py +626 -0
- truthound_dashboard/api/notifications.py +561 -0
- truthound_dashboard/api/profile.py +52 -0
- truthound_dashboard/api/router.py +83 -0
- truthound_dashboard/api/rules.py +277 -0
- truthound_dashboard/api/schedules.py +329 -0
- truthound_dashboard/api/schemas.py +136 -0
- truthound_dashboard/api/sources.py +229 -0
- truthound_dashboard/api/validations.py +125 -0
- truthound_dashboard/cli.py +226 -0
- truthound_dashboard/config.py +132 -0
- truthound_dashboard/core/__init__.py +264 -0
- truthound_dashboard/core/base.py +185 -0
- truthound_dashboard/core/cache.py +479 -0
- truthound_dashboard/core/connections.py +331 -0
- truthound_dashboard/core/encryption.py +409 -0
- truthound_dashboard/core/exceptions.py +627 -0
- truthound_dashboard/core/logging.py +488 -0
- truthound_dashboard/core/maintenance.py +542 -0
- truthound_dashboard/core/notifications/__init__.py +56 -0
- truthound_dashboard/core/notifications/base.py +390 -0
- truthound_dashboard/core/notifications/channels.py +557 -0
- truthound_dashboard/core/notifications/dispatcher.py +453 -0
- truthound_dashboard/core/notifications/events.py +155 -0
- truthound_dashboard/core/notifications/service.py +744 -0
- truthound_dashboard/core/sampling.py +626 -0
- truthound_dashboard/core/scheduler.py +311 -0
- truthound_dashboard/core/services.py +1531 -0
- truthound_dashboard/core/truthound_adapter.py +659 -0
- truthound_dashboard/db/__init__.py +67 -0
- truthound_dashboard/db/base.py +108 -0
- truthound_dashboard/db/database.py +196 -0
- truthound_dashboard/db/models.py +732 -0
- truthound_dashboard/db/repository.py +237 -0
- truthound_dashboard/main.py +309 -0
- truthound_dashboard/schemas/__init__.py +150 -0
- truthound_dashboard/schemas/base.py +96 -0
- truthound_dashboard/schemas/drift.py +118 -0
- truthound_dashboard/schemas/history.py +74 -0
- truthound_dashboard/schemas/profile.py +91 -0
- truthound_dashboard/schemas/rule.py +199 -0
- truthound_dashboard/schemas/schedule.py +88 -0
- truthound_dashboard/schemas/schema.py +121 -0
- truthound_dashboard/schemas/source.py +138 -0
- truthound_dashboard/schemas/validation.py +192 -0
- truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
- truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
- truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
- truthound_dashboard/static/index.html +15 -0
- truthound_dashboard/static/mockServiceWorker.js +349 -0
- truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
- truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
- truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
- 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")
|