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,561 @@
1
+ """Notification management API endpoints.
2
+
3
+ This module provides REST API endpoints for managing notification
4
+ channels, rules, and viewing delivery logs.
5
+
6
+ Endpoints:
7
+ Channels:
8
+ GET /notifications/channels - List channels
9
+ POST /notifications/channels - Create channel
10
+ GET /notifications/channels/{id} - Get channel
11
+ PUT /notifications/channels/{id} - Update channel
12
+ DELETE /notifications/channels/{id} - Delete channel
13
+ POST /notifications/channels/{id}/test - Test channel
14
+ GET /notifications/channels/types - Get available types
15
+
16
+ Rules:
17
+ GET /notifications/rules - List rules
18
+ POST /notifications/rules - Create rule
19
+ GET /notifications/rules/{id} - Get rule
20
+ PUT /notifications/rules/{id} - Update rule
21
+ DELETE /notifications/rules/{id} - Delete rule
22
+ GET /notifications/rules/conditions - Get valid conditions
23
+
24
+ Logs:
25
+ GET /notifications/logs - List logs
26
+ GET /notifications/logs/stats - Get statistics
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ from fastapi import APIRouter, Depends, HTTPException, Query
34
+ from pydantic import BaseModel, Field
35
+ from sqlalchemy.ext.asyncio import AsyncSession
36
+
37
+ from ..api.deps import get_session
38
+ from ..core.notifications.dispatcher import create_dispatcher
39
+ from ..core.notifications.service import (
40
+ NotificationChannelService,
41
+ NotificationLogService,
42
+ NotificationRuleService,
43
+ )
44
+
45
+ router = APIRouter(prefix="/notifications")
46
+
47
+
48
+ # =============================================================================
49
+ # Request/Response Schemas
50
+ # =============================================================================
51
+
52
+
53
+ class ChannelCreate(BaseModel):
54
+ """Request schema for creating a notification channel."""
55
+
56
+ name: str = Field(..., min_length=1, max_length=255)
57
+ type: str = Field(..., description="Channel type (slack, email, webhook)")
58
+ config: dict[str, Any] = Field(..., description="Channel-specific configuration")
59
+ is_active: bool = Field(default=True)
60
+
61
+
62
+ class ChannelUpdate(BaseModel):
63
+ """Request schema for updating a notification channel."""
64
+
65
+ name: str | None = Field(default=None, min_length=1, max_length=255)
66
+ config: dict[str, Any] | None = Field(default=None)
67
+ is_active: bool | None = Field(default=None)
68
+
69
+
70
+ class ChannelResponse(BaseModel):
71
+ """Response schema for a notification channel."""
72
+
73
+ id: str
74
+ name: str
75
+ type: str
76
+ is_active: bool
77
+ config_summary: str
78
+ created_at: str
79
+ updated_at: str
80
+
81
+
82
+ class RuleCreate(BaseModel):
83
+ """Request schema for creating a notification rule."""
84
+
85
+ name: str = Field(..., min_length=1, max_length=255)
86
+ condition: str = Field(..., description="Trigger condition type")
87
+ channel_ids: list[str] = Field(..., min_length=1)
88
+ condition_config: dict[str, Any] | None = Field(default=None)
89
+ source_ids: list[str] | None = Field(
90
+ default=None, description="Source IDs to filter (null = all sources)"
91
+ )
92
+ is_active: bool = Field(default=True)
93
+
94
+
95
+ class RuleUpdate(BaseModel):
96
+ """Request schema for updating a notification rule."""
97
+
98
+ name: str | None = Field(default=None, min_length=1, max_length=255)
99
+ condition: str | None = Field(default=None)
100
+ channel_ids: list[str] | None = Field(default=None)
101
+ condition_config: dict[str, Any] | None = Field(default=None)
102
+ source_ids: list[str] | None = Field(default=None)
103
+ is_active: bool | None = Field(default=None)
104
+
105
+
106
+ class RuleResponse(BaseModel):
107
+ """Response schema for a notification rule."""
108
+
109
+ id: str
110
+ name: str
111
+ condition: str
112
+ condition_config: dict[str, Any] | None
113
+ channel_ids: list[str]
114
+ source_ids: list[str] | None
115
+ is_active: bool
116
+ created_at: str
117
+ updated_at: str
118
+
119
+
120
+ class LogResponse(BaseModel):
121
+ """Response schema for a notification log."""
122
+
123
+ id: str
124
+ channel_id: str
125
+ rule_id: str | None
126
+ event_type: str
127
+ status: str
128
+ message_preview: str
129
+ error_message: str | None
130
+ created_at: str
131
+ sent_at: str | None
132
+
133
+
134
+ # =============================================================================
135
+ # Channel Endpoints
136
+ # =============================================================================
137
+
138
+
139
+ @router.get("/channels/types")
140
+ async def get_channel_types(
141
+ session: AsyncSession = Depends(get_session),
142
+ ) -> dict[str, Any]:
143
+ """Get available notification channel types with their configuration schemas."""
144
+ service = NotificationChannelService(session)
145
+ return {
146
+ "success": True,
147
+ "data": service.get_available_types(),
148
+ }
149
+
150
+
151
+ @router.get("/channels")
152
+ async def list_channels(
153
+ offset: int = Query(default=0, ge=0),
154
+ limit: int = Query(default=50, ge=1, le=100),
155
+ active_only: bool = Query(default=False),
156
+ channel_type: str | None = Query(default=None),
157
+ session: AsyncSession = Depends(get_session),
158
+ ) -> dict[str, Any]:
159
+ """List notification channels."""
160
+ service = NotificationChannelService(session)
161
+ channels = await service.list(
162
+ offset=offset,
163
+ limit=limit,
164
+ active_only=active_only,
165
+ channel_type=channel_type,
166
+ )
167
+
168
+ return {
169
+ "success": True,
170
+ "data": [
171
+ {
172
+ "id": c.id,
173
+ "name": c.name,
174
+ "type": c.type,
175
+ "is_active": c.is_active,
176
+ "config_summary": c.get_config_summary(),
177
+ "created_at": c.created_at.isoformat(),
178
+ "updated_at": c.updated_at.isoformat(),
179
+ }
180
+ for c in channels
181
+ ],
182
+ "count": len(channels),
183
+ }
184
+
185
+
186
+ @router.post("/channels")
187
+ async def create_channel(
188
+ request: ChannelCreate,
189
+ session: AsyncSession = Depends(get_session),
190
+ ) -> dict[str, Any]:
191
+ """Create a new notification channel."""
192
+ service = NotificationChannelService(session)
193
+
194
+ try:
195
+ channel = await service.create(
196
+ name=request.name,
197
+ channel_type=request.type,
198
+ config=request.config,
199
+ is_active=request.is_active,
200
+ )
201
+ await session.commit()
202
+
203
+ return {
204
+ "success": True,
205
+ "data": {
206
+ "id": channel.id,
207
+ "name": channel.name,
208
+ "type": channel.type,
209
+ "is_active": channel.is_active,
210
+ },
211
+ }
212
+ except ValueError as e:
213
+ raise HTTPException(status_code=400, detail=str(e))
214
+
215
+
216
+ @router.get("/channels/{channel_id}")
217
+ async def get_channel(
218
+ channel_id: str,
219
+ session: AsyncSession = Depends(get_session),
220
+ ) -> dict[str, Any]:
221
+ """Get a notification channel by ID."""
222
+ service = NotificationChannelService(session)
223
+ channel = await service.get_by_id(channel_id)
224
+
225
+ if channel is None:
226
+ raise HTTPException(status_code=404, detail="Channel not found")
227
+
228
+ return {
229
+ "success": True,
230
+ "data": {
231
+ "id": channel.id,
232
+ "name": channel.name,
233
+ "type": channel.type,
234
+ "is_active": channel.is_active,
235
+ "config_summary": channel.get_config_summary(),
236
+ "created_at": channel.created_at.isoformat(),
237
+ "updated_at": channel.updated_at.isoformat(),
238
+ },
239
+ }
240
+
241
+
242
+ @router.put("/channels/{channel_id}")
243
+ async def update_channel(
244
+ channel_id: str,
245
+ request: ChannelUpdate,
246
+ session: AsyncSession = Depends(get_session),
247
+ ) -> dict[str, Any]:
248
+ """Update a notification channel."""
249
+ service = NotificationChannelService(session)
250
+
251
+ try:
252
+ channel = await service.update(
253
+ channel_id,
254
+ name=request.name,
255
+ config=request.config,
256
+ is_active=request.is_active,
257
+ )
258
+ await session.commit()
259
+
260
+ if channel is None:
261
+ raise HTTPException(status_code=404, detail="Channel not found")
262
+
263
+ return {
264
+ "success": True,
265
+ "data": {
266
+ "id": channel.id,
267
+ "name": channel.name,
268
+ "type": channel.type,
269
+ "is_active": channel.is_active,
270
+ },
271
+ }
272
+ except ValueError as e:
273
+ raise HTTPException(status_code=400, detail=str(e))
274
+
275
+
276
+ @router.delete("/channels/{channel_id}")
277
+ async def delete_channel(
278
+ channel_id: str,
279
+ session: AsyncSession = Depends(get_session),
280
+ ) -> dict[str, Any]:
281
+ """Delete a notification channel."""
282
+ service = NotificationChannelService(session)
283
+ deleted = await service.delete(channel_id)
284
+ await session.commit()
285
+
286
+ if not deleted:
287
+ raise HTTPException(status_code=404, detail="Channel not found")
288
+
289
+ return {"success": True}
290
+
291
+
292
+ @router.post("/channels/{channel_id}/test")
293
+ async def test_channel(
294
+ channel_id: str,
295
+ session: AsyncSession = Depends(get_session),
296
+ ) -> dict[str, Any]:
297
+ """Send a test notification to a channel."""
298
+ dispatcher = create_dispatcher(session)
299
+ result = await dispatcher.test_channel(channel_id)
300
+ await session.commit()
301
+
302
+ return {
303
+ "success": result.success,
304
+ "message": "Test notification sent" if result.success else "Test failed",
305
+ "error": result.error,
306
+ }
307
+
308
+
309
+ # =============================================================================
310
+ # Rule Endpoints
311
+ # =============================================================================
312
+
313
+
314
+ @router.get("/rules/conditions")
315
+ async def get_rule_conditions(
316
+ session: AsyncSession = Depends(get_session),
317
+ ) -> dict[str, Any]:
318
+ """Get valid notification rule conditions."""
319
+ service = NotificationRuleService(session)
320
+ return {
321
+ "success": True,
322
+ "data": service.get_valid_conditions(),
323
+ }
324
+
325
+
326
+ @router.get("/rules")
327
+ async def list_rules(
328
+ offset: int = Query(default=0, ge=0),
329
+ limit: int = Query(default=50, ge=1, le=100),
330
+ active_only: bool = Query(default=False),
331
+ condition: str | None = Query(default=None),
332
+ session: AsyncSession = Depends(get_session),
333
+ ) -> dict[str, Any]:
334
+ """List notification rules."""
335
+ service = NotificationRuleService(session)
336
+ rules = await service.list(
337
+ offset=offset,
338
+ limit=limit,
339
+ active_only=active_only,
340
+ condition=condition,
341
+ )
342
+
343
+ return {
344
+ "success": True,
345
+ "data": [
346
+ {
347
+ "id": r.id,
348
+ "name": r.name,
349
+ "condition": r.condition,
350
+ "condition_config": r.condition_config,
351
+ "channel_ids": r.channel_ids,
352
+ "source_ids": r.source_ids,
353
+ "is_active": r.is_active,
354
+ "created_at": r.created_at.isoformat(),
355
+ "updated_at": r.updated_at.isoformat(),
356
+ }
357
+ for r in rules
358
+ ],
359
+ "count": len(rules),
360
+ }
361
+
362
+
363
+ @router.post("/rules")
364
+ async def create_rule(
365
+ request: RuleCreate,
366
+ session: AsyncSession = Depends(get_session),
367
+ ) -> dict[str, Any]:
368
+ """Create a new notification rule."""
369
+ service = NotificationRuleService(session)
370
+
371
+ try:
372
+ rule = await service.create(
373
+ name=request.name,
374
+ condition=request.condition,
375
+ channel_ids=request.channel_ids,
376
+ condition_config=request.condition_config,
377
+ source_ids=request.source_ids,
378
+ is_active=request.is_active,
379
+ )
380
+ await session.commit()
381
+
382
+ return {
383
+ "success": True,
384
+ "data": {
385
+ "id": rule.id,
386
+ "name": rule.name,
387
+ "condition": rule.condition,
388
+ },
389
+ }
390
+ except ValueError as e:
391
+ raise HTTPException(status_code=400, detail=str(e))
392
+
393
+
394
+ @router.get("/rules/{rule_id}")
395
+ async def get_rule(
396
+ rule_id: str,
397
+ session: AsyncSession = Depends(get_session),
398
+ ) -> dict[str, Any]:
399
+ """Get a notification rule by ID."""
400
+ service = NotificationRuleService(session)
401
+ rule = await service.get_by_id(rule_id)
402
+
403
+ if rule is None:
404
+ raise HTTPException(status_code=404, detail="Rule not found")
405
+
406
+ return {
407
+ "success": True,
408
+ "data": {
409
+ "id": rule.id,
410
+ "name": rule.name,
411
+ "condition": rule.condition,
412
+ "condition_config": rule.condition_config,
413
+ "channel_ids": rule.channel_ids,
414
+ "source_ids": rule.source_ids,
415
+ "is_active": rule.is_active,
416
+ "created_at": rule.created_at.isoformat(),
417
+ "updated_at": rule.updated_at.isoformat(),
418
+ },
419
+ }
420
+
421
+
422
+ @router.put("/rules/{rule_id}")
423
+ async def update_rule(
424
+ rule_id: str,
425
+ request: RuleUpdate,
426
+ session: AsyncSession = Depends(get_session),
427
+ ) -> dict[str, Any]:
428
+ """Update a notification rule."""
429
+ service = NotificationRuleService(session)
430
+
431
+ try:
432
+ rule = await service.update(
433
+ rule_id,
434
+ name=request.name,
435
+ condition=request.condition,
436
+ channel_ids=request.channel_ids,
437
+ condition_config=request.condition_config,
438
+ source_ids=request.source_ids,
439
+ is_active=request.is_active,
440
+ )
441
+ await session.commit()
442
+
443
+ if rule is None:
444
+ raise HTTPException(status_code=404, detail="Rule not found")
445
+
446
+ return {
447
+ "success": True,
448
+ "data": {
449
+ "id": rule.id,
450
+ "name": rule.name,
451
+ "condition": rule.condition,
452
+ },
453
+ }
454
+ except ValueError as e:
455
+ raise HTTPException(status_code=400, detail=str(e))
456
+
457
+
458
+ @router.delete("/rules/{rule_id}")
459
+ async def delete_rule(
460
+ rule_id: str,
461
+ session: AsyncSession = Depends(get_session),
462
+ ) -> dict[str, Any]:
463
+ """Delete a notification rule."""
464
+ service = NotificationRuleService(session)
465
+ deleted = await service.delete(rule_id)
466
+ await session.commit()
467
+
468
+ if not deleted:
469
+ raise HTTPException(status_code=404, detail="Rule not found")
470
+
471
+ return {"success": True}
472
+
473
+
474
+ # =============================================================================
475
+ # Log Endpoints
476
+ # =============================================================================
477
+
478
+
479
+ @router.get("/logs")
480
+ async def list_logs(
481
+ offset: int = Query(default=0, ge=0),
482
+ limit: int = Query(default=50, ge=1, le=100),
483
+ channel_id: str | None = Query(default=None),
484
+ status: str | None = Query(default=None),
485
+ hours: int | None = Query(default=None, ge=1, le=168),
486
+ session: AsyncSession = Depends(get_session),
487
+ ) -> dict[str, Any]:
488
+ """List notification delivery logs."""
489
+ service = NotificationLogService(session)
490
+ logs = await service.list(
491
+ offset=offset,
492
+ limit=limit,
493
+ channel_id=channel_id,
494
+ status=status,
495
+ hours=hours,
496
+ )
497
+
498
+ return {
499
+ "success": True,
500
+ "data": [
501
+ {
502
+ "id": log.id,
503
+ "channel_id": log.channel_id,
504
+ "rule_id": log.rule_id,
505
+ "event_type": log.event_type,
506
+ "status": log.status,
507
+ "message_preview": (
508
+ log.message[:100] + "..." if len(log.message) > 100 else log.message
509
+ ),
510
+ "error_message": log.error_message,
511
+ "created_at": log.created_at.isoformat(),
512
+ "sent_at": log.sent_at.isoformat() if log.sent_at else None,
513
+ }
514
+ for log in logs
515
+ ],
516
+ "count": len(logs),
517
+ }
518
+
519
+
520
+ @router.get("/logs/stats")
521
+ async def get_log_stats(
522
+ hours: int = Query(default=24, ge=1, le=168),
523
+ session: AsyncSession = Depends(get_session),
524
+ ) -> dict[str, Any]:
525
+ """Get notification delivery statistics."""
526
+ service = NotificationLogService(session)
527
+ stats = await service.get_stats(hours=hours)
528
+
529
+ return {
530
+ "success": True,
531
+ "data": stats,
532
+ }
533
+
534
+
535
+ @router.get("/logs/{log_id}")
536
+ async def get_log(
537
+ log_id: str,
538
+ session: AsyncSession = Depends(get_session),
539
+ ) -> dict[str, Any]:
540
+ """Get a notification log by ID."""
541
+ service = NotificationLogService(session)
542
+ log = await service.get_by_id(log_id)
543
+
544
+ if log is None:
545
+ raise HTTPException(status_code=404, detail="Log not found")
546
+
547
+ return {
548
+ "success": True,
549
+ "data": {
550
+ "id": log.id,
551
+ "channel_id": log.channel_id,
552
+ "rule_id": log.rule_id,
553
+ "event_type": log.event_type,
554
+ "event_data": log.event_data,
555
+ "status": log.status,
556
+ "message": log.message,
557
+ "error_message": log.error_message,
558
+ "created_at": log.created_at.isoformat(),
559
+ "sent_at": log.sent_at.isoformat() if log.sent_at else None,
560
+ },
561
+ }
@@ -0,0 +1,52 @@
1
+ """Profile API endpoints.
2
+
3
+ This module provides endpoints for data profiling.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, HTTPException, Path
11
+
12
+ from truthound_dashboard.schemas import ProfileResponse
13
+
14
+ from .deps import ProfileServiceDep, SourceServiceDep
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ @router.post(
20
+ "/sources/{source_id}/profile",
21
+ response_model=ProfileResponse,
22
+ summary="Profile source",
23
+ description="Run data profiling on a source",
24
+ )
25
+ async def profile_source(
26
+ service: ProfileServiceDep,
27
+ source_service: SourceServiceDep,
28
+ source_id: Annotated[str, Path(description="Source ID to profile")],
29
+ ) -> ProfileResponse:
30
+ """Run data profiling on a source.
31
+
32
+ Args:
33
+ service: Injected profile service.
34
+ source_service: Injected source service.
35
+ source_id: Source to profile.
36
+
37
+ Returns:
38
+ Profiling result with column statistics.
39
+
40
+ Raises:
41
+ HTTPException: 404 if source not found.
42
+ """
43
+ # Verify source exists
44
+ source = await source_service.get_by_id(source_id)
45
+ if source is None:
46
+ raise HTTPException(status_code=404, detail="Source not found")
47
+
48
+ try:
49
+ result = await service.profile_source(source_id)
50
+ return ProfileResponse.from_result(result)
51
+ except Exception as e:
52
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,83 @@
1
+ """API router configuration.
2
+
3
+ This module configures the main API router and includes all sub-routers.
4
+ """
5
+
6
+ from fastapi import APIRouter
7
+
8
+ from . import (
9
+ drift,
10
+ health,
11
+ history,
12
+ notifications,
13
+ profile,
14
+ rules,
15
+ schedules,
16
+ schemas,
17
+ sources,
18
+ validations,
19
+ )
20
+
21
+ api_router = APIRouter()
22
+
23
+ # Health endpoints (no prefix)
24
+ api_router.include_router(
25
+ health.router,
26
+ tags=["health"],
27
+ )
28
+
29
+ # Source management
30
+ api_router.include_router(
31
+ sources.router,
32
+ prefix="/sources",
33
+ tags=["sources"],
34
+ )
35
+
36
+ # Schema management
37
+ api_router.include_router(
38
+ schemas.router,
39
+ tags=["schemas"],
40
+ )
41
+
42
+ # Rules management
43
+ api_router.include_router(
44
+ rules.router,
45
+ tags=["rules"],
46
+ )
47
+
48
+ # Validation endpoints
49
+ api_router.include_router(
50
+ validations.router,
51
+ prefix="/validations",
52
+ tags=["validations"],
53
+ )
54
+
55
+ # Profiling endpoints
56
+ api_router.include_router(
57
+ profile.router,
58
+ tags=["profiling"],
59
+ )
60
+
61
+ # History endpoints (Phase 2)
62
+ api_router.include_router(
63
+ history.router,
64
+ tags=["history"],
65
+ )
66
+
67
+ # Drift detection endpoints (Phase 2)
68
+ api_router.include_router(
69
+ drift.router,
70
+ tags=["drift"],
71
+ )
72
+
73
+ # Schedule management endpoints (Phase 2)
74
+ api_router.include_router(
75
+ schedules.router,
76
+ tags=["schedules"],
77
+ )
78
+
79
+ # Notification management endpoints (Phase 3)
80
+ api_router.include_router(
81
+ notifications.router,
82
+ tags=["notifications"],
83
+ )