truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.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.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +437 -10
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +11 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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=
|
|
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
|
-
) ->
|
|
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=
|
|
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
|
-
) ->
|
|
120
|
+
) -> DriftMonitorSummary:
|
|
114
121
|
"""Get summary of all drift monitors."""
|
|
115
122
|
summary = await service.get_summary()
|
|
116
|
-
return
|
|
123
|
+
return DriftMonitorSummary(**summary)
|
|
117
124
|
|
|
118
125
|
|
|
119
126
|
@router.post(
|
|
120
127
|
"/drift/preview",
|
|
121
|
-
response_model=
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
171
|
+
return DriftMonitorResponse(**_monitor_to_dict(monitor))
|
|
168
172
|
|
|
169
173
|
|
|
170
174
|
@router.put(
|
|
171
175
|
"/drift/monitors/{monitor_id}",
|
|
172
|
-
response_model=
|
|
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
|
-
) ->
|
|
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
|
|
192
|
+
return DriftMonitorResponse(**_monitor_to_dict(monitor))
|
|
189
193
|
|
|
190
194
|
|
|
191
195
|
@router.delete(
|
|
192
196
|
"/drift/monitors/{monitor_id}",
|
|
193
|
-
response_model=
|
|
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
|
-
) ->
|
|
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
|
|
210
|
+
return MessageResponse(message="Monitor deleted")
|
|
207
211
|
|
|
208
212
|
|
|
209
213
|
@router.post(
|
|
210
214
|
"/drift/monitors/{monitor_id}/run",
|
|
211
|
-
response_model=
|
|
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
|
-
) ->
|
|
222
|
+
) -> MonitorRunResult:
|
|
219
223
|
"""Manually run a drift monitor."""
|
|
220
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"
|
|
229
|
-
|
|
230
|
-
"
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
421
|
+
return RootCauseAnalysis(**analysis)
|
|
276
422
|
|
|
277
423
|
|
|
278
424
|
@router.get(
|
|
279
425
|
"/drift/comparisons/{run_id}/root-cause",
|
|
280
|
-
response_model=
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
494
|
+
return DriftAlertResponse(**_alert_to_dict(alert))
|
|
350
495
|
|
|
351
496
|
|
|
352
497
|
@router.put(
|
|
353
498
|
"/drift/alerts/{alert_id}",
|
|
354
|
-
response_model=
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
-
) ->
|
|
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
|
|
618
|
+
return JobProgressResponse(**progress)
|
|
474
619
|
|
|
475
620
|
|
|
476
621
|
@router.post(
|
|
477
622
|
"/drift/jobs/{job_id}/cancel",
|
|
478
|
-
response_model=
|
|
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
|
-
) ->
|
|
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
|
|
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
|
|
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
|
|
519
|
-
"updated_at": monitor.updated_at
|
|
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.
|
|
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
|
|
689
|
+
"acknowledged_at": alert.acknowledged_at,
|
|
535
690
|
"acknowledged_by": alert.acknowledged_by,
|
|
536
|
-
"resolved_at": alert.resolved_at
|
|
691
|
+
"resolved_at": alert.resolved_at,
|
|
537
692
|
"notes": alert.notes,
|
|
538
|
-
"created_at": alert.created_at
|
|
539
|
-
"updated_at": alert.updated_at
|
|
693
|
+
"created_at": alert.created_at,
|
|
694
|
+
"updated_at": alert.updated_at,
|
|
540
695
|
}
|