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
@@ -1,6 +1,12 @@
1
1
  """Drift monitoring API endpoints.
2
2
 
3
3
  This module provides REST API endpoints for drift monitoring management.
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
9
+ - Success is indicated by HTTP status codes (200, 201, 204)
4
10
  """
5
11
 
6
12
  from __future__ import annotations
@@ -8,6 +14,7 @@ from __future__ import annotations
8
14
  from typing import Annotated
9
15
 
10
16
  from fastapi import APIRouter, Depends, HTTPException, Query
17
+ from pydantic import BaseModel, Field
11
18
 
12
19
  from truthound_dashboard.core.drift_monitor import DriftMonitorService
13
20
  from truthound_dashboard.schemas.drift_monitor import (
@@ -15,16 +22,20 @@ from truthound_dashboard.schemas.drift_monitor import (
15
22
  DriftMonitorUpdate,
16
23
  DriftMonitorResponse,
17
24
  DriftMonitorListResponse,
25
+ DriftMonitorSummary,
18
26
  DriftAlertResponse,
19
27
  DriftAlertListResponse,
20
28
  DriftAlertUpdate,
21
- DriftMonitorSummary,
22
29
  DriftTrendResponse,
23
30
  DriftPreviewRequest,
24
- DriftPreviewResponse,
25
- SamplingConfig,
26
- SampledComparisonRequest,
31
+ DriftPreviewData,
32
+ RootCauseAnalysis,
33
+ SampleSizeEstimateResponse,
34
+ JobProgressResponse,
35
+ SampledComparisonResult,
36
+ MonitorRunResult,
27
37
  )
38
+ from truthound_dashboard.schemas.base import MessageResponse
28
39
  from .deps import SessionDep
29
40
 
30
41
  router = APIRouter()
@@ -44,7 +55,7 @@ DriftMonitorServiceDep = Annotated[DriftMonitorService, Depends(get_drift_monito
44
55
 
45
56
  @router.post(
46
57
  "/drift/monitors",
47
- response_model=dict,
58
+ response_model=DriftMonitorResponse,
48
59
  status_code=201,
49
60
  summary="Create drift monitor",
50
61
  description="Create a new drift monitor for automatic drift detection.",
@@ -52,7 +63,7 @@ DriftMonitorServiceDep = Annotated[DriftMonitorService, Depends(get_drift_monito
52
63
  async def create_monitor(
53
64
  request: DriftMonitorCreate,
54
65
  service: DriftMonitorServiceDep,
55
- ) -> dict:
66
+ ) -> DriftMonitorResponse:
56
67
  """Create a new drift monitor."""
57
68
  monitor = await service.create_monitor(
58
69
  name=request.name,
@@ -68,10 +79,7 @@ async def create_monitor(
68
79
  notification_channel_ids=request.notification_channel_ids,
69
80
  )
70
81
 
71
- return {
72
- "success": True,
73
- "data": _monitor_to_dict(monitor),
74
- }
82
+ return DriftMonitorResponse(**_monitor_to_dict(monitor))
75
83
 
76
84
 
77
85
  @router.get(
@@ -94,7 +102,6 @@ async def list_monitors(
94
102
  )
95
103
 
96
104
  return DriftMonitorListResponse(
97
- success=True,
98
105
  data=[DriftMonitorResponse(**_monitor_to_dict(m)) for m in monitors],
99
106
  total=total,
100
107
  offset=offset,
@@ -104,28 +111,28 @@ async def list_monitors(
104
111
 
105
112
  @router.get(
106
113
  "/drift/monitors/summary",
107
- response_model=dict,
114
+ response_model=DriftMonitorSummary,
108
115
  summary="Get monitors summary",
109
116
  description="Get summary statistics for all drift monitors.",
110
117
  )
111
118
  async def get_monitors_summary(
112
119
  service: DriftMonitorServiceDep,
113
- ) -> dict:
120
+ ) -> DriftMonitorSummary:
114
121
  """Get summary of all drift monitors."""
115
122
  summary = await service.get_summary()
116
- return {"success": True, "data": summary}
123
+ return DriftMonitorSummary(**summary)
117
124
 
118
125
 
119
126
  @router.post(
120
127
  "/drift/preview",
121
- response_model=DriftPreviewResponse,
128
+ response_model=DriftPreviewData,
122
129
  summary="Preview drift comparison",
123
130
  description="Preview drift comparison results without creating a monitor or saving results.",
124
131
  )
125
132
  async def preview_drift(
126
133
  request: DriftPreviewRequest,
127
134
  service: DriftMonitorServiceDep,
128
- ) -> DriftPreviewResponse:
135
+ ) -> DriftPreviewData:
129
136
  """Preview drift comparison without persisting results.
130
137
 
131
138
  This endpoint allows users to see drift comparison results before
@@ -139,10 +146,7 @@ async def preview_drift(
139
146
  method=request.method,
140
147
  threshold=request.threshold,
141
148
  )
142
- return DriftPreviewResponse(
143
- success=True,
144
- data=preview_result,
145
- )
149
+ return DriftPreviewData(**preview_result)
146
150
  except ValueError as e:
147
151
  raise HTTPException(status_code=404, detail=str(e))
148
152
  except Exception as e:
@@ -151,25 +155,25 @@ async def preview_drift(
151
155
 
152
156
  @router.get(
153
157
  "/drift/monitors/{monitor_id}",
154
- response_model=dict,
158
+ response_model=DriftMonitorResponse,
155
159
  summary="Get drift monitor",
156
160
  description="Get a drift monitor by ID.",
157
161
  )
158
162
  async def get_monitor(
159
163
  monitor_id: str,
160
164
  service: DriftMonitorServiceDep,
161
- ) -> dict:
165
+ ) -> DriftMonitorResponse:
162
166
  """Get a drift monitor by ID."""
163
167
  monitor = await service.get_monitor(monitor_id)
164
168
  if not monitor:
165
169
  raise HTTPException(status_code=404, detail="Monitor not found")
166
170
 
167
- return {"success": True, "data": _monitor_to_dict(monitor)}
171
+ return DriftMonitorResponse(**_monitor_to_dict(monitor))
168
172
 
169
173
 
170
174
  @router.put(
171
175
  "/drift/monitors/{monitor_id}",
172
- response_model=dict,
176
+ response_model=DriftMonitorResponse,
173
177
  summary="Update drift monitor",
174
178
  description="Update a drift monitor configuration.",
175
179
  )
@@ -177,7 +181,7 @@ async def update_monitor(
177
181
  monitor_id: str,
178
182
  request: DriftMonitorUpdate,
179
183
  service: DriftMonitorServiceDep,
180
- ) -> dict:
184
+ ) -> DriftMonitorResponse:
181
185
  """Update a drift monitor."""
182
186
  update_data = request.model_dump(exclude_unset=True)
183
187
  monitor = await service.update_monitor(monitor_id, **update_data)
@@ -185,56 +189,63 @@ async def update_monitor(
185
189
  if not monitor:
186
190
  raise HTTPException(status_code=404, detail="Monitor not found")
187
191
 
188
- return {"success": True, "data": _monitor_to_dict(monitor)}
192
+ return DriftMonitorResponse(**_monitor_to_dict(monitor))
189
193
 
190
194
 
191
195
  @router.delete(
192
196
  "/drift/monitors/{monitor_id}",
193
- response_model=dict,
197
+ response_model=MessageResponse,
194
198
  summary="Delete drift monitor",
195
199
  description="Delete a drift monitor.",
196
200
  )
197
201
  async def delete_monitor(
198
202
  monitor_id: str,
199
203
  service: DriftMonitorServiceDep,
200
- ) -> dict:
204
+ ) -> MessageResponse:
201
205
  """Delete a drift monitor."""
202
206
  deleted = await service.delete_monitor(monitor_id)
203
207
  if not deleted:
204
208
  raise HTTPException(status_code=404, detail="Monitor not found")
205
209
 
206
- return {"success": True, "message": "Monitor deleted"}
210
+ return MessageResponse(message="Monitor deleted")
207
211
 
208
212
 
209
213
  @router.post(
210
214
  "/drift/monitors/{monitor_id}/run",
211
- response_model=dict,
215
+ response_model=MonitorRunResult,
212
216
  summary="Run drift monitor",
213
217
  description="Manually trigger a drift monitoring run.",
214
218
  )
215
219
  async def run_monitor(
216
220
  monitor_id: str,
217
221
  service: DriftMonitorServiceDep,
218
- ) -> dict:
222
+ ) -> MonitorRunResult:
219
223
  """Manually run a drift monitor."""
220
- comparison = await service.run_monitor(monitor_id)
224
+ # force=True allows running paused monitors manually
225
+ comparison = await service.run_monitor(monitor_id, force=True)
221
226
  if not comparison:
222
227
  raise HTTPException(status_code=400, detail="Monitor run failed")
223
228
 
224
- return {
225
- "success": True,
226
- "data": {
227
- "comparison_id": comparison.id,
228
- "has_drift": comparison.has_drift,
229
- "drift_percentage": comparison.drift_percentage,
230
- "drifted_columns": comparison.drifted_columns,
231
- },
232
- }
229
+ # Extract drifted column names from result_json
230
+ drifted_column_names: list[str] = []
231
+ if comparison.result_json and "columns" in comparison.result_json:
232
+ drifted_column_names = [
233
+ col["column"]
234
+ for col in comparison.result_json["columns"]
235
+ if col.get("drifted", False)
236
+ ]
237
+
238
+ return MonitorRunResult(
239
+ comparison_id=comparison.id,
240
+ has_drift=comparison.has_drift,
241
+ drift_percentage=comparison.drift_percentage or 0.0,
242
+ drifted_columns=drifted_column_names,
243
+ )
233
244
 
234
245
 
235
246
  @router.get(
236
247
  "/drift/monitors/{monitor_id}/trend",
237
- response_model=dict,
248
+ response_model=DriftTrendResponse,
238
249
  summary="Get drift trend",
239
250
  description="Get drift trend data for a monitor over time.",
240
251
  )
@@ -242,18 +253,153 @@ async def get_monitor_trend(
242
253
  monitor_id: str,
243
254
  service: DriftMonitorServiceDep,
244
255
  days: int = Query(30, ge=1, le=365),
245
- ) -> dict:
256
+ ) -> DriftTrendResponse:
246
257
  """Get drift trend for a monitor."""
247
258
  trend = await service.get_trend(monitor_id, days=days)
248
259
  if not trend:
249
260
  raise HTTPException(status_code=404, detail="Monitor not found")
250
261
 
251
- return {"success": True, "data": trend}
262
+ return DriftTrendResponse(**trend)
263
+
264
+
265
+ # Run History Endpoints
266
+
267
+ from truthound_dashboard.schemas.drift_monitor import (
268
+ DriftMonitorRunResponse,
269
+ DriftMonitorRunListResponse,
270
+ DriftMonitorRunStatistics,
271
+ )
272
+
273
+
274
+ @router.get(
275
+ "/drift/monitors/{monitor_id}/runs",
276
+ response_model=DriftMonitorRunListResponse,
277
+ summary="List monitor runs",
278
+ description="Get execution history for a drift monitor.",
279
+ )
280
+ async def list_monitor_runs(
281
+ monitor_id: str,
282
+ service: DriftMonitorServiceDep,
283
+ status: str | None = Query(None, description="Filter by status"),
284
+ limit: int = Query(50, ge=1, le=100),
285
+ offset: int = Query(0, ge=0),
286
+ ) -> DriftMonitorRunListResponse:
287
+ """List runs for a drift monitor."""
288
+ runs, total = await service.list_runs(
289
+ monitor_id=monitor_id,
290
+ status=status,
291
+ limit=limit,
292
+ offset=offset,
293
+ )
294
+
295
+ return DriftMonitorRunListResponse(
296
+ data=[
297
+ DriftMonitorRunResponse(
298
+ id=run.id,
299
+ monitor_id=run.monitor_id,
300
+ status=run.status,
301
+ has_drift=run.has_drift,
302
+ max_drift_score=run.max_drift_score,
303
+ total_columns=run.total_columns,
304
+ drifted_columns=run.drifted_columns,
305
+ column_results=run.column_results,
306
+ root_cause_analysis=run.root_cause_analysis,
307
+ duration_ms=run.duration_ms,
308
+ error_message=run.error_message,
309
+ created_at=run.created_at,
310
+ completed_at=run.completed_at,
311
+ )
312
+ for run in runs
313
+ ],
314
+ total=total,
315
+ offset=offset,
316
+ limit=limit,
317
+ )
318
+
319
+
320
+ @router.get(
321
+ "/drift/monitors/{monitor_id}/runs/latest",
322
+ response_model=DriftMonitorRunResponse | None,
323
+ summary="Get latest run",
324
+ description="Get the most recent run for a drift monitor.",
325
+ )
326
+ async def get_latest_run(
327
+ monitor_id: str,
328
+ service: DriftMonitorServiceDep,
329
+ ) -> DriftMonitorRunResponse | None:
330
+ """Get the latest run for a monitor."""
331
+ run = await service.get_latest_run(monitor_id)
332
+ if not run:
333
+ return None
334
+
335
+ return DriftMonitorRunResponse(
336
+ id=run.id,
337
+ monitor_id=run.monitor_id,
338
+ status=run.status,
339
+ has_drift=run.has_drift,
340
+ max_drift_score=run.max_drift_score,
341
+ total_columns=run.total_columns,
342
+ drifted_columns=run.drifted_columns,
343
+ column_results=run.column_results,
344
+ root_cause_analysis=run.root_cause_analysis,
345
+ duration_ms=run.duration_ms,
346
+ error_message=run.error_message,
347
+ created_at=run.created_at,
348
+ completed_at=run.completed_at,
349
+ )
350
+
351
+
352
+ @router.get(
353
+ "/drift/monitors/{monitor_id}/runs/statistics",
354
+ response_model=DriftMonitorRunStatistics,
355
+ summary="Get run statistics",
356
+ description="Get aggregated statistics for drift monitor runs.",
357
+ )
358
+ async def get_run_statistics(
359
+ monitor_id: str,
360
+ service: DriftMonitorServiceDep,
361
+ ) -> DriftMonitorRunStatistics:
362
+ """Get run statistics for a monitor."""
363
+ stats = await service.get_run_statistics(monitor_id)
364
+ return DriftMonitorRunStatistics(**stats)
365
+
366
+
367
+ @router.get(
368
+ "/drift/monitors/{monitor_id}/runs/{run_id}",
369
+ response_model=DriftMonitorRunResponse,
370
+ summary="Get run details",
371
+ description="Get detailed information about a specific run.",
372
+ )
373
+ async def get_run(
374
+ monitor_id: str,
375
+ run_id: str,
376
+ service: DriftMonitorServiceDep,
377
+ ) -> DriftMonitorRunResponse:
378
+ """Get a specific run."""
379
+ run = await service.get_run(run_id)
380
+ if not run or run.monitor_id != monitor_id:
381
+ raise HTTPException(status_code=404, detail="Run not found")
382
+
383
+ return DriftMonitorRunResponse(
384
+ id=run.id,
385
+ monitor_id=run.monitor_id,
386
+ status=run.status,
387
+ has_drift=run.has_drift,
388
+ max_drift_score=run.max_drift_score,
389
+ total_columns=run.total_columns,
390
+ drifted_columns=run.drifted_columns,
391
+ column_results=run.column_results,
392
+ root_cause_analysis=run.root_cause_analysis,
393
+ duration_ms=run.duration_ms,
394
+ error_message=run.error_message,
395
+ created_at=run.created_at,
396
+ completed_at=run.completed_at,
397
+ )
252
398
 
253
399
 
254
400
  @router.get(
255
401
  "/drift/monitors/{monitor_id}/runs/{run_id}/root-cause",
256
- response_model=dict,
402
+ response_model=RootCauseAnalysis,
257
403
  summary="Analyze drift root cause",
258
404
  description="Analyze root causes of drift for a specific comparison run.",
259
405
  )
@@ -261,7 +407,7 @@ async def get_root_cause_analysis(
261
407
  monitor_id: str,
262
408
  run_id: str,
263
409
  service: DriftMonitorServiceDep,
264
- ) -> dict:
410
+ ) -> RootCauseAnalysis:
265
411
  """Get root cause analysis for a drift run.
266
412
 
267
413
  Analyzes a drift comparison to identify why drift is occurring,
@@ -272,19 +418,19 @@ async def get_root_cause_analysis(
272
418
  if not analysis:
273
419
  raise HTTPException(status_code=404, detail="Drift run not found")
274
420
 
275
- return {"success": True, "data": analysis}
421
+ return RootCauseAnalysis(**analysis)
276
422
 
277
423
 
278
424
  @router.get(
279
425
  "/drift/comparisons/{run_id}/root-cause",
280
- response_model=dict,
426
+ response_model=RootCauseAnalysis,
281
427
  summary="Analyze drift root cause (standalone)",
282
428
  description="Analyze root causes for a drift comparison without a monitor.",
283
429
  )
284
430
  async def get_comparison_root_cause_analysis(
285
431
  run_id: str,
286
432
  service: DriftMonitorServiceDep,
287
- ) -> dict:
433
+ ) -> RootCauseAnalysis:
288
434
  """Get root cause analysis for a standalone drift comparison.
289
435
 
290
436
  Similar to the monitor-based endpoint but for one-off comparisons.
@@ -293,7 +439,7 @@ async def get_comparison_root_cause_analysis(
293
439
  if not analysis:
294
440
  raise HTTPException(status_code=404, detail="Drift comparison not found")
295
441
 
296
- return {"success": True, "data": analysis}
442
+ return RootCauseAnalysis(**analysis)
297
443
 
298
444
 
299
445
  # Alert Endpoints
@@ -323,7 +469,6 @@ async def list_alerts(
323
469
  )
324
470
 
325
471
  return DriftAlertListResponse(
326
- success=True,
327
472
  data=[DriftAlertResponse(**_alert_to_dict(a)) for a in alerts],
328
473
  total=total,
329
474
  offset=offset,
@@ -333,25 +478,25 @@ async def list_alerts(
333
478
 
334
479
  @router.get(
335
480
  "/drift/alerts/{alert_id}",
336
- response_model=dict,
481
+ response_model=DriftAlertResponse,
337
482
  summary="Get drift alert",
338
483
  description="Get a drift alert by ID.",
339
484
  )
340
485
  async def get_alert(
341
486
  alert_id: str,
342
487
  service: DriftMonitorServiceDep,
343
- ) -> dict:
488
+ ) -> DriftAlertResponse:
344
489
  """Get a drift alert by ID."""
345
490
  alert = await service.get_alert(alert_id)
346
491
  if not alert:
347
492
  raise HTTPException(status_code=404, detail="Alert not found")
348
493
 
349
- return {"success": True, "data": _alert_to_dict(alert)}
494
+ return DriftAlertResponse(**_alert_to_dict(alert))
350
495
 
351
496
 
352
497
  @router.put(
353
498
  "/drift/alerts/{alert_id}",
354
- response_model=dict,
499
+ response_model=DriftAlertResponse,
355
500
  summary="Update drift alert",
356
501
  description="Update a drift alert status or notes.",
357
502
  )
@@ -359,7 +504,7 @@ async def update_alert(
359
504
  alert_id: str,
360
505
  request: DriftAlertUpdate,
361
506
  service: DriftMonitorServiceDep,
362
- ) -> dict:
507
+ ) -> DriftAlertResponse:
363
508
  """Update a drift alert."""
364
509
  alert = await service.update_alert(
365
510
  alert_id,
@@ -370,7 +515,7 @@ async def update_alert(
370
515
  if not alert:
371
516
  raise HTTPException(status_code=404, detail="Alert not found")
372
517
 
373
- return {"success": True, "data": _alert_to_dict(alert)}
518
+ return DriftAlertResponse(**_alert_to_dict(alert))
374
519
 
375
520
 
376
521
  # Large-Scale Dataset Optimization Endpoints
@@ -378,7 +523,7 @@ async def update_alert(
378
523
 
379
524
  @router.post(
380
525
  "/drift/monitors/{monitor_id}/run-sampled",
381
- response_model=dict,
526
+ response_model=SampledComparisonResult,
382
527
  summary="Run sampled drift comparison",
383
528
  description="Run drift comparison with sampling for large datasets (100M+ rows).",
384
529
  )
@@ -390,7 +535,7 @@ async def run_sampled_comparison(
390
535
  confidence_level: float = Query(0.95, ge=0.80, le=0.99, description="Target confidence level"),
391
536
  early_stop_threshold: float = Query(0.5, ge=0.1, le=1.0, description="Early stop threshold"),
392
537
  max_workers: int = Query(4, ge=1, le=16, description="Max parallel workers"),
393
- ) -> dict:
538
+ ) -> SampledComparisonResult:
394
539
  """Run a sampled drift comparison for large datasets.
395
540
 
396
541
  Optimized for 100M+ row datasets with:
@@ -408,7 +553,7 @@ async def run_sampled_comparison(
408
553
  early_stop_threshold=early_stop_threshold,
409
554
  max_workers=max_workers,
410
555
  )
411
- return {"success": True, "data": result}
556
+ return SampledComparisonResult(**result)
412
557
  except ValueError as e:
413
558
  raise HTTPException(status_code=404, detail=str(e))
414
559
  except Exception as e:
@@ -417,7 +562,7 @@ async def run_sampled_comparison(
417
562
 
418
563
  @router.get(
419
564
  "/drift/estimate-sample-size",
420
- response_model=dict,
565
+ response_model=SampleSizeEstimateResponse,
421
566
  summary="Estimate optimal sample size",
422
567
  description="Estimate optimal sample size for drift comparison between two sources.",
423
568
  )
@@ -427,7 +572,7 @@ async def estimate_sample_size(
427
572
  current_source_id: str = Query(..., description="Current source ID"),
428
573
  confidence_level: float = Query(0.95, ge=0.80, le=0.99, description="Target confidence level"),
429
574
  margin_of_error: float = Query(0.03, ge=0.01, le=0.10, description="Acceptable margin of error"),
430
- ) -> dict:
575
+ ) -> SampleSizeEstimateResponse:
431
576
  """Estimate optimal sample size for a drift comparison.
432
577
 
433
578
  Returns recommended sample size based on dataset sizes and
@@ -441,7 +586,7 @@ async def estimate_sample_size(
441
586
  confidence_level=confidence_level,
442
587
  margin_of_error=margin_of_error,
443
588
  )
444
- return {"success": True, "data": estimate}
589
+ return SampleSizeEstimateResponse(**estimate)
445
590
  except ValueError as e:
446
591
  raise HTTPException(status_code=404, detail=str(e))
447
592
  except Exception as e:
@@ -450,14 +595,14 @@ async def estimate_sample_size(
450
595
 
451
596
  @router.get(
452
597
  "/drift/jobs/{job_id}/progress",
453
- response_model=dict,
598
+ response_model=JobProgressResponse,
454
599
  summary="Get job progress",
455
600
  description="Get progress for an active sampled comparison job.",
456
601
  )
457
602
  async def get_job_progress(
458
603
  job_id: str,
459
604
  service: DriftMonitorServiceDep,
460
- ) -> dict:
605
+ ) -> JobProgressResponse:
461
606
  """Get progress for an active comparison job.
462
607
 
463
608
  Returns current progress including:
@@ -470,25 +615,25 @@ async def get_job_progress(
470
615
  if not progress:
471
616
  raise HTTPException(status_code=404, detail="Job not found or completed")
472
617
 
473
- return {"success": True, "data": progress}
618
+ return JobProgressResponse(**progress)
474
619
 
475
620
 
476
621
  @router.post(
477
622
  "/drift/jobs/{job_id}/cancel",
478
- response_model=dict,
623
+ response_model=MessageResponse,
479
624
  summary="Cancel comparison job",
480
625
  description="Cancel an active sampled comparison job.",
481
626
  )
482
627
  async def cancel_job(
483
628
  job_id: str,
484
629
  service: DriftMonitorServiceDep,
485
- ) -> dict:
630
+ ) -> MessageResponse:
486
631
  """Cancel an active comparison job."""
487
632
  cancelled = await service.cancel_job(job_id)
488
633
  if not cancelled:
489
634
  raise HTTPException(status_code=404, detail="Job not found or already completed")
490
635
 
491
- return {"success": True, "message": "Job cancelled"}
636
+ return MessageResponse(message="Job cancelled")
492
637
 
493
638
 
494
639
  # Helper functions
@@ -496,11 +641,21 @@ async def cancel_job(
496
641
 
497
642
  def _monitor_to_dict(monitor) -> dict:
498
643
  """Convert monitor model to dictionary."""
644
+ # Get source names from relationships if available
645
+ baseline_source_name = None
646
+ current_source_name = None
647
+ if hasattr(monitor, "baseline_source") and monitor.baseline_source:
648
+ baseline_source_name = monitor.baseline_source.name
649
+ if hasattr(monitor, "current_source") and monitor.current_source:
650
+ current_source_name = monitor.current_source.name
651
+
499
652
  return {
500
653
  "id": monitor.id,
501
654
  "name": monitor.name,
502
655
  "baseline_source_id": monitor.baseline_source_id,
503
656
  "current_source_id": monitor.current_source_id,
657
+ "baseline_source_name": baseline_source_name,
658
+ "current_source_name": current_source_name,
504
659
  "cron_expression": monitor.cron_expression,
505
660
  "method": monitor.method,
506
661
  "threshold": monitor.threshold,
@@ -510,13 +665,13 @@ def _monitor_to_dict(monitor) -> dict:
510
665
  "alert_threshold_high": monitor.alert_threshold_high,
511
666
  "notification_channel_ids": monitor.notification_channel_ids_json,
512
667
  "status": monitor.status,
513
- "last_run_at": monitor.last_run_at.isoformat() if monitor.last_run_at else None,
668
+ "last_run_at": monitor.last_run_at,
514
669
  "last_drift_detected": monitor.last_drift_detected,
515
670
  "total_runs": monitor.total_runs,
516
671
  "drift_detected_count": monitor.drift_detected_count,
517
672
  "consecutive_drift_count": monitor.consecutive_drift_count,
518
- "created_at": monitor.created_at.isoformat() if monitor.created_at else None,
519
- "updated_at": monitor.updated_at.isoformat() if monitor.updated_at else None,
673
+ "created_at": monitor.created_at,
674
+ "updated_at": monitor.updated_at,
520
675
  }
521
676
 
522
677
 
@@ -525,16 +680,16 @@ def _alert_to_dict(alert) -> dict:
525
680
  return {
526
681
  "id": alert.id,
527
682
  "monitor_id": alert.monitor_id,
528
- "comparison_id": alert.comparison_id,
683
+ "comparison_id": alert.run_id, # Map run_id to comparison_id for frontend compatibility
529
684
  "severity": alert.severity,
530
- "drift_percentage": alert.drift_percentage,
531
- "drifted_columns": alert.drifted_columns_json or [],
685
+ "drift_percentage": alert.drift_score, # Map drift_score to drift_percentage
686
+ "drifted_columns": alert.affected_columns or [], # Map affected_columns to drifted_columns
532
687
  "message": alert.message,
533
688
  "status": alert.status,
534
- "acknowledged_at": alert.acknowledged_at.isoformat() if alert.acknowledged_at else None,
689
+ "acknowledged_at": alert.acknowledged_at,
535
690
  "acknowledged_by": alert.acknowledged_by,
536
- "resolved_at": alert.resolved_at.isoformat() if alert.resolved_at else None,
691
+ "resolved_at": alert.resolved_at,
537
692
  "notes": alert.notes,
538
- "created_at": alert.created_at.isoformat() if alert.created_at else None,
539
- "updated_at": alert.updated_at.isoformat() if alert.updated_at else None,
693
+ "created_at": alert.created_at,
694
+ "updated_at": alert.updated_at,
540
695
  }