truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__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 (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +645 -23
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +15 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -206,8 +206,6 @@ async def generate_validation_report(
206
206
  "text/html": {},
207
207
  "text/csv": {},
208
208
  "application/json": {},
209
- "text/markdown": {},
210
- "application/pdf": {},
211
209
  },
212
210
  },
213
211
  404: {"description": "Validation not found"},
@@ -237,7 +235,7 @@ async def download_validation_report(
237
235
  Args:
238
236
  service: Injected validation service.
239
237
  validation_id: Validation to generate report for.
240
- format: Report format (html, csv, json, markdown).
238
+ format: Report format (html, csv, json).
241
239
  theme: Visual theme for the report.
242
240
  locale: Report language code.
243
241
  include_samples: Include sample problematic values.
@@ -250,8 +248,8 @@ async def download_validation_report(
250
248
  HTTPException: 404 if validation not found.
251
249
  HTTPException: 400 if format or locale is not supported.
252
250
  """
253
- # Get validation
254
- validation = await service.get_validation(validation_id)
251
+ # Get validation with source eagerly loaded (required for report generation)
252
+ validation = await service.get_validation(validation_id, with_source=True)
255
253
  if validation is None:
256
254
  raise HTTPException(status_code=404, detail="Validation not found")
257
255
 
@@ -295,7 +293,6 @@ async def download_validation_report(
295
293
  "text/html": {},
296
294
  "text/csv": {},
297
295
  "application/json": {},
298
- "text/markdown": {},
299
296
  },
300
297
  },
301
298
  404: {"description": "Validation not found"},
@@ -320,7 +317,7 @@ async def preview_validation_report(
320
317
  Args:
321
318
  service: Injected validation service.
322
319
  validation_id: Validation to generate report for.
323
- format: Report format (html, csv, json, markdown).
320
+ format: Report format (html, csv, json).
324
321
  theme: Visual theme for the report.
325
322
  locale: Report language code.
326
323
 
@@ -331,8 +328,8 @@ async def preview_validation_report(
331
328
  HTTPException: 404 if validation not found.
332
329
  HTTPException: 400 if format or locale is not supported.
333
330
  """
334
- # Get validation
335
- validation = await service.get_validation(validation_id)
331
+ # Get validation with source eagerly loaded (required for report generation)
332
+ validation = await service.get_validation(validation_id, with_source=True)
336
333
  if validation is None:
337
334
  raise HTTPException(status_code=404, detail="Validation not found")
338
335
 
@@ -393,7 +390,7 @@ async def list_report_history(
393
390
  source_id: Filter by source ID.
394
391
  validation_id: Filter by validation ID.
395
392
  reporter_id: Filter by reporter ID.
396
- format: Filter by format (html, pdf, csv, json, markdown, junit, excel).
393
+ format: Filter by format (html, csv, json).
397
394
  status: Filter by status (pending, generating, completed, failed, expired).
398
395
  include_expired: Include expired reports (default: false).
399
396
  search: Search by report name.
@@ -602,8 +599,6 @@ async def delete_report_record(
602
599
  "text/html": {},
603
600
  "text/csv": {},
604
601
  "application/json": {},
605
- "text/markdown": {},
606
- "application/pdf": {},
607
602
  },
608
603
  },
609
604
  404: {"description": "Report not found or file missing"},
@@ -645,12 +640,8 @@ async def download_saved_report(
645
640
  # Build filename
646
641
  ext_map = {
647
642
  "html": ".html",
648
- "pdf": ".pdf",
649
643
  "csv": ".csv",
650
644
  "json": ".json",
651
- "markdown": ".md",
652
- "junit": ".xml",
653
- "excel": ".xlsx",
654
645
  }
655
646
  fmt = report.format.value if hasattr(report.format, "value") else report.format
656
647
  ext = ext_map.get(fmt, ".html")
@@ -9,6 +9,7 @@ from . import (
9
9
  # Phase 1-4
10
10
  alerts,
11
11
  drift,
12
+ drift_monitor,
12
13
  health,
13
14
  history,
14
15
  maintenance,
@@ -16,6 +17,7 @@ from . import (
16
17
  notifications,
17
18
  notifications_advanced,
18
19
  profile,
20
+ quality_reporter,
19
21
  reports,
20
22
  rules,
21
23
  scan,
@@ -34,6 +36,7 @@ from . import (
34
36
  # Schema Evolution & Rule Suggestions
35
37
  rule_suggestions,
36
38
  schema_evolution,
39
+ schema_watcher,
37
40
  # Phase 9: Plugin System
38
41
  plugins,
39
42
  # Phase 10: ML & Lineage
@@ -42,6 +45,12 @@ from . import (
42
45
  model_monitoring,
43
46
  # Cross-Feature Integration
44
47
  cross_alerts,
48
+ # Storage Tiering (truthound 1.2.10+)
49
+ tiering,
50
+ # Enterprise Sampling (truthound 1.2.10+)
51
+ enterprise_sampling,
52
+ # Observability (truthound store observability)
53
+ observability,
45
54
  )
46
55
 
47
56
  api_router = APIRouter()
@@ -102,6 +111,12 @@ api_router.include_router(
102
111
  tags=["drift"],
103
112
  )
104
113
 
114
+ # Drift monitoring endpoints
115
+ api_router.include_router(
116
+ drift_monitor.router,
117
+ tags=["drift-monitor"],
118
+ )
119
+
105
120
  # PII scan endpoints
106
121
  api_router.include_router(
107
122
  scan.router,
@@ -279,3 +294,54 @@ api_router.include_router(
279
294
  plugins.router,
280
295
  tags=["plugins"],
281
296
  )
297
+
298
+ # =============================================================================
299
+ # Storage Tiering (truthound 1.2.10+)
300
+ # =============================================================================
301
+
302
+ # Storage tiering endpoints (tiers, policies, configs, migrations)
303
+ api_router.include_router(
304
+ tiering.router,
305
+ tags=["tiering"],
306
+ )
307
+
308
+ # =============================================================================
309
+ # Quality Reporter (truthound 1.2.10+)
310
+ # =============================================================================
311
+
312
+ # Quality scoring and reporting endpoints
313
+ api_router.include_router(
314
+ quality_reporter.router,
315
+ tags=["quality-reporter"],
316
+ )
317
+
318
+ # =============================================================================
319
+ # Schema Watcher (truthound 1.2.10+)
320
+ # =============================================================================
321
+
322
+ # Schema watcher endpoints for continuous schema monitoring
323
+ api_router.include_router(
324
+ schema_watcher.router,
325
+ tags=["schema-watcher"],
326
+ )
327
+
328
+ # =============================================================================
329
+ # Enterprise Sampling (truthound 1.2.10+)
330
+ # =============================================================================
331
+
332
+ # Enterprise sampling endpoints (block, multi-stage, column-aware, progressive)
333
+ api_router.include_router(
334
+ enterprise_sampling.router,
335
+ tags=["enterprise-sampling"],
336
+ )
337
+
338
+ # =============================================================================
339
+ # Observability (truthound store observability)
340
+ # =============================================================================
341
+
342
+ # Observability endpoints (audit, metrics, tracing)
343
+ api_router.include_router(
344
+ observability.router,
345
+ prefix="/observability",
346
+ tags=["observability"],
347
+ )
@@ -72,7 +72,7 @@ async def suggest_rules(
72
72
  Rule suggestion response with single-column and cross-column suggestions.
73
73
  """
74
74
  # Verify source exists
75
- source = await source_service.get(source_id)
75
+ source = await source_service.get_by_id(source_id)
76
76
  if not source:
77
77
  raise HTTPException(
78
78
  status_code=status.HTTP_404_NOT_FOUND,
@@ -100,7 +100,7 @@ async def suggest_rules(
100
100
  )
101
101
 
102
102
  # Get schema (optional)
103
- schema = await schema_service.get_active(source_id)
103
+ schema = await schema_service.get_schema(source_id)
104
104
 
105
105
  # Generate suggestions with advanced options including cross-column
106
106
  result = await generator_service.generate_suggestions(
@@ -144,7 +144,7 @@ async def apply_rule_suggestions(
144
144
  Apply rules response.
145
145
  """
146
146
  # Verify source exists
147
- source = await source_service.get(source_id)
147
+ source = await source_service.get_by_id(source_id)
148
148
  if not source:
149
149
  raise HTTPException(
150
150
  status_code=status.HTTP_404_NOT_FOUND,
@@ -200,7 +200,7 @@ async def export_rules(
200
200
  Export response with content.
201
201
  """
202
202
  # Verify source exists
203
- source = await source_service.get(source_id)
203
+ source = await source_service.get_by_id(source_id)
204
204
  if not source:
205
205
  raise HTTPException(
206
206
  status_code=status.HTTP_404_NOT_FOUND,
@@ -250,7 +250,7 @@ async def download_exported_rules(
250
250
  Plain text response with file content.
251
251
  """
252
252
  # Verify source exists
253
- source = await source_service.get(source_id)
253
+ source = await source_service.get_by_id(source_id)
254
254
  if not source:
255
255
  raise HTTPException(
256
256
  status_code=status.HTTP_404_NOT_FOUND,
@@ -27,24 +27,27 @@ router = APIRouter()
27
27
  "/sources/{source_id}/scan",
28
28
  response_model=PIIScanResponse,
29
29
  summary="Run PII scan",
30
- description="Scan data source for personally identifiable information (PII)",
30
+ description="""
31
+ Scan data source for personally identifiable information (PII).
32
+
33
+ Note: truthound's th.scan() does not support configuration parameters.
34
+ The scan runs on all columns with default settings.
35
+ """,
31
36
  )
32
37
  async def run_pii_scan(
33
38
  service: PIIScanServiceDep,
34
39
  source_id: Annotated[str, Path(description="Source ID to scan")],
35
- request: PIIScanRequest,
40
+ request: PIIScanRequest | None = None,
36
41
  ) -> PIIScanResponse:
37
42
  """Run PII scan on a data source.
38
43
 
39
- Supports all th.scan() parameters for maximum flexibility:
40
- - columns: Specific columns to scan
41
- - regulations: Privacy regulations to check (gdpr, ccpa, lgpd)
42
- - min_confidence: Confidence threshold for PII detection
44
+ Note: truthound's th.scan() does not support configuration parameters.
45
+ The scan runs on all columns with default settings.
43
46
 
44
47
  Args:
45
48
  service: Injected PII scan service.
46
49
  source_id: Source to scan.
47
- request: Scan options.
50
+ request: Optional request body (not used, kept for API compatibility).
48
51
 
49
52
  Returns:
50
53
  PII scan result with findings and violations.
@@ -53,12 +56,7 @@ async def run_pii_scan(
53
56
  HTTPException: 404 if source not found.
54
57
  """
55
58
  try:
56
- scan = await service.run_scan(
57
- source_id,
58
- columns=request.columns,
59
- regulations=request.regulations,
60
- min_confidence=request.min_confidence,
61
- )
59
+ scan = await service.run_scan(source_id)
62
60
  return PIIScanResponse.from_model(scan)
63
61
  except ValueError as e:
64
62
  raise HTTPException(status_code=404, detail=str(e))
@@ -134,15 +132,15 @@ async def list_source_pii_scans(
134
132
 
135
133
  @router.get(
136
134
  "/sources/{source_id}/scans/latest",
137
- response_model=PIIScanResponse,
135
+ response_model=PIIScanResponse | None,
138
136
  summary="Get latest PII scan",
139
- description="Get the most recent PII scan for a source",
137
+ description="Get the most recent PII scan for a source, or null if no scans exist",
140
138
  )
141
139
  async def get_latest_pii_scan(
142
140
  service: PIIScanServiceDep,
143
141
  source_service: SourceServiceDep,
144
142
  source_id: Annotated[str, Path(description="Source ID")],
145
- ) -> PIIScanResponse:
143
+ ) -> PIIScanResponse | None:
146
144
  """Get the most recent PII scan for a source.
147
145
 
148
146
  Args:
@@ -151,10 +149,10 @@ async def get_latest_pii_scan(
151
149
  source_id: Source to get latest scan for.
152
150
 
153
151
  Returns:
154
- Latest PII scan result.
152
+ Latest PII scan result, or null if no scans exist.
155
153
 
156
154
  Raises:
157
- HTTPException: 404 if source or scan not found.
155
+ HTTPException: 404 if source not found.
158
156
  """
159
157
  # Verify source exists
160
158
  source = await source_service.get_by_id(source_id)
@@ -163,6 +161,6 @@ async def get_latest_pii_scan(
163
161
 
164
162
  scan = await service.get_latest_for_source(source_id)
165
163
  if scan is None:
166
- raise HTTPException(status_code=404, detail="No PII scans found for source")
164
+ return None
167
165
 
168
166
  return PIIScanResponse.from_model(scan)
@@ -1,6 +1,11 @@
1
1
  """Schedule management API endpoints.
2
2
 
3
3
  Provides CRUD endpoints for validation schedules.
4
+
5
+ API Design: Direct Response Style
6
+ - Single resources return the resource directly
7
+ - List endpoints return PaginatedResponse with data, total, offset, limit
8
+ - Errors are handled via HTTPException
4
9
  """
5
10
 
6
11
  from __future__ import annotations
@@ -11,11 +16,14 @@ from fastapi import APIRouter, Depends, HTTPException, Query
11
16
 
12
17
  from truthound_dashboard.core import ScheduleService, ValidationService
13
18
  from truthound_dashboard.schemas import (
19
+ MessageResponse,
14
20
  ScheduleActionResponse,
15
21
  ScheduleCreate,
16
22
  ScheduleListResponse,
23
+ ScheduleResponse,
17
24
  ScheduleUpdate,
18
25
  )
26
+ from truthound_dashboard.schemas.schedule import ScheduleListItem
19
27
 
20
28
  from .deps import SessionDep
21
29
 
@@ -30,25 +38,49 @@ async def get_schedule_service(session: SessionDep) -> ScheduleService:
30
38
  ScheduleServiceDep = Annotated[ScheduleService, Depends(get_schedule_service)]
31
39
 
32
40
 
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": (
41
+ def _schedule_to_response(schedule) -> ScheduleResponse:
42
+ """Convert schedule model to response schema."""
43
+ return ScheduleResponse(
44
+ id=schedule.id,
45
+ name=schedule.name,
46
+ source_id=schedule.source_id,
47
+ cron_expression=schedule.cron_expression,
48
+ trigger_type=getattr(schedule, "trigger_type", None) or "cron",
49
+ trigger_config=getattr(schedule, "trigger_config", None),
50
+ is_active=schedule.is_active,
51
+ notify_on_failure=schedule.notify_on_failure,
52
+ last_run_at=(
43
53
  schedule.last_run_at.isoformat() if schedule.last_run_at else None
44
54
  ),
45
- "next_run_at": (
55
+ next_run_at=(
46
56
  schedule.next_run_at.isoformat() if schedule.next_run_at else None
47
57
  ),
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
- }
58
+ config=schedule.config,
59
+ created_at=schedule.created_at,
60
+ updated_at=schedule.updated_at,
61
+ )
62
+
63
+
64
+ def _schedule_to_list_item(schedule) -> ScheduleListItem:
65
+ """Convert schedule model to list item schema."""
66
+ return ScheduleListItem(
67
+ id=schedule.id,
68
+ name=schedule.name,
69
+ source_id=schedule.source_id,
70
+ cron_expression=schedule.cron_expression,
71
+ trigger_type=getattr(schedule, "trigger_type", None) or "cron",
72
+ trigger_config=getattr(schedule, "trigger_config", None),
73
+ is_active=schedule.is_active,
74
+ notify_on_failure=schedule.notify_on_failure,
75
+ last_run_at=(
76
+ schedule.last_run_at.isoformat() if schedule.last_run_at else None
77
+ ),
78
+ next_run_at=(
79
+ schedule.next_run_at.isoformat() if schedule.next_run_at else None
80
+ ),
81
+ created_at=schedule.created_at,
82
+ updated_at=schedule.updated_at,
83
+ )
52
84
 
53
85
 
54
86
  @router.get(
@@ -61,6 +93,7 @@ async def list_schedules(
61
93
  service: ScheduleServiceDep,
62
94
  source_id: str | None = Query(None, description="Filter by source ID"),
63
95
  active_only: bool = Query(False, description="Only return active schedules"),
96
+ offset: int = Query(0, ge=0, description="Offset for pagination"),
64
97
  limit: int = Query(100, ge=1, le=500, description="Maximum results"),
65
98
  ) -> ScheduleListResponse:
66
99
  """List validation schedules.
@@ -69,10 +102,11 @@ async def list_schedules(
69
102
  service: Schedule service.
70
103
  source_id: Optional source ID filter.
71
104
  active_only: Only return active schedules.
105
+ offset: Offset for pagination.
72
106
  limit: Maximum results.
73
107
 
74
108
  Returns:
75
- List of schedules.
109
+ Paginated list of schedules.
76
110
  """
77
111
  schedules = await service.list_schedules(
78
112
  source_id=source_id,
@@ -81,15 +115,16 @@ async def list_schedules(
81
115
  )
82
116
 
83
117
  return ScheduleListResponse(
84
- success=True,
85
- data=[_schedule_to_response(s) for s in schedules],
118
+ data=[_schedule_to_list_item(s) for s in schedules],
86
119
  total=len(schedules),
120
+ offset=offset,
121
+ limit=limit,
87
122
  )
88
123
 
89
124
 
90
125
  @router.post(
91
126
  "/schedules",
92
- response_model=dict,
127
+ response_model=ScheduleResponse,
93
128
  status_code=201,
94
129
  summary="Create schedule",
95
130
  description="Create a new validation schedule.",
@@ -97,7 +132,7 @@ async def list_schedules(
97
132
  async def create_schedule(
98
133
  request: ScheduleCreate,
99
134
  service: ScheduleServiceDep,
100
- ) -> dict:
135
+ ) -> ScheduleResponse:
101
136
  """Create a new schedule.
102
137
 
103
138
  Args:
@@ -112,14 +147,12 @@ async def create_schedule(
112
147
  source_id=request.source_id,
113
148
  name=request.name,
114
149
  cron_expression=request.cron_expression,
150
+ trigger_type=request.trigger_type,
151
+ trigger_config=request.trigger_config,
115
152
  notify_on_failure=request.notify_on_failure,
116
153
  config=request.config,
117
154
  )
118
-
119
- return {
120
- "success": True,
121
- "data": _schedule_to_response(schedule),
122
- }
155
+ return _schedule_to_response(schedule)
123
156
  except ValueError as e:
124
157
  raise HTTPException(status_code=400, detail=str(e))
125
158
  except Exception as e:
@@ -128,14 +161,14 @@ async def create_schedule(
128
161
 
129
162
  @router.get(
130
163
  "/schedules/{schedule_id}",
131
- response_model=dict,
164
+ response_model=ScheduleResponse,
132
165
  summary="Get schedule",
133
166
  description="Get a specific schedule by ID.",
134
167
  )
135
168
  async def get_schedule(
136
169
  schedule_id: str,
137
170
  service: ScheduleServiceDep,
138
- ) -> dict:
171
+ ) -> ScheduleResponse:
139
172
  """Get a schedule by ID.
140
173
 
141
174
  Args:
@@ -149,15 +182,12 @@ async def get_schedule(
149
182
  if schedule is None:
150
183
  raise HTTPException(status_code=404, detail="Schedule not found")
151
184
 
152
- return {
153
- "success": True,
154
- "data": _schedule_to_response(schedule),
155
- }
185
+ return _schedule_to_response(schedule)
156
186
 
157
187
 
158
188
  @router.put(
159
189
  "/schedules/{schedule_id}",
160
- response_model=dict,
190
+ response_model=ScheduleResponse,
161
191
  summary="Update schedule",
162
192
  description="Update an existing schedule.",
163
193
  )
@@ -165,7 +195,7 @@ async def update_schedule(
165
195
  schedule_id: str,
166
196
  request: ScheduleUpdate,
167
197
  service: ScheduleServiceDep,
168
- ) -> dict:
198
+ ) -> ScheduleResponse:
169
199
  """Update a schedule.
170
200
 
171
201
  Args:
@@ -188,24 +218,21 @@ async def update_schedule(
188
218
  if schedule is None:
189
219
  raise HTTPException(status_code=404, detail="Schedule not found")
190
220
 
191
- return {
192
- "success": True,
193
- "data": _schedule_to_response(schedule),
194
- }
221
+ return _schedule_to_response(schedule)
195
222
  except ValueError as e:
196
223
  raise HTTPException(status_code=400, detail=str(e))
197
224
 
198
225
 
199
226
  @router.delete(
200
227
  "/schedules/{schedule_id}",
201
- response_model=dict,
228
+ response_model=MessageResponse,
202
229
  summary="Delete schedule",
203
230
  description="Delete a schedule.",
204
231
  )
205
232
  async def delete_schedule(
206
233
  schedule_id: str,
207
234
  service: ScheduleServiceDep,
208
- ) -> dict:
235
+ ) -> MessageResponse:
209
236
  """Delete a schedule.
210
237
 
211
238
  Args:
@@ -219,7 +246,7 @@ async def delete_schedule(
219
246
  if not deleted:
220
247
  raise HTTPException(status_code=404, detail="Schedule not found")
221
248
 
222
- return {"success": True, "message": "Schedule deleted"}
249
+ return MessageResponse(message="Schedule deleted")
223
250
 
224
251
 
225
252
  @router.post(
@@ -246,7 +273,6 @@ async def pause_schedule(
246
273
  raise HTTPException(status_code=404, detail="Schedule not found")
247
274
 
248
275
  return ScheduleActionResponse(
249
- success=True,
250
276
  message="Schedule paused",
251
277
  schedule=_schedule_to_response(schedule),
252
278
  )
@@ -276,15 +302,25 @@ async def resume_schedule(
276
302
  raise HTTPException(status_code=404, detail="Schedule not found")
277
303
 
278
304
  return ScheduleActionResponse(
279
- success=True,
280
305
  message="Schedule resumed",
281
306
  schedule=_schedule_to_response(schedule),
282
307
  )
283
308
 
284
309
 
310
+ from pydantic import BaseModel
311
+
312
+
313
+ class ScheduleRunResponse(BaseModel):
314
+ """Response for schedule run action."""
315
+
316
+ message: str
317
+ validation_id: str
318
+ passed: bool | None = None
319
+
320
+
285
321
  @router.post(
286
322
  "/schedules/{schedule_id}/run",
287
- response_model=dict,
323
+ response_model=ScheduleRunResponse,
288
324
  summary="Run schedule now",
289
325
  description="Trigger immediate execution of a scheduled validation.",
290
326
  )
@@ -292,7 +328,7 @@ async def run_schedule_now(
292
328
  schedule_id: str,
293
329
  service: ScheduleServiceDep,
294
330
  session: SessionDep,
295
- ) -> dict:
331
+ ) -> ScheduleRunResponse:
296
332
  """Run a scheduled validation immediately.
297
333
 
298
334
  Args:
@@ -319,11 +355,10 @@ async def run_schedule_now(
319
355
  auto_schema=config.get("auto_schema", False),
320
356
  )
321
357
 
322
- return {
323
- "success": True,
324
- "message": "Validation triggered",
325
- "validation_id": validation.id,
326
- "passed": validation.passed,
327
- }
358
+ return ScheduleRunResponse(
359
+ message="Validation triggered",
360
+ validation_id=validation.id,
361
+ passed=validation.passed,
362
+ )
328
363
  except Exception as e:
329
364
  raise HTTPException(status_code=500, detail=str(e))
@@ -51,7 +51,7 @@ async def list_schema_versions(
51
51
  List of schema versions.
52
52
  """
53
53
  # Verify source exists
54
- source = await source_service.get(source_id)
54
+ source = await source_service.get_by_id(source_id)
55
55
  if not source:
56
56
  raise HTTPException(
57
57
  status_code=status.HTTP_404_NOT_FOUND,
@@ -93,7 +93,7 @@ async def get_schema_version(
93
93
  Schema version details.
94
94
  """
95
95
  # Verify source exists
96
- source = await source_service.get(source_id)
96
+ source = await source_service.get_by_id(source_id)
97
97
  if not source:
98
98
  raise HTTPException(
99
99
  status_code=status.HTTP_404_NOT_FOUND,
@@ -136,7 +136,7 @@ async def list_schema_changes(
136
136
  List of schema changes.
137
137
  """
138
138
  # Verify source exists
139
- source = await source_service.get(source_id)
139
+ source = await source_service.get_by_id(source_id)
140
140
  if not source:
141
141
  raise HTTPException(
142
142
  status_code=status.HTTP_404_NOT_FOUND,
@@ -178,7 +178,7 @@ async def detect_schema_changes(
178
178
  Schema evolution detection result.
179
179
  """
180
180
  # Verify source exists
181
- source = await source_service.get(source_id)
181
+ source = await source_service.get_by_id(source_id)
182
182
  if not source:
183
183
  raise HTTPException(
184
184
  status_code=status.HTTP_404_NOT_FOUND,
@@ -186,7 +186,7 @@ async def detect_schema_changes(
186
186
  )
187
187
 
188
188
  # Get current schema
189
- schema = await schema_service.get_active(source_id)
189
+ schema = await schema_service.get_schema(source_id)
190
190
  if not schema:
191
191
  raise HTTPException(
192
192
  status_code=status.HTTP_404_NOT_FOUND,
@@ -221,7 +221,7 @@ async def get_evolution_summary(
221
221
  Evolution summary.
222
222
  """
223
223
  # Verify source exists
224
- source = await source_service.get(source_id)
224
+ source = await source_service.get_by_id(source_id)
225
225
  if not source:
226
226
  raise HTTPException(
227
227
  status_code=status.HTTP_404_NOT_FOUND,