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,277 @@
1
+ """Rules API endpoints.
2
+
3
+ This module provides endpoints for managing custom validation rules.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, HTTPException, Path, Query
11
+
12
+ from truthound_dashboard.schemas import (
13
+ MessageResponse,
14
+ RuleCreate,
15
+ RuleListItem,
16
+ RuleListResponse,
17
+ RuleResponse,
18
+ RuleUpdate,
19
+ )
20
+
21
+ from .deps import RuleServiceDep, SourceServiceDep
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ @router.get(
27
+ "/sources/{source_id}/rules",
28
+ response_model=RuleListResponse,
29
+ summary="List source rules",
30
+ description="Get all validation rules for a data source",
31
+ )
32
+ async def list_source_rules(
33
+ service: RuleServiceDep,
34
+ source_service: SourceServiceDep,
35
+ source_id: Annotated[str, Path(description="Source ID")],
36
+ limit: Annotated[int, Query(ge=1, le=100, description="Maximum items")] = 50,
37
+ active_only: Annotated[bool, Query(description="Only return active rules")] = False,
38
+ ) -> RuleListResponse:
39
+ """List all rules for a source.
40
+
41
+ Args:
42
+ service: Injected rule service.
43
+ source_service: Injected source service.
44
+ source_id: Source to get rules for.
45
+ limit: Maximum rules to return.
46
+ active_only: Only return active rules.
47
+
48
+ Returns:
49
+ List of rules.
50
+
51
+ Raises:
52
+ HTTPException: 404 if source not found.
53
+ """
54
+ # Verify source exists
55
+ source = await source_service.get_by_id(source_id)
56
+ if source is None:
57
+ raise HTTPException(status_code=404, detail="Source not found")
58
+
59
+ rules = await service.get_rules_for_source(
60
+ source_id,
61
+ limit=limit,
62
+ active_only=active_only,
63
+ )
64
+
65
+ return RuleListResponse(
66
+ data=[RuleListItem.from_model(r) for r in rules],
67
+ total=len(rules),
68
+ limit=limit,
69
+ )
70
+
71
+
72
+ @router.get(
73
+ "/sources/{source_id}/rules/active",
74
+ response_model=RuleResponse | None,
75
+ summary="Get active rule",
76
+ description="Get the currently active rule for a source",
77
+ )
78
+ async def get_active_rule(
79
+ service: RuleServiceDep,
80
+ source_service: SourceServiceDep,
81
+ source_id: Annotated[str, Path(description="Source ID")],
82
+ ) -> RuleResponse | None:
83
+ """Get the active rule for a source.
84
+
85
+ Args:
86
+ service: Injected rule service.
87
+ source_service: Injected source service.
88
+ source_id: Source to get active rule for.
89
+
90
+ Returns:
91
+ Active rule or None.
92
+
93
+ Raises:
94
+ HTTPException: 404 if source not found.
95
+ """
96
+ # Verify source exists
97
+ source = await source_service.get_by_id(source_id)
98
+ if source is None:
99
+ raise HTTPException(status_code=404, detail="Source not found")
100
+
101
+ rule = await service.get_active_rule(source_id)
102
+ if rule is None:
103
+ return None
104
+ return RuleResponse.from_model(rule)
105
+
106
+
107
+ @router.post(
108
+ "/sources/{source_id}/rules",
109
+ response_model=RuleResponse,
110
+ status_code=201,
111
+ summary="Create rule",
112
+ description="Create a new validation rule for a source",
113
+ )
114
+ async def create_rule(
115
+ service: RuleServiceDep,
116
+ source_service: SourceServiceDep,
117
+ source_id: Annotated[str, Path(description="Source ID")],
118
+ rule: RuleCreate,
119
+ activate: Annotated[bool, Query(description="Activate this rule")] = True,
120
+ ) -> RuleResponse:
121
+ """Create a new rule for a source.
122
+
123
+ Args:
124
+ service: Injected rule service.
125
+ source_service: Injected source service.
126
+ source_id: Source to create rule for.
127
+ rule: Rule creation data.
128
+ activate: Whether to make this the active rule.
129
+
130
+ Returns:
131
+ Created rule.
132
+
133
+ Raises:
134
+ HTTPException: 404 if source not found, 400 if YAML is invalid.
135
+ """
136
+ # Verify source exists
137
+ source = await source_service.get_by_id(source_id)
138
+ if source is None:
139
+ raise HTTPException(status_code=404, detail="Source not found")
140
+
141
+ try:
142
+ created = await service.create_rule(
143
+ source_id,
144
+ rules_yaml=rule.rules_yaml,
145
+ name=rule.name,
146
+ description=rule.description,
147
+ activate=activate,
148
+ )
149
+ return RuleResponse.from_model(created)
150
+ except ValueError as e:
151
+ raise HTTPException(status_code=400, detail=str(e))
152
+
153
+
154
+ @router.get(
155
+ "/rules/{rule_id}",
156
+ response_model=RuleResponse,
157
+ summary="Get rule",
158
+ description="Get a specific rule by ID",
159
+ )
160
+ async def get_rule(
161
+ service: RuleServiceDep,
162
+ rule_id: Annotated[str, Path(description="Rule ID")],
163
+ ) -> RuleResponse:
164
+ """Get a specific rule.
165
+
166
+ Args:
167
+ service: Injected rule service.
168
+ rule_id: Rule unique identifier.
169
+
170
+ Returns:
171
+ Rule details.
172
+
173
+ Raises:
174
+ HTTPException: 404 if rule not found.
175
+ """
176
+ rule = await service.get_rule(rule_id)
177
+ if rule is None:
178
+ raise HTTPException(status_code=404, detail="Rule not found")
179
+ return RuleResponse.from_model(rule)
180
+
181
+
182
+ @router.put(
183
+ "/rules/{rule_id}",
184
+ response_model=RuleResponse,
185
+ summary="Update rule",
186
+ description="Update an existing rule",
187
+ )
188
+ async def update_rule(
189
+ service: RuleServiceDep,
190
+ rule_id: Annotated[str, Path(description="Rule ID")],
191
+ update: RuleUpdate,
192
+ ) -> RuleResponse:
193
+ """Update an existing rule.
194
+
195
+ Args:
196
+ service: Injected rule service.
197
+ rule_id: Rule unique identifier.
198
+ update: Update data.
199
+
200
+ Returns:
201
+ Updated rule.
202
+
203
+ Raises:
204
+ HTTPException: 404 if rule not found, 400 if YAML is invalid.
205
+ """
206
+ try:
207
+ updated = await service.update_rule(
208
+ rule_id,
209
+ name=update.name,
210
+ description=update.description,
211
+ rules_yaml=update.rules_yaml,
212
+ version=update.version,
213
+ is_active=update.is_active,
214
+ )
215
+ if updated is None:
216
+ raise HTTPException(status_code=404, detail="Rule not found")
217
+ return RuleResponse.from_model(updated)
218
+ except ValueError as e:
219
+ raise HTTPException(status_code=400, detail=str(e))
220
+
221
+
222
+ @router.delete(
223
+ "/rules/{rule_id}",
224
+ response_model=MessageResponse,
225
+ summary="Delete rule",
226
+ description="Delete a rule",
227
+ )
228
+ async def delete_rule(
229
+ service: RuleServiceDep,
230
+ rule_id: Annotated[str, Path(description="Rule ID")],
231
+ ) -> MessageResponse:
232
+ """Delete a rule.
233
+
234
+ Args:
235
+ service: Injected rule service.
236
+ rule_id: Rule unique identifier.
237
+
238
+ Returns:
239
+ Success message.
240
+
241
+ Raises:
242
+ HTTPException: 404 if rule not found.
243
+ """
244
+ deleted = await service.delete_rule(rule_id)
245
+ if not deleted:
246
+ raise HTTPException(status_code=404, detail="Rule not found")
247
+ return MessageResponse(message="Rule deleted successfully")
248
+
249
+
250
+ @router.post(
251
+ "/rules/{rule_id}/activate",
252
+ response_model=RuleResponse,
253
+ summary="Activate rule",
254
+ description="Activate a rule and deactivate others for the same source",
255
+ )
256
+ async def activate_rule(
257
+ service: RuleServiceDep,
258
+ rule_id: Annotated[str, Path(description="Rule ID")],
259
+ ) -> RuleResponse:
260
+ """Activate a rule.
261
+
262
+ This will deactivate any other active rules for the same source.
263
+
264
+ Args:
265
+ service: Injected rule service.
266
+ rule_id: Rule unique identifier.
267
+
268
+ Returns:
269
+ Activated rule.
270
+
271
+ Raises:
272
+ HTTPException: 404 if rule not found.
273
+ """
274
+ rule = await service.activate_rule(rule_id)
275
+ if rule is None:
276
+ raise HTTPException(status_code=404, detail="Rule not found")
277
+ return RuleResponse.from_model(rule)
@@ -0,0 +1,329 @@
1
+ """Schedule management API endpoints.
2
+
3
+ Provides CRUD endpoints for validation schedules.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, Depends, HTTPException, Query
11
+
12
+ from truthound_dashboard.core import ScheduleService, ValidationService
13
+ from truthound_dashboard.schemas import (
14
+ ScheduleActionResponse,
15
+ ScheduleCreate,
16
+ ScheduleListResponse,
17
+ ScheduleUpdate,
18
+ )
19
+
20
+ from .deps import SessionDep
21
+
22
+ router = APIRouter()
23
+
24
+
25
+ async def get_schedule_service(session: SessionDep) -> ScheduleService:
26
+ """Get schedule service dependency."""
27
+ return ScheduleService(session)
28
+
29
+
30
+ ScheduleServiceDep = Annotated[ScheduleService, Depends(get_schedule_service)]
31
+
32
+
33
+ def _schedule_to_response(schedule) -> dict:
34
+ """Convert schedule model to response dict."""
35
+ return {
36
+ "id": schedule.id,
37
+ "name": schedule.name,
38
+ "source_id": schedule.source_id,
39
+ "cron_expression": schedule.cron_expression,
40
+ "is_active": schedule.is_active,
41
+ "notify_on_failure": schedule.notify_on_failure,
42
+ "last_run_at": (
43
+ schedule.last_run_at.isoformat() if schedule.last_run_at else None
44
+ ),
45
+ "next_run_at": (
46
+ schedule.next_run_at.isoformat() if schedule.next_run_at else None
47
+ ),
48
+ "config": schedule.config,
49
+ "created_at": schedule.created_at.isoformat() if schedule.created_at else None,
50
+ "updated_at": schedule.updated_at.isoformat() if schedule.updated_at else None,
51
+ }
52
+
53
+
54
+ @router.get(
55
+ "/schedules",
56
+ response_model=ScheduleListResponse,
57
+ summary="List schedules",
58
+ description="List all validation schedules.",
59
+ )
60
+ async def list_schedules(
61
+ service: ScheduleServiceDep,
62
+ source_id: str | None = Query(None, description="Filter by source ID"),
63
+ active_only: bool = Query(False, description="Only return active schedules"),
64
+ limit: int = Query(100, ge=1, le=500, description="Maximum results"),
65
+ ) -> ScheduleListResponse:
66
+ """List validation schedules.
67
+
68
+ Args:
69
+ service: Schedule service.
70
+ source_id: Optional source ID filter.
71
+ active_only: Only return active schedules.
72
+ limit: Maximum results.
73
+
74
+ Returns:
75
+ List of schedules.
76
+ """
77
+ schedules = await service.list_schedules(
78
+ source_id=source_id,
79
+ active_only=active_only,
80
+ limit=limit,
81
+ )
82
+
83
+ return ScheduleListResponse(
84
+ success=True,
85
+ data=[_schedule_to_response(s) for s in schedules],
86
+ total=len(schedules),
87
+ )
88
+
89
+
90
+ @router.post(
91
+ "/schedules",
92
+ response_model=dict,
93
+ status_code=201,
94
+ summary="Create schedule",
95
+ description="Create a new validation schedule.",
96
+ )
97
+ async def create_schedule(
98
+ request: ScheduleCreate,
99
+ service: ScheduleServiceDep,
100
+ ) -> dict:
101
+ """Create a new schedule.
102
+
103
+ Args:
104
+ request: Schedule creation request.
105
+ service: Schedule service.
106
+
107
+ Returns:
108
+ Created schedule.
109
+ """
110
+ try:
111
+ schedule = await service.create_schedule(
112
+ source_id=request.source_id,
113
+ name=request.name,
114
+ cron_expression=request.cron_expression,
115
+ notify_on_failure=request.notify_on_failure,
116
+ config=request.config,
117
+ )
118
+
119
+ return {
120
+ "success": True,
121
+ "data": _schedule_to_response(schedule),
122
+ }
123
+ except ValueError as e:
124
+ raise HTTPException(status_code=400, detail=str(e))
125
+ except Exception as e:
126
+ raise HTTPException(status_code=500, detail=str(e))
127
+
128
+
129
+ @router.get(
130
+ "/schedules/{schedule_id}",
131
+ response_model=dict,
132
+ summary="Get schedule",
133
+ description="Get a specific schedule by ID.",
134
+ )
135
+ async def get_schedule(
136
+ schedule_id: str,
137
+ service: ScheduleServiceDep,
138
+ ) -> dict:
139
+ """Get a schedule by ID.
140
+
141
+ Args:
142
+ schedule_id: Schedule ID.
143
+ service: Schedule service.
144
+
145
+ Returns:
146
+ Schedule details.
147
+ """
148
+ schedule = await service.get_schedule(schedule_id)
149
+ if schedule is None:
150
+ raise HTTPException(status_code=404, detail="Schedule not found")
151
+
152
+ return {
153
+ "success": True,
154
+ "data": _schedule_to_response(schedule),
155
+ }
156
+
157
+
158
+ @router.put(
159
+ "/schedules/{schedule_id}",
160
+ response_model=dict,
161
+ summary="Update schedule",
162
+ description="Update an existing schedule.",
163
+ )
164
+ async def update_schedule(
165
+ schedule_id: str,
166
+ request: ScheduleUpdate,
167
+ service: ScheduleServiceDep,
168
+ ) -> dict:
169
+ """Update a schedule.
170
+
171
+ Args:
172
+ schedule_id: Schedule ID.
173
+ request: Update request.
174
+ service: Schedule service.
175
+
176
+ Returns:
177
+ Updated schedule.
178
+ """
179
+ try:
180
+ schedule = await service.update_schedule(
181
+ schedule_id,
182
+ name=request.name,
183
+ cron_expression=request.cron_expression,
184
+ notify_on_failure=request.notify_on_failure,
185
+ config=request.config,
186
+ )
187
+
188
+ if schedule is None:
189
+ raise HTTPException(status_code=404, detail="Schedule not found")
190
+
191
+ return {
192
+ "success": True,
193
+ "data": _schedule_to_response(schedule),
194
+ }
195
+ except ValueError as e:
196
+ raise HTTPException(status_code=400, detail=str(e))
197
+
198
+
199
+ @router.delete(
200
+ "/schedules/{schedule_id}",
201
+ response_model=dict,
202
+ summary="Delete schedule",
203
+ description="Delete a schedule.",
204
+ )
205
+ async def delete_schedule(
206
+ schedule_id: str,
207
+ service: ScheduleServiceDep,
208
+ ) -> dict:
209
+ """Delete a schedule.
210
+
211
+ Args:
212
+ schedule_id: Schedule ID.
213
+ service: Schedule service.
214
+
215
+ Returns:
216
+ Success message.
217
+ """
218
+ deleted = await service.delete_schedule(schedule_id)
219
+ if not deleted:
220
+ raise HTTPException(status_code=404, detail="Schedule not found")
221
+
222
+ return {"success": True, "message": "Schedule deleted"}
223
+
224
+
225
+ @router.post(
226
+ "/schedules/{schedule_id}/pause",
227
+ response_model=ScheduleActionResponse,
228
+ summary="Pause schedule",
229
+ description="Pause a schedule.",
230
+ )
231
+ async def pause_schedule(
232
+ schedule_id: str,
233
+ service: ScheduleServiceDep,
234
+ ) -> ScheduleActionResponse:
235
+ """Pause a schedule.
236
+
237
+ Args:
238
+ schedule_id: Schedule ID.
239
+ service: Schedule service.
240
+
241
+ Returns:
242
+ Action result with updated schedule.
243
+ """
244
+ schedule = await service.pause_schedule(schedule_id)
245
+ if schedule is None:
246
+ raise HTTPException(status_code=404, detail="Schedule not found")
247
+
248
+ return ScheduleActionResponse(
249
+ success=True,
250
+ message="Schedule paused",
251
+ schedule=_schedule_to_response(schedule),
252
+ )
253
+
254
+
255
+ @router.post(
256
+ "/schedules/{schedule_id}/resume",
257
+ response_model=ScheduleActionResponse,
258
+ summary="Resume schedule",
259
+ description="Resume a paused schedule.",
260
+ )
261
+ async def resume_schedule(
262
+ schedule_id: str,
263
+ service: ScheduleServiceDep,
264
+ ) -> ScheduleActionResponse:
265
+ """Resume a paused schedule.
266
+
267
+ Args:
268
+ schedule_id: Schedule ID.
269
+ service: Schedule service.
270
+
271
+ Returns:
272
+ Action result with updated schedule.
273
+ """
274
+ schedule = await service.resume_schedule(schedule_id)
275
+ if schedule is None:
276
+ raise HTTPException(status_code=404, detail="Schedule not found")
277
+
278
+ return ScheduleActionResponse(
279
+ success=True,
280
+ message="Schedule resumed",
281
+ schedule=_schedule_to_response(schedule),
282
+ )
283
+
284
+
285
+ @router.post(
286
+ "/schedules/{schedule_id}/run",
287
+ response_model=dict,
288
+ summary="Run schedule now",
289
+ description="Trigger immediate execution of a scheduled validation.",
290
+ )
291
+ async def run_schedule_now(
292
+ schedule_id: str,
293
+ service: ScheduleServiceDep,
294
+ session: SessionDep,
295
+ ) -> dict:
296
+ """Run a scheduled validation immediately.
297
+
298
+ Args:
299
+ schedule_id: Schedule ID.
300
+ service: Schedule service.
301
+ session: Database session.
302
+
303
+ Returns:
304
+ Validation result.
305
+ """
306
+ schedule = await service.get_schedule(schedule_id)
307
+ if schedule is None:
308
+ raise HTTPException(status_code=404, detail="Schedule not found")
309
+
310
+ # Run validation using ValidationService
311
+ validation_service = ValidationService(session)
312
+ config = schedule.config or {}
313
+
314
+ try:
315
+ validation = await validation_service.run_validation(
316
+ schedule.source_id,
317
+ validators=config.get("validators"),
318
+ schema_path=config.get("schema_path"),
319
+ auto_schema=config.get("auto_schema", False),
320
+ )
321
+
322
+ return {
323
+ "success": True,
324
+ "message": "Validation triggered",
325
+ "validation_id": validation.id,
326
+ "passed": validation.passed,
327
+ }
328
+ except Exception as e:
329
+ raise HTTPException(status_code=500, detail=str(e))