truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.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)
|