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,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()