truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.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 (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
  164. truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
  166. truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,23 +7,41 @@ from fastapi import APIRouter
7
7
 
8
8
  from . import (
9
9
  # Phase 1-4
10
+ alerts,
10
11
  drift,
11
12
  health,
12
13
  history,
14
+ maintenance,
13
15
  mask,
14
16
  notifications,
17
+ notifications_advanced,
15
18
  profile,
19
+ reports,
16
20
  rules,
17
21
  scan,
18
22
  schedules,
19
23
  schemas,
20
24
  sources,
25
+ triggers,
21
26
  validations,
22
27
  validators,
28
+ versioning,
29
+ websocket,
23
30
  # Phase 5
24
31
  catalog,
25
32
  collaboration,
26
33
  glossary,
34
+ # Schema Evolution & Rule Suggestions
35
+ rule_suggestions,
36
+ schema_evolution,
37
+ # Phase 9: Plugin System
38
+ plugins,
39
+ # Phase 10: ML & Lineage
40
+ anomaly,
41
+ lineage,
42
+ model_monitoring,
43
+ # Cross-Feature Integration
44
+ cross_alerts,
27
45
  )
28
46
 
29
47
  api_router = APIRouter()
@@ -132,3 +150,132 @@ api_router.include_router(
132
150
  collaboration.router,
133
151
  tags=["collaboration"],
134
152
  )
153
+
154
+ # =============================================================================
155
+ # Phase 4: Reports & Maintenance & Versioning
156
+ # =============================================================================
157
+
158
+ # Report generation endpoints
159
+ api_router.include_router(
160
+ reports.router,
161
+ prefix="/reports",
162
+ tags=["reports"],
163
+ )
164
+
165
+ # Maintenance and retention policy endpoints
166
+ api_router.include_router(
167
+ maintenance.router,
168
+ prefix="/maintenance",
169
+ tags=["maintenance"],
170
+ )
171
+
172
+ # Versioning endpoints
173
+ api_router.include_router(
174
+ versioning.router,
175
+ tags=["versioning"],
176
+ )
177
+
178
+ # =============================================================================
179
+ # Schema Evolution & Rule Suggestions
180
+ # =============================================================================
181
+
182
+ # Schema evolution endpoints
183
+ api_router.include_router(
184
+ schema_evolution.router,
185
+ tags=["schema-evolution"],
186
+ )
187
+
188
+ # Rule suggestion endpoints
189
+ api_router.include_router(
190
+ rule_suggestions.router,
191
+ tags=["rule-suggestions"],
192
+ )
193
+
194
+ # Rule suggestion presets endpoint
195
+ api_router.include_router(
196
+ rule_suggestions.presets_router,
197
+ tags=["rule-suggestions"],
198
+ )
199
+
200
+ # =============================================================================
201
+ # Phase 14: Advanced Notifications
202
+ # =============================================================================
203
+
204
+ # Advanced notification endpoints (routing, deduplication, throttling, escalation)
205
+ api_router.include_router(
206
+ notifications_advanced.router,
207
+ tags=["notifications-advanced"],
208
+ )
209
+
210
+ # =============================================================================
211
+ # Phase 10: ML & Lineage
212
+ # =============================================================================
213
+
214
+ # Data lineage endpoints
215
+ api_router.include_router(
216
+ lineage.router,
217
+ prefix="/lineage",
218
+ tags=["lineage"],
219
+ )
220
+
221
+ # Anomaly detection endpoints
222
+ api_router.include_router(
223
+ anomaly.router,
224
+ tags=["anomaly"],
225
+ )
226
+
227
+ # ML Model Monitoring endpoints
228
+ api_router.include_router(
229
+ model_monitoring.router,
230
+ tags=["model-monitoring"],
231
+ )
232
+
233
+ # =============================================================================
234
+ # Unified Alerts
235
+ # =============================================================================
236
+
237
+ # Unified alerts aggregation endpoints
238
+ api_router.include_router(
239
+ alerts.router,
240
+ tags=["alerts"],
241
+ )
242
+
243
+ # =============================================================================
244
+ # Cross-Feature Integration
245
+ # =============================================================================
246
+
247
+ # Cross-alert correlation endpoints (anomaly + drift)
248
+ api_router.include_router(
249
+ cross_alerts.router,
250
+ tags=["cross-alerts"],
251
+ )
252
+
253
+ # =============================================================================
254
+ # Trigger Monitoring & Webhooks
255
+ # =============================================================================
256
+
257
+ # Trigger monitoring and webhook endpoints
258
+ api_router.include_router(
259
+ triggers.router,
260
+ tags=["triggers"],
261
+ )
262
+
263
+ # =============================================================================
264
+ # WebSocket Real-time Updates
265
+ # =============================================================================
266
+
267
+ # WebSocket endpoints for real-time updates
268
+ api_router.include_router(
269
+ websocket.router,
270
+ tags=["websocket"],
271
+ )
272
+
273
+ # =============================================================================
274
+ # Phase 9: Plugin System
275
+ # =============================================================================
276
+
277
+ # Plugin marketplace and custom validators/reporters
278
+ api_router.include_router(
279
+ plugins.router,
280
+ tags=["plugins"],
281
+ )
@@ -0,0 +1,310 @@
1
+ """Rule suggestion API endpoints.
2
+
3
+ This module provides endpoints for generating and applying
4
+ validation rule suggestions based on profile data.
5
+
6
+ Features:
7
+ - Multiple strictness levels (loose, medium, strict)
8
+ - Preset templates for different use cases
9
+ - Multiple export formats (YAML, JSON, Python, TOML)
10
+ - Category-based filtering
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from fastapi import APIRouter, HTTPException, status
16
+ from fastapi.responses import PlainTextResponse
17
+
18
+ from truthound_dashboard.core.rule_generator import RuleGeneratorService
19
+ from truthound_dashboard.schemas import (
20
+ ApplyRulesRequest,
21
+ ApplyRulesResponse,
22
+ ExportRulesRequest,
23
+ ExportRulesResponse,
24
+ PresetsResponse,
25
+ RuleSuggestionRequest,
26
+ RuleSuggestionResponse,
27
+ )
28
+
29
+ from .deps import (
30
+ ProfileServiceDep,
31
+ RuleGeneratorServiceDep,
32
+ SchemaServiceDep,
33
+ SourceServiceDep,
34
+ )
35
+
36
+ router = APIRouter(prefix="/sources", tags=["rule-suggestions"])
37
+ presets_router = APIRouter(prefix="/rule-suggestions", tags=["rule-suggestions"])
38
+
39
+
40
+ @router.post(
41
+ "/{source_id}/rules/suggest",
42
+ response_model=RuleSuggestionResponse,
43
+ summary="Generate rule suggestions",
44
+ description="""Generate validation rule suggestions based on profile data.
45
+
46
+ Supports:
47
+ - **Strictness levels**: loose, medium, strict
48
+ - **Presets**: default, strict, loose, minimal, comprehensive, ci_cd, schema_only, format_only, cross_column, data_integrity
49
+ - **Category filtering**: schema, stats, pattern, completeness, uniqueness, distribution, relationship, multi_column
50
+ - **Cross-column rules**: composite keys, column comparisons, arithmetic relationships, dependencies, coexistence
51
+ """,
52
+ )
53
+ async def suggest_rules(
54
+ source_id: str,
55
+ generator_service: RuleGeneratorServiceDep,
56
+ profile_service: ProfileServiceDep,
57
+ schema_service: SchemaServiceDep,
58
+ source_service: SourceServiceDep,
59
+ request: RuleSuggestionRequest | None = None,
60
+ ) -> RuleSuggestionResponse:
61
+ """Generate rule suggestions for a source.
62
+
63
+ Args:
64
+ source_id: Source ID.
65
+ generator_service: Rule generator service.
66
+ profile_service: Profile service.
67
+ schema_service: Schema service.
68
+ source_service: Source service.
69
+ request: Optional request parameters.
70
+
71
+ Returns:
72
+ Rule suggestion response with single-column and cross-column suggestions.
73
+ """
74
+ # Verify source exists
75
+ source = await source_service.get(source_id)
76
+ if not source:
77
+ raise HTTPException(
78
+ status_code=status.HTTP_404_NOT_FOUND,
79
+ detail=f"Source {source_id} not found",
80
+ )
81
+
82
+ # Parse request
83
+ if request is None:
84
+ request = RuleSuggestionRequest()
85
+
86
+ # Get profile
87
+ if request.profile_id and not request.use_latest_profile:
88
+ profile = await profile_service.get(request.profile_id)
89
+ if not profile:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_404_NOT_FOUND,
92
+ detail=f"Profile {request.profile_id} not found",
93
+ )
94
+ else:
95
+ profile = await profile_service.get_latest(source_id)
96
+ if not profile:
97
+ raise HTTPException(
98
+ status_code=status.HTTP_404_NOT_FOUND,
99
+ detail=f"No profile found for source {source_id}. Run profiling first.",
100
+ )
101
+
102
+ # Get schema (optional)
103
+ schema = await schema_service.get_active(source_id)
104
+
105
+ # Generate suggestions with advanced options including cross-column
106
+ result = await generator_service.generate_suggestions(
107
+ source,
108
+ profile,
109
+ schema,
110
+ min_confidence=request.min_confidence,
111
+ strictness=request.strictness,
112
+ preset=request.preset,
113
+ include_categories=request.include_categories,
114
+ exclude_categories=request.exclude_categories,
115
+ enable_cross_column=request.enable_cross_column,
116
+ include_cross_column_types=request.include_cross_column_types,
117
+ exclude_cross_column_types=request.exclude_cross_column_types,
118
+ )
119
+
120
+ return result
121
+
122
+
123
+ @router.post(
124
+ "/{source_id}/rules/apply-suggestions",
125
+ response_model=ApplyRulesResponse,
126
+ summary="Apply rule suggestions",
127
+ description="Apply selected rule suggestions to create validation rules.",
128
+ )
129
+ async def apply_rule_suggestions(
130
+ source_id: str,
131
+ request: ApplyRulesRequest,
132
+ generator_service: RuleGeneratorServiceDep,
133
+ source_service: SourceServiceDep,
134
+ ) -> ApplyRulesResponse:
135
+ """Apply selected rule suggestions.
136
+
137
+ Args:
138
+ source_id: Source ID.
139
+ request: Apply rules request.
140
+ generator_service: Rule generator service.
141
+ source_service: Source service.
142
+
143
+ Returns:
144
+ Apply rules response.
145
+ """
146
+ # Verify source exists
147
+ source = await source_service.get(source_id)
148
+ if not source:
149
+ raise HTTPException(
150
+ status_code=status.HTTP_404_NOT_FOUND,
151
+ detail=f"Source {source_id} not found",
152
+ )
153
+
154
+ # Validate request
155
+ if not request.suggestions:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_400_BAD_REQUEST,
158
+ detail="No suggestions provided to apply",
159
+ )
160
+
161
+ # Apply suggestions
162
+ result = await generator_service.apply_suggestions(
163
+ source,
164
+ request.suggestions,
165
+ rule_name=request.rule_name,
166
+ rule_description=request.rule_description,
167
+ )
168
+
169
+ return result
170
+
171
+
172
+ @router.post(
173
+ "/{source_id}/rules/export",
174
+ response_model=ExportRulesResponse,
175
+ summary="Export rules",
176
+ description="""Export generated rules in various formats.
177
+
178
+ Supported formats:
179
+ - **yaml**: Human-readable YAML format
180
+ - **json**: Machine-readable JSON format
181
+ - **python**: Executable Python code
182
+ - **toml**: Configuration-friendly TOML format
183
+ """,
184
+ )
185
+ async def export_rules(
186
+ source_id: str,
187
+ request: ExportRulesRequest,
188
+ generator_service: RuleGeneratorServiceDep,
189
+ source_service: SourceServiceDep,
190
+ ) -> ExportRulesResponse:
191
+ """Export rules in specified format.
192
+
193
+ Args:
194
+ source_id: Source ID.
195
+ request: Export request.
196
+ generator_service: Rule generator service.
197
+ source_service: Source service.
198
+
199
+ Returns:
200
+ Export response with content.
201
+ """
202
+ # Verify source exists
203
+ source = await source_service.get(source_id)
204
+ if not source:
205
+ raise HTTPException(
206
+ status_code=status.HTTP_404_NOT_FOUND,
207
+ detail=f"Source {source_id} not found",
208
+ )
209
+
210
+ # Validate request
211
+ if not request.suggestions:
212
+ raise HTTPException(
213
+ status_code=status.HTTP_400_BAD_REQUEST,
214
+ detail="No suggestions provided to export",
215
+ )
216
+
217
+ # Export rules
218
+ result = generator_service.export_rules(
219
+ request.suggestions,
220
+ format=request.format,
221
+ rule_name=request.rule_name,
222
+ description=request.description,
223
+ include_metadata=request.include_metadata,
224
+ )
225
+
226
+ return result
227
+
228
+
229
+ @router.post(
230
+ "/{source_id}/rules/export/download",
231
+ response_class=PlainTextResponse,
232
+ summary="Download exported rules",
233
+ description="Download rules as a file in the specified format.",
234
+ )
235
+ async def download_exported_rules(
236
+ source_id: str,
237
+ request: ExportRulesRequest,
238
+ generator_service: RuleGeneratorServiceDep,
239
+ source_service: SourceServiceDep,
240
+ ) -> PlainTextResponse:
241
+ """Download rules as a file.
242
+
243
+ Args:
244
+ source_id: Source ID.
245
+ request: Export request.
246
+ generator_service: Rule generator service.
247
+ source_service: Source service.
248
+
249
+ Returns:
250
+ Plain text response with file content.
251
+ """
252
+ # Verify source exists
253
+ source = await source_service.get(source_id)
254
+ if not source:
255
+ raise HTTPException(
256
+ status_code=status.HTTP_404_NOT_FOUND,
257
+ detail=f"Source {source_id} not found",
258
+ )
259
+
260
+ if not request.suggestions:
261
+ raise HTTPException(
262
+ status_code=status.HTTP_400_BAD_REQUEST,
263
+ detail="No suggestions provided to export",
264
+ )
265
+
266
+ # Export rules
267
+ result = generator_service.export_rules(
268
+ request.suggestions,
269
+ format=request.format,
270
+ rule_name=request.rule_name,
271
+ description=request.description,
272
+ include_metadata=request.include_metadata,
273
+ )
274
+
275
+ # Determine content type
276
+ content_type_map = {
277
+ "yaml": "application/x-yaml",
278
+ "json": "application/json",
279
+ "python": "text/x-python",
280
+ "toml": "application/toml",
281
+ }
282
+ content_type = content_type_map.get(result.format.value, "text/plain")
283
+
284
+ return PlainTextResponse(
285
+ content=result.content,
286
+ media_type=content_type,
287
+ headers={
288
+ "Content-Disposition": f'attachment; filename="{result.filename}"'
289
+ },
290
+ )
291
+
292
+
293
+ # =============================================================================
294
+ # Presets Router
295
+ # =============================================================================
296
+
297
+
298
+ @presets_router.get(
299
+ "/presets",
300
+ response_model=PresetsResponse,
301
+ summary="Get available presets",
302
+ description="Get list of available presets, strictness levels, categories, and export formats.",
303
+ )
304
+ async def get_presets() -> PresetsResponse:
305
+ """Get available rule generation presets and options.
306
+
307
+ Returns:
308
+ Presets response.
309
+ """
310
+ return RuleGeneratorService.get_presets()
@@ -0,0 +1,231 @@
1
+ """Schema evolution API endpoints.
2
+
3
+ This module provides endpoints for schema evolution detection,
4
+ version tracking, and change history.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from fastapi import APIRouter, HTTPException, status
10
+
11
+ from truthound_dashboard.schemas import (
12
+ SchemaChangeListResponse,
13
+ SchemaEvolutionResponse,
14
+ SchemaEvolutionSummary,
15
+ SchemaVersionListResponse,
16
+ SchemaVersionResponse,
17
+ )
18
+
19
+ from .deps import (
20
+ SchemaEvolutionServiceDep,
21
+ SchemaServiceDep,
22
+ SourceServiceDep,
23
+ )
24
+
25
+ router = APIRouter(prefix="/sources", tags=["schema-evolution"])
26
+
27
+
28
+ @router.get(
29
+ "/{source_id}/schema/versions",
30
+ response_model=SchemaVersionListResponse,
31
+ summary="List schema versions",
32
+ description="Get schema version history for a source.",
33
+ )
34
+ async def list_schema_versions(
35
+ source_id: str,
36
+ evolution_service: SchemaEvolutionServiceDep,
37
+ source_service: SourceServiceDep,
38
+ limit: int = 20,
39
+ offset: int = 0,
40
+ ) -> SchemaVersionListResponse:
41
+ """List schema versions for a source.
42
+
43
+ Args:
44
+ source_id: Source ID.
45
+ evolution_service: Schema evolution service.
46
+ source_service: Source service.
47
+ limit: Maximum versions to return.
48
+ offset: Number to skip.
49
+
50
+ Returns:
51
+ List of schema versions.
52
+ """
53
+ # Verify source exists
54
+ source = await source_service.get(source_id)
55
+ if not source:
56
+ raise HTTPException(
57
+ status_code=status.HTTP_404_NOT_FOUND,
58
+ detail=f"Source {source_id} not found",
59
+ )
60
+
61
+ versions = await evolution_service.get_version_history(
62
+ source_id, limit=limit, offset=offset
63
+ )
64
+
65
+ return SchemaVersionListResponse(
66
+ versions=versions,
67
+ total=len(versions),
68
+ source_id=source_id,
69
+ )
70
+
71
+
72
+ @router.get(
73
+ "/{source_id}/schema/versions/{version_id}",
74
+ response_model=SchemaVersionResponse,
75
+ summary="Get schema version",
76
+ description="Get a specific schema version by ID.",
77
+ )
78
+ async def get_schema_version(
79
+ source_id: str,
80
+ version_id: str,
81
+ evolution_service: SchemaEvolutionServiceDep,
82
+ source_service: SourceServiceDep,
83
+ ) -> SchemaVersionResponse:
84
+ """Get a specific schema version.
85
+
86
+ Args:
87
+ source_id: Source ID.
88
+ version_id: Version ID.
89
+ evolution_service: Schema evolution service.
90
+ source_service: Source service.
91
+
92
+ Returns:
93
+ Schema version details.
94
+ """
95
+ # Verify source exists
96
+ source = await source_service.get(source_id)
97
+ if not source:
98
+ raise HTTPException(
99
+ status_code=status.HTTP_404_NOT_FOUND,
100
+ detail=f"Source {source_id} not found",
101
+ )
102
+
103
+ version = await evolution_service.get_version(version_id)
104
+ if not version:
105
+ raise HTTPException(
106
+ status_code=status.HTTP_404_NOT_FOUND,
107
+ detail=f"Version {version_id} not found",
108
+ )
109
+
110
+ return version
111
+
112
+
113
+ @router.get(
114
+ "/{source_id}/schema/changes",
115
+ response_model=SchemaChangeListResponse,
116
+ summary="List schema changes",
117
+ description="Get schema change history for a source.",
118
+ )
119
+ async def list_schema_changes(
120
+ source_id: str,
121
+ evolution_service: SchemaEvolutionServiceDep,
122
+ source_service: SourceServiceDep,
123
+ limit: int = 50,
124
+ offset: int = 0,
125
+ ) -> SchemaChangeListResponse:
126
+ """List schema changes for a source.
127
+
128
+ Args:
129
+ source_id: Source ID.
130
+ evolution_service: Schema evolution service.
131
+ source_service: Source service.
132
+ limit: Maximum changes to return.
133
+ offset: Number to skip.
134
+
135
+ Returns:
136
+ List of schema changes.
137
+ """
138
+ # Verify source exists
139
+ source = await source_service.get(source_id)
140
+ if not source:
141
+ raise HTTPException(
142
+ status_code=status.HTTP_404_NOT_FOUND,
143
+ detail=f"Source {source_id} not found",
144
+ )
145
+
146
+ changes = await evolution_service.get_changes(
147
+ source_id, limit=limit, offset=offset
148
+ )
149
+
150
+ return SchemaChangeListResponse(
151
+ changes=changes,
152
+ total=len(changes),
153
+ source_id=source_id,
154
+ )
155
+
156
+
157
+ @router.post(
158
+ "/{source_id}/schema/detect-changes",
159
+ response_model=SchemaEvolutionResponse,
160
+ summary="Detect schema changes",
161
+ description="Manually trigger schema change detection.",
162
+ )
163
+ async def detect_schema_changes(
164
+ source_id: str,
165
+ evolution_service: SchemaEvolutionServiceDep,
166
+ schema_service: SchemaServiceDep,
167
+ source_service: SourceServiceDep,
168
+ ) -> SchemaEvolutionResponse:
169
+ """Detect schema changes for a source.
170
+
171
+ Args:
172
+ source_id: Source ID.
173
+ evolution_service: Schema evolution service.
174
+ schema_service: Schema service.
175
+ source_service: Source service.
176
+
177
+ Returns:
178
+ Schema evolution detection result.
179
+ """
180
+ # Verify source exists
181
+ source = await source_service.get(source_id)
182
+ if not source:
183
+ raise HTTPException(
184
+ status_code=status.HTTP_404_NOT_FOUND,
185
+ detail=f"Source {source_id} not found",
186
+ )
187
+
188
+ # Get current schema
189
+ schema = await schema_service.get_active(source_id)
190
+ if not schema:
191
+ raise HTTPException(
192
+ status_code=status.HTTP_404_NOT_FOUND,
193
+ detail=f"No active schema found for source {source_id}",
194
+ )
195
+
196
+ # Detect changes
197
+ result = await evolution_service.detect_changes(source, schema)
198
+
199
+ return result
200
+
201
+
202
+ @router.get(
203
+ "/{source_id}/schema/evolution/summary",
204
+ response_model=SchemaEvolutionSummary,
205
+ summary="Get evolution summary",
206
+ description="Get schema evolution summary for a source.",
207
+ )
208
+ async def get_evolution_summary(
209
+ source_id: str,
210
+ evolution_service: SchemaEvolutionServiceDep,
211
+ source_service: SourceServiceDep,
212
+ ) -> SchemaEvolutionSummary:
213
+ """Get schema evolution summary for a source.
214
+
215
+ Args:
216
+ source_id: Source ID.
217
+ evolution_service: Schema evolution service.
218
+ source_service: Source service.
219
+
220
+ Returns:
221
+ Evolution summary.
222
+ """
223
+ # Verify source exists
224
+ source = await source_service.get(source_id)
225
+ if not source:
226
+ raise HTTPException(
227
+ status_code=status.HTTP_404_NOT_FOUND,
228
+ detail=f"Source {source_id} not found",
229
+ )
230
+
231
+ return await evolution_service.get_evolution_summary(source_id)