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,237 @@
1
+ """Generic repository pattern for database operations.
2
+
3
+ This module provides a generic repository base class that implements
4
+ common CRUD operations, reducing boilerplate in API endpoints.
5
+
6
+ Example:
7
+ class SourceRepository(BaseRepository[Source]):
8
+ pass
9
+
10
+ async with get_session() as session:
11
+ repo = SourceRepository(session)
12
+ source = await repo.get_by_id("uuid-here")
13
+ sources = await repo.list(limit=10)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Sequence
19
+ from typing import Any, Generic, TypeVar
20
+
21
+ from sqlalchemy import func, select
22
+ from sqlalchemy.ext.asyncio import AsyncSession
23
+ from sqlalchemy.sql import Select
24
+
25
+ from .base import Base
26
+
27
+ # Type variable for model classes
28
+ ModelT = TypeVar("ModelT", bound=Base)
29
+
30
+
31
+ class BaseRepository(Generic[ModelT]):
32
+ """Generic repository providing common CRUD operations.
33
+
34
+ This class implements the repository pattern for database access,
35
+ providing a clean abstraction over SQLAlchemy operations.
36
+
37
+ Type Parameters:
38
+ ModelT: The SQLAlchemy model class this repository manages.
39
+
40
+ Attributes:
41
+ session: The async database session.
42
+ model: The model class for this repository.
43
+ """
44
+
45
+ model: type[ModelT]
46
+
47
+ def __init__(self, session: AsyncSession) -> None:
48
+ """Initialize repository with database session.
49
+
50
+ Args:
51
+ session: Async database session for operations.
52
+ """
53
+ self.session = session
54
+
55
+ def __init_subclass__(cls, **kwargs: Any) -> None:
56
+ """Set model class from generic type argument."""
57
+ super().__init_subclass__(**kwargs)
58
+ # Get the model type from Generic parameters
59
+ for base in cls.__orig_bases__: # type: ignore
60
+ if hasattr(base, "__args__"):
61
+ cls.model = base.__args__[0]
62
+ break
63
+
64
+ async def get_by_id(self, id: str) -> ModelT | None:
65
+ """Get a single record by ID.
66
+
67
+ Args:
68
+ id: The record's primary key.
69
+
70
+ Returns:
71
+ The model instance or None if not found.
72
+ """
73
+ result = await self.session.execute(
74
+ select(self.model).where(self.model.id == id)
75
+ )
76
+ return result.scalar_one_or_none()
77
+
78
+ async def get_by_id_or_raise(self, id: str) -> ModelT:
79
+ """Get a single record by ID, raising if not found.
80
+
81
+ Args:
82
+ id: The record's primary key.
83
+
84
+ Returns:
85
+ The model instance.
86
+
87
+ Raises:
88
+ ValueError: If record not found.
89
+ """
90
+ record = await self.get_by_id(id)
91
+ if record is None:
92
+ raise ValueError(f"{self.model.__name__} with id '{id}' not found")
93
+ return record
94
+
95
+ async def list(
96
+ self,
97
+ *,
98
+ offset: int = 0,
99
+ limit: int = 100,
100
+ order_by: Any = None,
101
+ filters: list[Any] | None = None,
102
+ ) -> Sequence[ModelT]:
103
+ """List records with pagination and filtering.
104
+
105
+ Args:
106
+ offset: Number of records to skip.
107
+ limit: Maximum records to return.
108
+ order_by: Column(s) to order by.
109
+ filters: List of SQLAlchemy filter conditions.
110
+
111
+ Returns:
112
+ Sequence of model instances.
113
+ """
114
+ query = self._build_query(filters)
115
+
116
+ if order_by is not None:
117
+ query = query.order_by(order_by)
118
+ elif hasattr(self.model, "created_at"):
119
+ query = query.order_by(self.model.created_at.desc())
120
+
121
+ query = query.offset(offset).limit(limit)
122
+ result = await self.session.execute(query)
123
+ return result.scalars().all()
124
+
125
+ async def count(self, filters: list[Any] | None = None) -> int:
126
+ """Count records matching filters.
127
+
128
+ Args:
129
+ filters: List of SQLAlchemy filter conditions.
130
+
131
+ Returns:
132
+ Total count of matching records.
133
+ """
134
+ query = select(func.count()).select_from(self.model)
135
+ if filters:
136
+ for f in filters:
137
+ query = query.where(f)
138
+ result = await self.session.execute(query)
139
+ return result.scalar_one()
140
+
141
+ async def create(self, **kwargs: Any) -> ModelT:
142
+ """Create a new record.
143
+
144
+ Args:
145
+ **kwargs: Field values for the new record.
146
+
147
+ Returns:
148
+ The created model instance.
149
+ """
150
+ instance = self.model(**kwargs)
151
+ self.session.add(instance)
152
+ await self.session.flush()
153
+ await self.session.refresh(instance)
154
+ return instance
155
+
156
+ async def update(self, id: str, **kwargs: Any) -> ModelT | None:
157
+ """Update a record by ID.
158
+
159
+ Args:
160
+ id: The record's primary key.
161
+ **kwargs: Field values to update.
162
+
163
+ Returns:
164
+ The updated model instance or None if not found.
165
+ """
166
+ instance = await self.get_by_id(id)
167
+ if instance is None:
168
+ return None
169
+
170
+ for key, value in kwargs.items():
171
+ if hasattr(instance, key):
172
+ setattr(instance, key, value)
173
+
174
+ await self.session.flush()
175
+ await self.session.refresh(instance)
176
+ return instance
177
+
178
+ async def delete(self, id: str) -> bool:
179
+ """Delete a record by ID.
180
+
181
+ Args:
182
+ id: The record's primary key.
183
+
184
+ Returns:
185
+ True if deleted, False if not found.
186
+ """
187
+ instance = await self.get_by_id(id)
188
+ if instance is None:
189
+ return False
190
+
191
+ await self.session.delete(instance)
192
+ await self.session.flush()
193
+ return True
194
+
195
+ async def bulk_create(self, items: list[dict[str, Any]]) -> list[ModelT]:
196
+ """Create multiple records at once.
197
+
198
+ Args:
199
+ items: List of dictionaries with field values.
200
+
201
+ Returns:
202
+ List of created model instances.
203
+ """
204
+ instances = [self.model(**item) for item in items]
205
+ self.session.add_all(instances)
206
+ await self.session.flush()
207
+ for instance in instances:
208
+ await self.session.refresh(instance)
209
+ return instances
210
+
211
+ async def exists(self, id: str) -> bool:
212
+ """Check if a record exists.
213
+
214
+ Args:
215
+ id: The record's primary key.
216
+
217
+ Returns:
218
+ True if exists, False otherwise.
219
+ """
220
+ query = select(func.count()).select_from(self.model).where(self.model.id == id)
221
+ result = await self.session.execute(query)
222
+ return result.scalar_one() > 0
223
+
224
+ def _build_query(self, filters: list[Any] | None = None) -> Select:
225
+ """Build base query with filters.
226
+
227
+ Args:
228
+ filters: List of SQLAlchemy filter conditions.
229
+
230
+ Returns:
231
+ Select query with filters applied.
232
+ """
233
+ query = select(self.model)
234
+ if filters:
235
+ for f in filters:
236
+ query = query.where(f)
237
+ return query
@@ -0,0 +1,309 @@
1
+ """FastAPI application entry point.
2
+
3
+ This module defines the main FastAPI application with all middleware,
4
+ routers, and lifecycle management.
5
+
6
+ The application serves both the API and the React SPA static files.
7
+
8
+ Features:
9
+ - Phase 4 production-ready components:
10
+ - Rate limiting and security headers
11
+ - Structured logging
12
+ - Error handling with localization
13
+ - Database maintenance scheduling
14
+ - Cache management
15
+
16
+ Example:
17
+ # Run with uvicorn
18
+ uvicorn truthound_dashboard.main:app --reload
19
+
20
+ # Or via CLI
21
+ truthound serve --reload
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from collections.abc import AsyncGenerator
28
+ from contextlib import asynccontextmanager
29
+ from pathlib import Path
30
+
31
+ from fastapi import FastAPI
32
+ from fastapi.middleware.cors import CORSMiddleware
33
+ from fastapi.responses import FileResponse
34
+ from fastapi.staticfiles import StaticFiles
35
+
36
+ from truthound_dashboard import __version__
37
+ from truthound_dashboard.api.error_handlers import setup_error_handlers
38
+ from truthound_dashboard.api.middleware import (
39
+ RateLimitConfig,
40
+ RateLimitMiddleware,
41
+ RequestLoggingMiddleware,
42
+ SecurityHeadersMiddleware,
43
+ )
44
+ from truthound_dashboard.api.router import api_router
45
+ from truthound_dashboard.config import get_settings
46
+ from truthound_dashboard.core.cache import get_cache
47
+ from truthound_dashboard.core.logging import setup_logging
48
+ from truthound_dashboard.core.maintenance import get_maintenance_manager
49
+ from truthound_dashboard.core.scheduler import get_scheduler
50
+ from truthound_dashboard.db import init_db
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ @asynccontextmanager
56
+ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
57
+ """Application lifespan manager.
58
+
59
+ Handles startup and shutdown events:
60
+ - Startup:
61
+ - Configure logging
62
+ - Initialize database tables
63
+ - Start cache cleanup task
64
+ - Start validation scheduler
65
+ - Schedule maintenance tasks
66
+ - Shutdown:
67
+ - Stop scheduler
68
+ - Stop cache cleanup
69
+ - Cleanup resources
70
+
71
+ Args:
72
+ app: FastAPI application instance.
73
+
74
+ Yields:
75
+ None during application runtime.
76
+ """
77
+ settings = get_settings()
78
+
79
+ # Configure logging
80
+ setup_logging(level=settings.log_level)
81
+ logger.info(f"Starting Truthound Dashboard v{__version__}")
82
+
83
+ # Initialize database
84
+ await init_db()
85
+ logger.info("Database initialized")
86
+
87
+ # Start cache cleanup
88
+ cache = get_cache()
89
+ await cache.start_cleanup_task()
90
+ logger.info("Cache cleanup task started")
91
+
92
+ # Start scheduler
93
+ scheduler = get_scheduler()
94
+ await scheduler.start()
95
+ logger.info("Validation scheduler started")
96
+
97
+ # Register maintenance tasks with scheduler
98
+ _register_maintenance_tasks()
99
+
100
+ yield
101
+
102
+ # Shutdown
103
+ logger.info("Shutting down Truthound Dashboard")
104
+
105
+ # Stop scheduler
106
+ await scheduler.stop()
107
+ logger.info("Scheduler stopped")
108
+
109
+ # Stop cache cleanup
110
+ await cache.stop_cleanup_task()
111
+ logger.info("Cache cleanup stopped")
112
+
113
+
114
+ def _register_maintenance_tasks() -> None:
115
+ """Register maintenance tasks with the scheduler.
116
+
117
+ Schedules daily cleanup at 3 AM and weekly vacuum on Sunday at 4 AM.
118
+ """
119
+ from apscheduler.triggers.cron import CronTrigger
120
+
121
+ from truthound_dashboard.core.maintenance import (
122
+ cleanup_notification_logs,
123
+ cleanup_old_profiles,
124
+ cleanup_old_validations,
125
+ vacuum_database,
126
+ )
127
+
128
+ scheduler = get_scheduler()
129
+
130
+ # Daily cleanup at 3 AM
131
+ scheduler._scheduler.add_job(
132
+ cleanup_old_validations,
133
+ trigger=CronTrigger.from_crontab("0 3 * * *"),
134
+ id="maintenance_validations",
135
+ replace_existing=True,
136
+ name="Cleanup old validations",
137
+ )
138
+
139
+ scheduler._scheduler.add_job(
140
+ cleanup_old_profiles,
141
+ trigger=CronTrigger.from_crontab("0 3 * * *"),
142
+ id="maintenance_profiles",
143
+ replace_existing=True,
144
+ name="Cleanup old profiles",
145
+ )
146
+
147
+ scheduler._scheduler.add_job(
148
+ cleanup_notification_logs,
149
+ trigger=CronTrigger.from_crontab("0 3 * * *"),
150
+ id="maintenance_notification_logs",
151
+ replace_existing=True,
152
+ name="Cleanup notification logs",
153
+ )
154
+
155
+ # Weekly vacuum on Sunday at 4 AM
156
+ scheduler._scheduler.add_job(
157
+ vacuum_database,
158
+ trigger=CronTrigger.from_crontab("0 4 * * 0"),
159
+ id="maintenance_vacuum",
160
+ replace_existing=True,
161
+ name="Database vacuum",
162
+ )
163
+
164
+ logger.info("Maintenance tasks registered")
165
+
166
+
167
+ def create_app() -> FastAPI:
168
+ """Create and configure FastAPI application.
169
+
170
+ This factory function creates a fully configured FastAPI app
171
+ with all middleware, routes, and static file handling.
172
+
173
+ Returns:
174
+ Configured FastAPI application.
175
+ """
176
+ app = FastAPI(
177
+ title="Truthound Dashboard",
178
+ description="Open-source data quality dashboard - GX Cloud alternative",
179
+ version=__version__,
180
+ lifespan=lifespan,
181
+ docs_url="/docs",
182
+ redoc_url="/redoc",
183
+ openapi_url="/api/openapi.json",
184
+ )
185
+
186
+ # Configure CORS (must be first middleware)
187
+ configure_cors(app)
188
+
189
+ # Add security middleware
190
+ configure_middleware(app)
191
+
192
+ # Register exception handlers
193
+ setup_error_handlers(app)
194
+
195
+ # Include API routes
196
+ app.include_router(api_router, prefix="/api/v1")
197
+
198
+ # Mount static files for React SPA
199
+ mount_static_files(app)
200
+
201
+ return app
202
+
203
+
204
+ def configure_cors(app: FastAPI) -> None:
205
+ """Configure CORS middleware.
206
+
207
+ Allows requests from the Vite dev server during development.
208
+
209
+ Args:
210
+ app: FastAPI application.
211
+ """
212
+ settings = get_settings()
213
+
214
+ origins = [
215
+ "http://localhost:5173", # Vite dev server
216
+ "http://127.0.0.1:5173",
217
+ f"http://localhost:{settings.port}", # Dashboard server
218
+ f"http://127.0.0.1:{settings.port}",
219
+ ]
220
+
221
+ app.add_middleware(
222
+ CORSMiddleware,
223
+ allow_origins=origins,
224
+ allow_credentials=True,
225
+ allow_methods=["*"],
226
+ allow_headers=["*"],
227
+ )
228
+
229
+
230
+ def configure_middleware(app: FastAPI) -> None:
231
+ """Configure security and logging middleware.
232
+
233
+ Adds:
234
+ - Request logging
235
+ - Rate limiting
236
+ - Security headers
237
+
238
+ Args:
239
+ app: FastAPI application.
240
+ """
241
+ # Request logging (last added = first executed)
242
+ app.add_middleware(RequestLoggingMiddleware)
243
+
244
+ # Rate limiting
245
+ rate_limit_config = RateLimitConfig(
246
+ requests_per_minute=120,
247
+ exclude_paths=[
248
+ "/health",
249
+ "/docs",
250
+ "/redoc",
251
+ "/openapi.json",
252
+ "/api/openapi.json",
253
+ ],
254
+ )
255
+ app.add_middleware(RateLimitMiddleware, config=rate_limit_config)
256
+
257
+ # Security headers
258
+ app.add_middleware(SecurityHeadersMiddleware)
259
+
260
+
261
+ def mount_static_files(app: FastAPI) -> None:
262
+ """Mount static files for React SPA.
263
+
264
+ Serves the built React application from the static directory.
265
+ Falls back to index.html for SPA routing.
266
+
267
+ Args:
268
+ app: FastAPI application.
269
+ """
270
+ static_dir = Path(__file__).parent / "static"
271
+
272
+ if not static_dir.exists():
273
+ return
274
+
275
+ index_file = static_dir / "index.html"
276
+ if not index_file.exists():
277
+ return
278
+
279
+ # Mount assets directory
280
+ assets_dir = static_dir / "assets"
281
+ if assets_dir.exists():
282
+ app.mount(
283
+ "/assets",
284
+ StaticFiles(directory=assets_dir),
285
+ name="assets",
286
+ )
287
+
288
+ # SPA fallback route - must be last
289
+ @app.get("/{full_path:path}", include_in_schema=False)
290
+ async def serve_spa(full_path: str) -> FileResponse:
291
+ """Serve SPA for all non-API routes.
292
+
293
+ Args:
294
+ full_path: Requested path.
295
+
296
+ Returns:
297
+ Static file or index.html for SPA routing.
298
+ """
299
+ # Check if it's a static file
300
+ file_path = static_dir / full_path
301
+ if file_path.exists() and file_path.is_file():
302
+ return FileResponse(file_path)
303
+
304
+ # Fall back to index.html for SPA routing
305
+ return FileResponse(index_file)
306
+
307
+
308
+ # Create app instance
309
+ app = create_app()
@@ -0,0 +1,150 @@
1
+ """Pydantic schemas for API request/response validation.
2
+
3
+ This module exports all schemas used in the API layer.
4
+ Schemas follow consistent naming conventions:
5
+ - *Base: Common fields
6
+ - *Create: POST request bodies
7
+ - *Update: PUT/PATCH request bodies
8
+ - *Response: Response bodies
9
+ - *ListResponse: Paginated list responses
10
+ """
11
+
12
+ from .base import (
13
+ BaseSchema,
14
+ ErrorResponse,
15
+ IDMixin,
16
+ ListResponseWrapper,
17
+ MessageResponse,
18
+ ResponseWrapper,
19
+ TimestampMixin,
20
+ )
21
+ from .drift import (
22
+ ColumnDriftResult,
23
+ DriftCompareRequest,
24
+ DriftComparisonListItem,
25
+ DriftComparisonListResponse,
26
+ DriftComparisonResponse,
27
+ DriftResult,
28
+ DriftSourceSummary,
29
+ )
30
+ from .history import (
31
+ FailureFrequencyItem,
32
+ HistoryQueryParams,
33
+ HistoryResponse,
34
+ HistorySummary,
35
+ RecentValidation,
36
+ TrendDataPoint,
37
+ )
38
+ from .profile import ColumnProfile, ProfileResponse
39
+ from .rule import (
40
+ RuleBase,
41
+ RuleCreate,
42
+ RuleListItem,
43
+ RuleListResponse,
44
+ RuleResponse,
45
+ RuleSummary,
46
+ RuleUpdate,
47
+ )
48
+ from .schedule import (
49
+ ScheduleActionResponse,
50
+ ScheduleBase,
51
+ ScheduleCreate,
52
+ ScheduleListItem,
53
+ ScheduleListResponse,
54
+ ScheduleResponse,
55
+ ScheduleUpdate,
56
+ )
57
+ from .schema import (
58
+ ColumnSchema,
59
+ SchemaLearnRequest,
60
+ SchemaResponse,
61
+ SchemaSummary,
62
+ SchemaUpdateRequest,
63
+ )
64
+ from .source import (
65
+ SourceBase,
66
+ SourceCreate,
67
+ SourceListResponse,
68
+ SourceResponse,
69
+ SourceSummary,
70
+ SourceType,
71
+ SourceUpdate,
72
+ )
73
+ from .validation import (
74
+ IssueSeverity,
75
+ ValidationIssue,
76
+ ValidationListItem,
77
+ ValidationListResponse,
78
+ ValidationResponse,
79
+ ValidationRunRequest,
80
+ ValidationStatus,
81
+ ValidationSummary,
82
+ )
83
+
84
+ __all__ = [
85
+ # Base
86
+ "BaseSchema",
87
+ "IDMixin",
88
+ "TimestampMixin",
89
+ "ResponseWrapper",
90
+ "ListResponseWrapper",
91
+ "ErrorResponse",
92
+ "MessageResponse",
93
+ # Source
94
+ "SourceType",
95
+ "SourceBase",
96
+ "SourceCreate",
97
+ "SourceUpdate",
98
+ "SourceResponse",
99
+ "SourceListResponse",
100
+ "SourceSummary",
101
+ # Rule
102
+ "RuleBase",
103
+ "RuleCreate",
104
+ "RuleUpdate",
105
+ "RuleResponse",
106
+ "RuleListItem",
107
+ "RuleListResponse",
108
+ "RuleSummary",
109
+ # Validation
110
+ "ValidationStatus",
111
+ "IssueSeverity",
112
+ "ValidationIssue",
113
+ "ValidationRunRequest",
114
+ "ValidationSummary",
115
+ "ValidationResponse",
116
+ "ValidationListItem",
117
+ "ValidationListResponse",
118
+ # Schema
119
+ "ColumnSchema",
120
+ "SchemaLearnRequest",
121
+ "SchemaUpdateRequest",
122
+ "SchemaResponse",
123
+ "SchemaSummary",
124
+ # Profile
125
+ "ColumnProfile",
126
+ "ProfileResponse",
127
+ # History
128
+ "TrendDataPoint",
129
+ "FailureFrequencyItem",
130
+ "RecentValidation",
131
+ "HistorySummary",
132
+ "HistoryResponse",
133
+ "HistoryQueryParams",
134
+ # Drift
135
+ "DriftCompareRequest",
136
+ "ColumnDriftResult",
137
+ "DriftResult",
138
+ "DriftSourceSummary",
139
+ "DriftComparisonResponse",
140
+ "DriftComparisonListItem",
141
+ "DriftComparisonListResponse",
142
+ # Schedule
143
+ "ScheduleBase",
144
+ "ScheduleCreate",
145
+ "ScheduleUpdate",
146
+ "ScheduleResponse",
147
+ "ScheduleListItem",
148
+ "ScheduleListResponse",
149
+ "ScheduleActionResponse",
150
+ ]