truthound-dashboard 1.3.1__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.
- 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.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.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.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
"""Profile API endpoints.
|
|
2
2
|
|
|
3
|
-
This module provides endpoints for data profiling.
|
|
3
|
+
This module provides endpoints for data profiling and comparison.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from typing import Annotated
|
|
9
9
|
|
|
10
|
-
from fastapi import APIRouter, HTTPException, Path
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Path, Query
|
|
11
11
|
|
|
12
|
-
from truthound_dashboard.schemas import
|
|
12
|
+
from truthound_dashboard.schemas import (
|
|
13
|
+
LatestComparisonResponse,
|
|
14
|
+
ProfileComparisonRequest,
|
|
15
|
+
ProfileComparisonResponse,
|
|
16
|
+
ProfileListResponse,
|
|
17
|
+
ProfileRequest,
|
|
18
|
+
ProfileResponse,
|
|
19
|
+
ProfileTrendRequest,
|
|
20
|
+
ProfileTrendResponse,
|
|
21
|
+
)
|
|
13
22
|
|
|
14
|
-
from .deps import ProfileServiceDep, SourceServiceDep
|
|
23
|
+
from .deps import ProfileComparisonServiceDep, ProfileServiceDep, SourceServiceDep
|
|
15
24
|
|
|
16
25
|
router = APIRouter()
|
|
17
26
|
|
|
@@ -20,7 +29,7 @@ router = APIRouter()
|
|
|
20
29
|
"/sources/{source_id}/profile",
|
|
21
30
|
response_model=ProfileResponse,
|
|
22
31
|
summary="Profile source",
|
|
23
|
-
description="Run data profiling on a source with optional sampling",
|
|
32
|
+
description="Run data profiling on a source with optional sampling and pattern detection",
|
|
24
33
|
)
|
|
25
34
|
async def profile_source(
|
|
26
35
|
service: ProfileServiceDep,
|
|
@@ -30,31 +39,219 @@ async def profile_source(
|
|
|
30
39
|
) -> ProfileResponse:
|
|
31
40
|
"""Run data profiling on a source.
|
|
32
41
|
|
|
42
|
+
Supports advanced configuration including:
|
|
43
|
+
- Sampling strategies: none, head, random, systematic, stratified, reservoir, adaptive, hash
|
|
44
|
+
- Pattern detection: email, phone, uuid, url, ip_address, credit_card, etc.
|
|
45
|
+
- Statistical analysis options: histograms, correlations, cardinality
|
|
46
|
+
|
|
33
47
|
Args:
|
|
34
48
|
service: Injected profile service.
|
|
35
49
|
source_service: Injected source service.
|
|
36
50
|
source_id: Source to profile.
|
|
37
|
-
request: Optional profiling configuration
|
|
51
|
+
request: Optional profiling configuration.
|
|
38
52
|
|
|
39
53
|
Returns:
|
|
40
|
-
Profiling result with column statistics.
|
|
54
|
+
Profiling result with column statistics, detected patterns, and sampling metadata.
|
|
41
55
|
|
|
42
56
|
Raises:
|
|
43
|
-
HTTPException: 404 if source not found.
|
|
57
|
+
HTTPException: 404 if source not found, 500 on profiling error.
|
|
44
58
|
"""
|
|
45
59
|
# Verify source exists
|
|
46
60
|
source = await source_service.get_by_id(source_id)
|
|
47
61
|
if source is None:
|
|
48
62
|
raise HTTPException(status_code=404, detail="Source not found")
|
|
49
63
|
|
|
50
|
-
#
|
|
51
|
-
|
|
64
|
+
# Build profiling kwargs from request
|
|
65
|
+
profile_kwargs: dict = {}
|
|
66
|
+
|
|
67
|
+
if request:
|
|
68
|
+
# Handle sampling configuration
|
|
69
|
+
if request.sampling:
|
|
70
|
+
# Advanced sampling config takes precedence
|
|
71
|
+
profile_kwargs["sampling_strategy"] = request.sampling.strategy
|
|
72
|
+
profile_kwargs["sample_size"] = request.sampling.sample_size
|
|
73
|
+
profile_kwargs["confidence_level"] = request.sampling.confidence_level
|
|
74
|
+
profile_kwargs["margin_of_error"] = request.sampling.margin_of_error
|
|
75
|
+
profile_kwargs["strata_column"] = request.sampling.strata_column
|
|
76
|
+
profile_kwargs["seed"] = request.sampling.seed
|
|
77
|
+
elif request.sample_size:
|
|
78
|
+
# Backward compatible simple sample_size
|
|
79
|
+
profile_kwargs["sample_size"] = request.sample_size
|
|
80
|
+
|
|
81
|
+
# Handle pattern detection configuration
|
|
82
|
+
if request.pattern_detection:
|
|
83
|
+
profile_kwargs["enable_pattern_detection"] = request.pattern_detection.enabled
|
|
84
|
+
profile_kwargs["pattern_sample_size"] = request.pattern_detection.sample_size
|
|
85
|
+
profile_kwargs["min_pattern_confidence"] = request.pattern_detection.min_confidence
|
|
86
|
+
profile_kwargs["patterns_to_detect"] = request.pattern_detection.patterns_to_detect
|
|
87
|
+
|
|
88
|
+
# Additional profiling options
|
|
89
|
+
profile_kwargs["include_histograms"] = request.include_histograms
|
|
90
|
+
profile_kwargs["include_correlations"] = request.include_correlations
|
|
91
|
+
profile_kwargs["include_cardinality"] = request.include_cardinality
|
|
52
92
|
|
|
53
93
|
try:
|
|
54
|
-
result = await service.profile_source(
|
|
55
|
-
source_id,
|
|
56
|
-
sample_size=sample_size,
|
|
57
|
-
)
|
|
94
|
+
result = await service.profile_source(source_id, **profile_kwargs)
|
|
58
95
|
return ProfileResponse.from_result(result)
|
|
59
96
|
except Exception as e:
|
|
60
97
|
raise HTTPException(status_code=500, detail=str(e))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# =============================================================================
|
|
101
|
+
# Profile History and Comparison Endpoints
|
|
102
|
+
# =============================================================================
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.get(
|
|
106
|
+
"/sources/{source_id}/profiles",
|
|
107
|
+
response_model=ProfileListResponse,
|
|
108
|
+
summary="List profiles",
|
|
109
|
+
description="Get profile history for a source.",
|
|
110
|
+
)
|
|
111
|
+
async def list_profiles(
|
|
112
|
+
source_id: Annotated[str, Path(description="Source ID")],
|
|
113
|
+
comparison_service: ProfileComparisonServiceDep,
|
|
114
|
+
source_service: SourceServiceDep,
|
|
115
|
+
limit: int = Query(default=20, ge=1, le=100, description="Maximum profiles to return"),
|
|
116
|
+
offset: int = Query(default=0, ge=0, description="Number to skip"),
|
|
117
|
+
) -> ProfileListResponse:
|
|
118
|
+
"""List profile history for a source.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
source_id: Source ID.
|
|
122
|
+
comparison_service: Profile comparison service.
|
|
123
|
+
source_service: Source service.
|
|
124
|
+
limit: Maximum profiles to return.
|
|
125
|
+
offset: Number to skip.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of profile summaries.
|
|
129
|
+
"""
|
|
130
|
+
# Verify source exists
|
|
131
|
+
source = await source_service.get_by_id(source_id)
|
|
132
|
+
if source is None:
|
|
133
|
+
raise HTTPException(status_code=404, detail="Source not found")
|
|
134
|
+
|
|
135
|
+
profiles = await comparison_service.list_profiles(
|
|
136
|
+
source_id, limit=limit, offset=offset
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return ProfileListResponse(
|
|
140
|
+
profiles=profiles,
|
|
141
|
+
total=len(profiles),
|
|
142
|
+
source_id=source_id,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.post(
|
|
147
|
+
"/profiles/compare",
|
|
148
|
+
response_model=ProfileComparisonResponse,
|
|
149
|
+
summary="Compare profiles",
|
|
150
|
+
description="Compare two specific profiles.",
|
|
151
|
+
)
|
|
152
|
+
async def compare_profiles(
|
|
153
|
+
request: ProfileComparisonRequest,
|
|
154
|
+
comparison_service: ProfileComparisonServiceDep,
|
|
155
|
+
profile_service: ProfileServiceDep,
|
|
156
|
+
source_service: SourceServiceDep,
|
|
157
|
+
) -> ProfileComparisonResponse:
|
|
158
|
+
"""Compare two profiles.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
request: Comparison request with profile IDs.
|
|
162
|
+
comparison_service: Profile comparison service.
|
|
163
|
+
profile_service: Profile service.
|
|
164
|
+
source_service: Source service.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Profile comparison result.
|
|
168
|
+
"""
|
|
169
|
+
# Get baseline profile to determine source
|
|
170
|
+
baseline = await profile_service.get(request.baseline_profile_id)
|
|
171
|
+
if baseline is None:
|
|
172
|
+
raise HTTPException(
|
|
173
|
+
status_code=404,
|
|
174
|
+
detail=f"Baseline profile {request.baseline_profile_id} not found",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Get source
|
|
178
|
+
source = await source_service.get_by_id(baseline.source_id)
|
|
179
|
+
if source is None:
|
|
180
|
+
raise HTTPException(status_code=404, detail="Source not found")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
result = await comparison_service.compare_profiles(
|
|
184
|
+
source,
|
|
185
|
+
request.baseline_profile_id,
|
|
186
|
+
request.current_profile_id,
|
|
187
|
+
significance_threshold=request.significance_threshold,
|
|
188
|
+
)
|
|
189
|
+
return result
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@router.get(
|
|
195
|
+
"/sources/{source_id}/profiles/trend",
|
|
196
|
+
response_model=ProfileTrendResponse,
|
|
197
|
+
summary="Get profile trends",
|
|
198
|
+
description="Get time-series profile trends for a source.",
|
|
199
|
+
)
|
|
200
|
+
async def get_profile_trend(
|
|
201
|
+
source_id: Annotated[str, Path(description="Source ID")],
|
|
202
|
+
comparison_service: ProfileComparisonServiceDep,
|
|
203
|
+
source_service: SourceServiceDep,
|
|
204
|
+
period: str = Query(default="30d", description="Time period (e.g., 7d, 30d, 90d)"),
|
|
205
|
+
granularity: str = Query(default="daily", description="Data granularity"),
|
|
206
|
+
) -> ProfileTrendResponse:
|
|
207
|
+
"""Get profile trends for a source.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
source_id: Source ID.
|
|
211
|
+
comparison_service: Profile comparison service.
|
|
212
|
+
source_service: Source service.
|
|
213
|
+
period: Time period to analyze.
|
|
214
|
+
granularity: Data granularity.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Profile trend data.
|
|
218
|
+
"""
|
|
219
|
+
# Verify source exists
|
|
220
|
+
source = await source_service.get_by_id(source_id)
|
|
221
|
+
if source is None:
|
|
222
|
+
raise HTTPException(status_code=404, detail="Source not found")
|
|
223
|
+
|
|
224
|
+
result = await comparison_service.get_profile_trend(
|
|
225
|
+
source, period=period, granularity=granularity
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@router.get(
|
|
232
|
+
"/sources/{source_id}/profiles/latest-comparison",
|
|
233
|
+
response_model=LatestComparisonResponse,
|
|
234
|
+
summary="Compare latest profiles",
|
|
235
|
+
description="Compare the latest profile with the previous one.",
|
|
236
|
+
)
|
|
237
|
+
async def get_latest_profile_comparison(
|
|
238
|
+
source_id: Annotated[str, Path(description="Source ID")],
|
|
239
|
+
comparison_service: ProfileComparisonServiceDep,
|
|
240
|
+
source_service: SourceServiceDep,
|
|
241
|
+
) -> LatestComparisonResponse:
|
|
242
|
+
"""Compare latest profile with previous.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
source_id: Source ID.
|
|
246
|
+
comparison_service: Profile comparison service.
|
|
247
|
+
source_service: Source service.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Latest comparison result.
|
|
251
|
+
"""
|
|
252
|
+
# Verify source exists
|
|
253
|
+
source = await source_service.get_by_id(source_id)
|
|
254
|
+
if source is None:
|
|
255
|
+
raise HTTPException(status_code=404, detail="Source not found")
|
|
256
|
+
|
|
257
|
+
return await comparison_service.get_latest_comparison(source)
|