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,311 @@
|
|
|
1
|
+
"""Validation scheduler with notification integration.
|
|
2
|
+
|
|
3
|
+
This module provides scheduled validation execution with automatic
|
|
4
|
+
notification dispatch on failures.
|
|
5
|
+
|
|
6
|
+
The scheduler:
|
|
7
|
+
1. Runs scheduled validations based on cron expressions
|
|
8
|
+
2. Triggers notifications on validation failures
|
|
9
|
+
3. Updates schedule run timestamps
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
20
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
21
|
+
|
|
22
|
+
from truthound_dashboard.db import Schedule, Source, get_session
|
|
23
|
+
|
|
24
|
+
from .notifications.dispatcher import create_dispatcher
|
|
25
|
+
from .services import ValidationService
|
|
26
|
+
from .truthound_adapter import get_adapter
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValidationScheduler:
|
|
32
|
+
"""Scheduler for automated validation runs with notifications.
|
|
33
|
+
|
|
34
|
+
Manages scheduled validation jobs using APScheduler and integrates
|
|
35
|
+
with the notification system to alert on failures.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
scheduler = ValidationScheduler()
|
|
39
|
+
await scheduler.start()
|
|
40
|
+
# ... on shutdown ...
|
|
41
|
+
await scheduler.stop()
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
"""Initialize the scheduler."""
|
|
46
|
+
self._scheduler = AsyncIOScheduler()
|
|
47
|
+
self._jobs: dict[str, str] = {} # schedule_id -> job_id mapping
|
|
48
|
+
|
|
49
|
+
async def start(self) -> None:
|
|
50
|
+
"""Start the scheduler and load existing schedules."""
|
|
51
|
+
logger.info("Starting validation scheduler")
|
|
52
|
+
self._scheduler.start()
|
|
53
|
+
await self._load_schedules()
|
|
54
|
+
|
|
55
|
+
async def stop(self) -> None:
|
|
56
|
+
"""Stop the scheduler."""
|
|
57
|
+
logger.info("Stopping validation scheduler")
|
|
58
|
+
self._scheduler.shutdown(wait=False)
|
|
59
|
+
|
|
60
|
+
async def _load_schedules(self) -> None:
|
|
61
|
+
"""Load active schedules from database."""
|
|
62
|
+
async with get_session() as session:
|
|
63
|
+
from sqlalchemy import select
|
|
64
|
+
|
|
65
|
+
result = await session.execute(
|
|
66
|
+
select(Schedule).where(Schedule.is_active == True)
|
|
67
|
+
)
|
|
68
|
+
schedules = result.scalars().all()
|
|
69
|
+
|
|
70
|
+
for schedule in schedules:
|
|
71
|
+
self.add_schedule(schedule)
|
|
72
|
+
|
|
73
|
+
def add_schedule(self, schedule: Schedule) -> None:
|
|
74
|
+
"""Add a schedule to the scheduler.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
schedule: Schedule model to add.
|
|
78
|
+
"""
|
|
79
|
+
if schedule.id in self._jobs:
|
|
80
|
+
self.remove_schedule(schedule.id)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
trigger = CronTrigger.from_crontab(schedule.cron_expression)
|
|
84
|
+
job = self._scheduler.add_job(
|
|
85
|
+
self._run_validation,
|
|
86
|
+
trigger=trigger,
|
|
87
|
+
args=[schedule.id],
|
|
88
|
+
id=f"schedule_{schedule.id}",
|
|
89
|
+
name=f"Validation: {schedule.name}",
|
|
90
|
+
replace_existing=True,
|
|
91
|
+
)
|
|
92
|
+
self._jobs[schedule.id] = job.id
|
|
93
|
+
logger.info(f"Added schedule: {schedule.name} ({schedule.cron_expression})")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Failed to add schedule {schedule.id}: {e}")
|
|
96
|
+
|
|
97
|
+
def remove_schedule(self, schedule_id: str) -> None:
|
|
98
|
+
"""Remove a schedule from the scheduler.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
schedule_id: Schedule ID to remove.
|
|
102
|
+
"""
|
|
103
|
+
job_id = self._jobs.pop(schedule_id, None)
|
|
104
|
+
if job_id:
|
|
105
|
+
try:
|
|
106
|
+
self._scheduler.remove_job(job_id)
|
|
107
|
+
logger.info(f"Removed schedule: {schedule_id}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"Failed to remove schedule {schedule_id}: {e}")
|
|
110
|
+
|
|
111
|
+
def update_schedule(self, schedule: Schedule) -> None:
|
|
112
|
+
"""Update a schedule in the scheduler.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
schedule: Updated schedule model.
|
|
116
|
+
"""
|
|
117
|
+
if schedule.is_active:
|
|
118
|
+
self.add_schedule(schedule)
|
|
119
|
+
else:
|
|
120
|
+
self.remove_schedule(schedule.id)
|
|
121
|
+
|
|
122
|
+
async def _run_validation(self, schedule_id: str) -> None:
|
|
123
|
+
"""Execute a scheduled validation.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
schedule_id: ID of the schedule to run.
|
|
127
|
+
"""
|
|
128
|
+
logger.info(f"Running scheduled validation: {schedule_id}")
|
|
129
|
+
|
|
130
|
+
async with get_session() as session:
|
|
131
|
+
from sqlalchemy import select
|
|
132
|
+
|
|
133
|
+
# Get schedule
|
|
134
|
+
result = await session.execute(
|
|
135
|
+
select(Schedule).where(Schedule.id == schedule_id)
|
|
136
|
+
)
|
|
137
|
+
schedule = result.scalar_one_or_none()
|
|
138
|
+
|
|
139
|
+
if schedule is None:
|
|
140
|
+
logger.error(f"Schedule not found: {schedule_id}")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if not schedule.is_active:
|
|
144
|
+
logger.info(f"Schedule is inactive: {schedule_id}")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Get source
|
|
148
|
+
result = await session.execute(
|
|
149
|
+
select(Source).where(Source.id == schedule.source_id)
|
|
150
|
+
)
|
|
151
|
+
source = result.scalar_one_or_none()
|
|
152
|
+
|
|
153
|
+
if source is None:
|
|
154
|
+
logger.error(f"Source not found for schedule: {schedule_id}")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Run validation
|
|
158
|
+
validation_service = ValidationService(session)
|
|
159
|
+
config = schedule.config or {}
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
validation = await validation_service.run_validation(
|
|
163
|
+
schedule.source_id,
|
|
164
|
+
validators=config.get("validators"),
|
|
165
|
+
schema_path=config.get("schema_path"),
|
|
166
|
+
auto_schema=config.get("auto_schema", False),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Update schedule run time
|
|
170
|
+
schedule.mark_run(self._get_next_run(schedule.cron_expression))
|
|
171
|
+
await session.commit()
|
|
172
|
+
|
|
173
|
+
logger.info(
|
|
174
|
+
f"Validation completed for schedule {schedule_id}: "
|
|
175
|
+
f"passed={validation.passed}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Send notifications if failed and notifications are enabled
|
|
179
|
+
if schedule.notify_on_failure and not validation.passed:
|
|
180
|
+
await self._send_failure_notification(
|
|
181
|
+
session=session,
|
|
182
|
+
source=source,
|
|
183
|
+
schedule=schedule,
|
|
184
|
+
validation=validation,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Validation failed for schedule {schedule_id}: {e}")
|
|
189
|
+
|
|
190
|
+
# Send error notification
|
|
191
|
+
if schedule.notify_on_failure:
|
|
192
|
+
await self._send_error_notification(
|
|
193
|
+
session=session,
|
|
194
|
+
source=source,
|
|
195
|
+
schedule=schedule,
|
|
196
|
+
error_message=str(e),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
async def _send_failure_notification(
|
|
200
|
+
self,
|
|
201
|
+
session: Any,
|
|
202
|
+
source: Source,
|
|
203
|
+
schedule: Schedule,
|
|
204
|
+
validation: Any,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Send notification for validation failure.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
session: Database session.
|
|
210
|
+
source: Source that was validated.
|
|
211
|
+
schedule: Schedule that triggered the validation.
|
|
212
|
+
validation: Validation result.
|
|
213
|
+
"""
|
|
214
|
+
dispatcher = create_dispatcher(session)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
results = await dispatcher.notify_schedule_failed(
|
|
218
|
+
source_id=source.id,
|
|
219
|
+
source_name=source.name,
|
|
220
|
+
schedule_id=schedule.id,
|
|
221
|
+
schedule_name=schedule.name,
|
|
222
|
+
validation_id=validation.id,
|
|
223
|
+
error_message=f"Validation failed with {validation.total_issues or 0} issues",
|
|
224
|
+
)
|
|
225
|
+
await session.commit()
|
|
226
|
+
|
|
227
|
+
success_count = sum(1 for r in results if r.success)
|
|
228
|
+
logger.info(
|
|
229
|
+
f"Sent {success_count}/{len(results)} failure notifications "
|
|
230
|
+
f"for schedule {schedule.id}"
|
|
231
|
+
)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Failed to send notifications: {e}")
|
|
234
|
+
|
|
235
|
+
async def _send_error_notification(
|
|
236
|
+
self,
|
|
237
|
+
session: Any,
|
|
238
|
+
source: Source,
|
|
239
|
+
schedule: Schedule,
|
|
240
|
+
error_message: str,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Send notification for validation error.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
session: Database session.
|
|
246
|
+
source: Source that was validated.
|
|
247
|
+
schedule: Schedule that triggered the validation.
|
|
248
|
+
error_message: Error message.
|
|
249
|
+
"""
|
|
250
|
+
dispatcher = create_dispatcher(session)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
results = await dispatcher.notify_schedule_failed(
|
|
254
|
+
source_id=source.id,
|
|
255
|
+
source_name=source.name,
|
|
256
|
+
schedule_id=schedule.id,
|
|
257
|
+
schedule_name=schedule.name,
|
|
258
|
+
error_message=error_message,
|
|
259
|
+
)
|
|
260
|
+
await session.commit()
|
|
261
|
+
|
|
262
|
+
success_count = sum(1 for r in results if r.success)
|
|
263
|
+
logger.info(
|
|
264
|
+
f"Sent {success_count}/{len(results)} error notifications "
|
|
265
|
+
f"for schedule {schedule.id}"
|
|
266
|
+
)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Failed to send error notifications: {e}")
|
|
269
|
+
|
|
270
|
+
def _get_next_run(self, cron_expression: str) -> datetime | None:
|
|
271
|
+
"""Calculate next run time from cron expression.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
cron_expression: Cron expression.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Next run datetime or None if invalid.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
trigger = CronTrigger.from_crontab(cron_expression)
|
|
281
|
+
return trigger.get_next_fire_time(None, datetime.utcnow())
|
|
282
|
+
except Exception:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Singleton instance
|
|
287
|
+
_scheduler: ValidationScheduler | None = None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def get_scheduler() -> ValidationScheduler:
|
|
291
|
+
"""Get the singleton scheduler instance.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
ValidationScheduler instance.
|
|
295
|
+
"""
|
|
296
|
+
global _scheduler
|
|
297
|
+
if _scheduler is None:
|
|
298
|
+
_scheduler = ValidationScheduler()
|
|
299
|
+
return _scheduler
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def start_scheduler() -> None:
|
|
303
|
+
"""Start the validation scheduler."""
|
|
304
|
+
scheduler = get_scheduler()
|
|
305
|
+
await scheduler.start()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
async def stop_scheduler() -> None:
|
|
309
|
+
"""Stop the validation scheduler."""
|
|
310
|
+
scheduler = get_scheduler()
|
|
311
|
+
await scheduler.stop()
|