truthound-dashboard 1.4.3__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.
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 +437 -10
  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 +11 -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.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.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.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,667 @@
1
+ """Schema Watcher API endpoints.
2
+
3
+ This module provides REST API endpoints for schema watcher management,
4
+ including continuous schema monitoring, alerts, and run history.
5
+
6
+ All schema watcher features use truthound's schema evolution module:
7
+ - SchemaEvolutionDetector for change detection
8
+ - SchemaHistory for version management
9
+ - ColumnRenameDetector for rename detection
10
+ - BreakingChangeAlertManager for alerts
11
+ - ImpactAnalyzer for impact analysis
12
+
13
+ API Design: Direct Response Style
14
+ - Single resources return the resource directly
15
+ - List endpoints return PaginatedResponse with data, total, offset, limit
16
+ - Errors are handled via HTTPException
17
+ - Success is indicated by HTTP status codes (200, 201, 204)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Annotated, Any
23
+
24
+ from fastapi import APIRouter, Depends, HTTPException, Query
25
+
26
+ from truthound_dashboard.core.schema_watcher import SchemaWatcherService
27
+ from truthound_dashboard.schemas.schema_watcher import (
28
+ SchemaWatcherCreate,
29
+ SchemaWatcherUpdate,
30
+ SchemaWatcherResponse,
31
+ SchemaWatcherSummary,
32
+ SchemaWatcherStatistics,
33
+ SchemaWatcherStatus,
34
+ SchemaWatcherStatusAction,
35
+ SchemaWatcherCheckNowResponse,
36
+ SchemaWatcherAlertResponse,
37
+ SchemaWatcherAlertSummary,
38
+ SchemaWatcherAlertAcknowledge,
39
+ SchemaWatcherAlertResolve,
40
+ SchemaWatcherAlertStatus,
41
+ SchemaWatcherAlertSeverity,
42
+ SchemaWatcherRunResponse,
43
+ SchemaWatcherRunSummary,
44
+ SchemaWatcherRunStatus,
45
+ SchemaWatcherSchedulerStatus,
46
+ # Schema Detection types
47
+ SchemaDetectionRequest,
48
+ SchemaDetectionResponse,
49
+ RenameDetectionRequest,
50
+ RenameDetectionResponse,
51
+ # Version History types
52
+ SchemaVersionCreate,
53
+ SchemaVersionResponse,
54
+ SchemaVersionSummary,
55
+ SchemaDiffRequest,
56
+ SchemaDiffResponse,
57
+ SchemaRollbackRequest,
58
+ )
59
+ from truthound_dashboard.schemas.base import MessageResponse, PaginatedResponse
60
+ from .deps import SessionDep
61
+
62
+ router = APIRouter(prefix="/schema-watchers", tags=["Schema Watchers"])
63
+
64
+
65
+ # =============================================================================
66
+ # Dependencies
67
+ # =============================================================================
68
+
69
+
70
+ async def get_schema_watcher_service(session: SessionDep) -> SchemaWatcherService:
71
+ """Get schema watcher service dependency."""
72
+ return SchemaWatcherService(session)
73
+
74
+
75
+ SchemaWatcherServiceDep = Annotated[
76
+ SchemaWatcherService, Depends(get_schema_watcher_service)
77
+ ]
78
+
79
+
80
+ # =============================================================================
81
+ # Watcher CRUD Endpoints
82
+ # =============================================================================
83
+
84
+
85
+ @router.post(
86
+ "",
87
+ response_model=SchemaWatcherResponse,
88
+ status_code=201,
89
+ summary="Create schema watcher",
90
+ description="""
91
+ Create a new schema watcher for continuous schema monitoring.
92
+
93
+ Uses truthound's SchemaWatcher with:
94
+ - SchemaEvolutionDetector for change detection
95
+ - SchemaHistory for version management
96
+ - ColumnRenameDetector for rename detection (configurable)
97
+ """,
98
+ )
99
+ async def create_watcher(
100
+ request: SchemaWatcherCreate,
101
+ service: SchemaWatcherServiceDep,
102
+ ) -> SchemaWatcherResponse:
103
+ """Create a new schema watcher."""
104
+ try:
105
+ return await service.create_watcher(request)
106
+ except ValueError as e:
107
+ raise HTTPException(status_code=400, detail=str(e))
108
+
109
+
110
+ @router.get(
111
+ "",
112
+ response_model=PaginatedResponse[SchemaWatcherSummary],
113
+ summary="List schema watchers",
114
+ description="List all schema watchers with optional filtering.",
115
+ )
116
+ async def list_watchers(
117
+ service: SchemaWatcherServiceDep,
118
+ status: SchemaWatcherStatus | None = Query(None, description="Filter by status"),
119
+ source_id: str | None = Query(None, description="Filter by source ID"),
120
+ limit: int = Query(50, ge=1, le=100),
121
+ offset: int = Query(0, ge=0),
122
+ ) -> PaginatedResponse[SchemaWatcherSummary]:
123
+ """List schema watchers."""
124
+ watchers, total = await service.list_watchers(
125
+ status=status,
126
+ source_id=source_id,
127
+ limit=limit,
128
+ offset=offset,
129
+ )
130
+
131
+ return PaginatedResponse(
132
+ data=watchers,
133
+ total=total,
134
+ offset=offset,
135
+ limit=limit,
136
+ )
137
+
138
+
139
+ @router.get(
140
+ "/statistics",
141
+ response_model=SchemaWatcherStatistics,
142
+ summary="Get watcher statistics",
143
+ description="Get aggregate statistics for all schema watchers.",
144
+ )
145
+ async def get_statistics(
146
+ service: SchemaWatcherServiceDep,
147
+ ) -> SchemaWatcherStatistics:
148
+ """Get schema watcher statistics."""
149
+ return await service.get_statistics()
150
+
151
+
152
+ @router.get(
153
+ "/scheduler/status",
154
+ response_model=SchemaWatcherSchedulerStatus,
155
+ summary="Get scheduler status",
156
+ description="Get the status of the schema watcher scheduler job.",
157
+ )
158
+ async def get_scheduler_status(
159
+ service: SchemaWatcherServiceDep,
160
+ ) -> SchemaWatcherSchedulerStatus:
161
+ """Get schema watcher scheduler status."""
162
+ return await service.get_scheduler_status()
163
+
164
+
165
+ # =============================================================================
166
+ # Alert Endpoints
167
+ # =============================================================================
168
+
169
+
170
+ @router.get(
171
+ "/alerts",
172
+ response_model=PaginatedResponse[SchemaWatcherAlertSummary],
173
+ summary="List alerts",
174
+ description="List all schema watcher alerts with optional filtering.",
175
+ )
176
+ async def list_alerts(
177
+ service: SchemaWatcherServiceDep,
178
+ watcher_id: str | None = Query(None, description="Filter by watcher ID"),
179
+ status: SchemaWatcherAlertStatus | None = Query(None, description="Filter by status"),
180
+ severity: SchemaWatcherAlertSeverity | None = Query(None, description="Filter by severity"),
181
+ limit: int = Query(50, ge=1, le=100),
182
+ offset: int = Query(0, ge=0),
183
+ ) -> PaginatedResponse[SchemaWatcherAlertSummary]:
184
+ """List schema watcher alerts."""
185
+ alerts, total = await service.list_alerts(
186
+ watcher_id=watcher_id,
187
+ status=status,
188
+ severity=severity,
189
+ limit=limit,
190
+ offset=offset,
191
+ )
192
+
193
+ return PaginatedResponse(
194
+ data=alerts,
195
+ total=total,
196
+ offset=offset,
197
+ limit=limit,
198
+ )
199
+
200
+
201
+ @router.get(
202
+ "/alerts/{alert_id}",
203
+ response_model=SchemaWatcherAlertResponse,
204
+ summary="Get alert",
205
+ description="Get a specific schema watcher alert by ID.",
206
+ )
207
+ async def get_alert(
208
+ alert_id: str,
209
+ service: SchemaWatcherServiceDep,
210
+ ) -> SchemaWatcherAlertResponse:
211
+ """Get a schema watcher alert by ID."""
212
+ alert = await service.get_alert(alert_id)
213
+ if not alert:
214
+ raise HTTPException(status_code=404, detail="Alert not found")
215
+ return alert
216
+
217
+
218
+ @router.post(
219
+ "/alerts/{alert_id}/acknowledge",
220
+ response_model=SchemaWatcherAlertResponse,
221
+ summary="Acknowledge alert",
222
+ description="Acknowledge a schema watcher alert.",
223
+ )
224
+ async def acknowledge_alert(
225
+ alert_id: str,
226
+ request: SchemaWatcherAlertAcknowledge,
227
+ service: SchemaWatcherServiceDep,
228
+ ) -> SchemaWatcherAlertResponse:
229
+ """Acknowledge an alert."""
230
+ alert = await service.acknowledge_alert(
231
+ alert_id,
232
+ acknowledged_by=request.acknowledged_by,
233
+ )
234
+ if not alert:
235
+ raise HTTPException(status_code=404, detail="Alert not found")
236
+ return alert
237
+
238
+
239
+ @router.post(
240
+ "/alerts/{alert_id}/resolve",
241
+ response_model=SchemaWatcherAlertResponse,
242
+ summary="Resolve alert",
243
+ description="Resolve a schema watcher alert.",
244
+ )
245
+ async def resolve_alert(
246
+ alert_id: str,
247
+ request: SchemaWatcherAlertResolve,
248
+ service: SchemaWatcherServiceDep,
249
+ ) -> SchemaWatcherAlertResponse:
250
+ """Resolve an alert."""
251
+ alert = await service.resolve_alert(
252
+ alert_id,
253
+ resolved_by=request.resolved_by,
254
+ resolution_notes=request.resolution_notes,
255
+ )
256
+ if not alert:
257
+ raise HTTPException(status_code=404, detail="Alert not found")
258
+ return alert
259
+
260
+
261
+ # =============================================================================
262
+ # Run Endpoints
263
+ # =============================================================================
264
+
265
+
266
+ @router.get(
267
+ "/runs",
268
+ response_model=PaginatedResponse[SchemaWatcherRunSummary],
269
+ summary="List runs",
270
+ description="List all schema watcher runs with optional filtering.",
271
+ )
272
+ async def list_runs(
273
+ service: SchemaWatcherServiceDep,
274
+ watcher_id: str | None = Query(None, description="Filter by watcher ID"),
275
+ status: SchemaWatcherRunStatus | None = Query(None, description="Filter by status"),
276
+ limit: int = Query(50, ge=1, le=100),
277
+ offset: int = Query(0, ge=0),
278
+ ) -> PaginatedResponse[SchemaWatcherRunSummary]:
279
+ """List schema watcher runs."""
280
+ runs, total = await service.list_runs(
281
+ watcher_id=watcher_id,
282
+ status=status,
283
+ limit=limit,
284
+ offset=offset,
285
+ )
286
+
287
+ return PaginatedResponse(
288
+ data=runs,
289
+ total=total,
290
+ offset=offset,
291
+ limit=limit,
292
+ )
293
+
294
+
295
+ @router.get(
296
+ "/runs/{run_id}",
297
+ response_model=SchemaWatcherRunResponse,
298
+ summary="Get run",
299
+ description="Get a specific schema watcher run by ID.",
300
+ )
301
+ async def get_run(
302
+ run_id: str,
303
+ service: SchemaWatcherServiceDep,
304
+ ) -> SchemaWatcherRunResponse:
305
+ """Get a schema watcher run by ID."""
306
+ run = await service.get_run(run_id)
307
+ if not run:
308
+ raise HTTPException(status_code=404, detail="Run not found")
309
+ return run
310
+
311
+
312
+ @router.get(
313
+ "/{watcher_id}",
314
+ response_model=SchemaWatcherResponse,
315
+ summary="Get schema watcher",
316
+ description="Get a specific schema watcher by ID.",
317
+ )
318
+ async def get_watcher(
319
+ watcher_id: str,
320
+ service: SchemaWatcherServiceDep,
321
+ ) -> SchemaWatcherResponse:
322
+ """Get a schema watcher by ID."""
323
+ watcher = await service.get_watcher(watcher_id)
324
+ if not watcher:
325
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
326
+ return watcher
327
+
328
+
329
+ @router.put(
330
+ "/{watcher_id}",
331
+ response_model=SchemaWatcherResponse,
332
+ summary="Update schema watcher",
333
+ description="Update a schema watcher's configuration.",
334
+ )
335
+ async def update_watcher(
336
+ watcher_id: str,
337
+ request: SchemaWatcherUpdate,
338
+ service: SchemaWatcherServiceDep,
339
+ ) -> SchemaWatcherResponse:
340
+ """Update a schema watcher."""
341
+ watcher = await service.update_watcher(watcher_id, request)
342
+ if not watcher:
343
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
344
+ return watcher
345
+
346
+
347
+ @router.delete(
348
+ "/{watcher_id}",
349
+ response_model=MessageResponse,
350
+ summary="Delete schema watcher",
351
+ description="Delete a schema watcher and all its alerts and runs.",
352
+ )
353
+ async def delete_watcher(
354
+ watcher_id: str,
355
+ service: SchemaWatcherServiceDep,
356
+ ) -> MessageResponse:
357
+ """Delete a schema watcher."""
358
+ deleted = await service.delete_watcher(watcher_id)
359
+ if not deleted:
360
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
361
+ return MessageResponse(message="Schema watcher deleted successfully")
362
+
363
+
364
+ @router.post(
365
+ "/{watcher_id}/status",
366
+ response_model=SchemaWatcherResponse,
367
+ summary="Set watcher status",
368
+ description="Change the status of a schema watcher (active, paused, stopped).",
369
+ )
370
+ async def set_watcher_status(
371
+ watcher_id: str,
372
+ request: SchemaWatcherStatusAction,
373
+ service: SchemaWatcherServiceDep,
374
+ ) -> SchemaWatcherResponse:
375
+ """Set schema watcher status."""
376
+ watcher = await service.set_watcher_status(watcher_id, request.status)
377
+ if not watcher:
378
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
379
+ return watcher
380
+
381
+
382
+ @router.post(
383
+ "/{watcher_id}/check",
384
+ response_model=SchemaWatcherCheckNowResponse,
385
+ summary="Check schema now",
386
+ description="""
387
+ Trigger an immediate schema check for a watcher.
388
+
389
+ This performs:
390
+ 1. Learn current schema from source using truthound
391
+ 2. Compare with previous version using SchemaEvolutionDetector
392
+ 3. Detect renames using ColumnRenameDetector (if enabled)
393
+ 4. Save new version to SchemaHistory
394
+ 5. Create alert if breaking changes detected
395
+ """,
396
+ )
397
+ async def check_now(
398
+ watcher_id: str,
399
+ service: SchemaWatcherServiceDep,
400
+ ) -> SchemaWatcherCheckNowResponse:
401
+ """Execute schema check immediately."""
402
+ try:
403
+ return await service.check_now(watcher_id)
404
+ except ValueError as e:
405
+ raise HTTPException(status_code=404, detail=str(e))
406
+
407
+
408
+ # =============================================================================
409
+ # Schema Detection Endpoints (truthound integration)
410
+ # =============================================================================
411
+
412
+
413
+ @router.post(
414
+ "/detect-changes",
415
+ response_model=SchemaDetectionResponse,
416
+ summary="Detect schema changes",
417
+ description="""
418
+ Detect changes between two schemas using truthound's SchemaEvolutionDetector.
419
+
420
+ Detects:
421
+ - Column additions/removals
422
+ - Type changes
423
+ - Nullable changes
424
+ - Constraint changes
425
+ - Column renames (with similarity algorithms)
426
+ """,
427
+ )
428
+ async def detect_schema_changes(
429
+ request: SchemaDetectionRequest,
430
+ service: SchemaWatcherServiceDep,
431
+ ) -> SchemaDetectionResponse:
432
+ """Detect schema changes between two schemas."""
433
+ return await service.detect_changes(
434
+ current_schema=request.current_schema,
435
+ baseline_schema=request.baseline_schema,
436
+ detect_renames=request.detect_renames,
437
+ rename_similarity_threshold=request.rename_similarity_threshold,
438
+ )
439
+
440
+
441
+ @router.post(
442
+ "/detect-renames",
443
+ response_model=RenameDetectionResponse,
444
+ summary="Detect column renames",
445
+ description="""
446
+ Detect column renames using truthound's ColumnRenameDetector.
447
+
448
+ Supports multiple similarity algorithms:
449
+ - composite: Weighted combination (default)
450
+ - levenshtein: Edit distance
451
+ - jaro_winkler: Short strings, prefixes
452
+ - ngram: Partial matches
453
+ - token: snake_case/camelCase names
454
+ """,
455
+ )
456
+ async def detect_column_renames(
457
+ request: RenameDetectionRequest,
458
+ service: SchemaWatcherServiceDep,
459
+ ) -> RenameDetectionResponse:
460
+ """Detect column renames between added and removed columns."""
461
+ return await service.detect_renames(
462
+ added_columns=request.added_columns,
463
+ removed_columns=request.removed_columns,
464
+ similarity_threshold=request.similarity_threshold,
465
+ require_type_match=request.require_type_match,
466
+ allow_compatible_types=request.allow_compatible_types,
467
+ algorithm=request.algorithm.value,
468
+ )
469
+
470
+
471
+ # =============================================================================
472
+ # Version History Endpoints (truthound integration)
473
+ # =============================================================================
474
+
475
+
476
+ @router.get(
477
+ "/{watcher_id}/versions",
478
+ response_model=list[SchemaVersionSummary],
479
+ summary="List schema versions",
480
+ description="List schema versions tracked by this watcher using truthound's SchemaHistory.",
481
+ )
482
+ async def list_schema_versions(
483
+ watcher_id: str,
484
+ service: SchemaWatcherServiceDep,
485
+ limit: int = Query(50, ge=1, le=100),
486
+ ) -> list[SchemaVersionSummary]:
487
+ """List schema versions for a watcher."""
488
+ # Verify watcher exists
489
+ watcher = await service.get_watcher(watcher_id)
490
+ if not watcher:
491
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
492
+
493
+ return await service.list_schema_versions(watcher_id, limit=limit)
494
+
495
+
496
+ @router.get(
497
+ "/{watcher_id}/versions/{version}",
498
+ response_model=SchemaVersionResponse,
499
+ summary="Get schema version",
500
+ description="Get a specific schema version by version string or ID.",
501
+ )
502
+ async def get_schema_version(
503
+ watcher_id: str,
504
+ version: str,
505
+ service: SchemaWatcherServiceDep,
506
+ ) -> SchemaVersionResponse:
507
+ """Get a specific schema version."""
508
+ result = await service.get_schema_version(watcher_id, version)
509
+ if not result:
510
+ raise HTTPException(status_code=404, detail="Schema version not found")
511
+ return result
512
+
513
+
514
+ @router.post(
515
+ "/{watcher_id}/versions",
516
+ response_model=SchemaVersionResponse,
517
+ status_code=201,
518
+ summary="Save schema version",
519
+ description="Manually save a schema version to history.",
520
+ )
521
+ async def save_schema_version(
522
+ watcher_id: str,
523
+ request: SchemaVersionCreate,
524
+ service: SchemaWatcherServiceDep,
525
+ ) -> SchemaVersionResponse:
526
+ """Save a schema version."""
527
+ # Verify watcher exists
528
+ watcher = await service.get_watcher(watcher_id)
529
+ if not watcher:
530
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
531
+
532
+ try:
533
+ return await service.save_schema_version(
534
+ watcher_id,
535
+ request.schema,
536
+ version=request.version,
537
+ metadata=request.metadata,
538
+ )
539
+ except ValueError as e:
540
+ raise HTTPException(status_code=400, detail=str(e))
541
+
542
+
543
+ @router.post(
544
+ "/{watcher_id}/versions/diff",
545
+ response_model=SchemaDiffResponse,
546
+ summary="Diff schema versions",
547
+ description="Get the diff between two schema versions.",
548
+ )
549
+ async def diff_schema_versions(
550
+ watcher_id: str,
551
+ request: SchemaDiffRequest,
552
+ service: SchemaWatcherServiceDep,
553
+ ) -> SchemaDiffResponse:
554
+ """Get diff between schema versions."""
555
+ # Verify watcher exists
556
+ watcher = await service.get_watcher(watcher_id)
557
+ if not watcher:
558
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
559
+
560
+ try:
561
+ return await service.diff_versions(
562
+ watcher_id,
563
+ request.from_version,
564
+ request.to_version,
565
+ )
566
+ except ValueError as e:
567
+ raise HTTPException(status_code=400, detail=str(e))
568
+
569
+
570
+ @router.post(
571
+ "/{watcher_id}/versions/rollback",
572
+ response_model=SchemaVersionResponse,
573
+ summary="Rollback schema version",
574
+ description="Rollback to a previous schema version (creates a new version matching the target).",
575
+ )
576
+ async def rollback_schema_version(
577
+ watcher_id: str,
578
+ request: SchemaRollbackRequest,
579
+ service: SchemaWatcherServiceDep,
580
+ ) -> SchemaVersionResponse:
581
+ """Rollback to a previous schema version."""
582
+ # Verify watcher exists
583
+ watcher = await service.get_watcher(watcher_id)
584
+ if not watcher:
585
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
586
+
587
+ try:
588
+ return await service.rollback_version(
589
+ watcher_id,
590
+ request.to_version,
591
+ reason=request.reason,
592
+ )
593
+ except ValueError as e:
594
+ raise HTTPException(status_code=400, detail=str(e))
595
+
596
+
597
+ # =============================================================================
598
+ # Watcher-specific Alert and Run Endpoints
599
+ # =============================================================================
600
+
601
+
602
+ @router.get(
603
+ "/{watcher_id}/alerts",
604
+ response_model=PaginatedResponse[SchemaWatcherAlertSummary],
605
+ summary="List watcher alerts",
606
+ description="List alerts for a specific schema watcher.",
607
+ )
608
+ async def list_watcher_alerts(
609
+ watcher_id: str,
610
+ service: SchemaWatcherServiceDep,
611
+ status: SchemaWatcherAlertStatus | None = Query(None, description="Filter by status"),
612
+ limit: int = Query(50, ge=1, le=100),
613
+ offset: int = Query(0, ge=0),
614
+ ) -> PaginatedResponse[SchemaWatcherAlertSummary]:
615
+ """List alerts for a specific watcher."""
616
+ # Verify watcher exists
617
+ watcher = await service.get_watcher(watcher_id)
618
+ if not watcher:
619
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
620
+
621
+ alerts, total = await service.list_alerts(
622
+ watcher_id=watcher_id,
623
+ status=status,
624
+ limit=limit,
625
+ offset=offset,
626
+ )
627
+
628
+ return PaginatedResponse(
629
+ data=alerts,
630
+ total=total,
631
+ offset=offset,
632
+ limit=limit,
633
+ )
634
+
635
+
636
+ @router.get(
637
+ "/{watcher_id}/runs",
638
+ response_model=PaginatedResponse[SchemaWatcherRunSummary],
639
+ summary="List watcher runs",
640
+ description="List runs for a specific schema watcher.",
641
+ )
642
+ async def list_watcher_runs(
643
+ watcher_id: str,
644
+ service: SchemaWatcherServiceDep,
645
+ status: SchemaWatcherRunStatus | None = Query(None, description="Filter by status"),
646
+ limit: int = Query(50, ge=1, le=100),
647
+ offset: int = Query(0, ge=0),
648
+ ) -> PaginatedResponse[SchemaWatcherRunSummary]:
649
+ """List runs for a specific watcher."""
650
+ # Verify watcher exists
651
+ watcher = await service.get_watcher(watcher_id)
652
+ if not watcher:
653
+ raise HTTPException(status_code=404, detail="Schema watcher not found")
654
+
655
+ runs, total = await service.list_runs(
656
+ watcher_id=watcher_id,
657
+ status=status,
658
+ limit=limit,
659
+ offset=offset,
660
+ )
661
+
662
+ return PaginatedResponse(
663
+ data=runs,
664
+ total=total,
665
+ offset=offset,
666
+ limit=limit,
667
+ )