truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.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 +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +645 -23
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +15 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -206,8 +206,6 @@ async def generate_validation_report(
|
|
|
206
206
|
"text/html": {},
|
|
207
207
|
"text/csv": {},
|
|
208
208
|
"application/json": {},
|
|
209
|
-
"text/markdown": {},
|
|
210
|
-
"application/pdf": {},
|
|
211
209
|
},
|
|
212
210
|
},
|
|
213
211
|
404: {"description": "Validation not found"},
|
|
@@ -237,7 +235,7 @@ async def download_validation_report(
|
|
|
237
235
|
Args:
|
|
238
236
|
service: Injected validation service.
|
|
239
237
|
validation_id: Validation to generate report for.
|
|
240
|
-
format: Report format (html, csv, json
|
|
238
|
+
format: Report format (html, csv, json).
|
|
241
239
|
theme: Visual theme for the report.
|
|
242
240
|
locale: Report language code.
|
|
243
241
|
include_samples: Include sample problematic values.
|
|
@@ -250,8 +248,8 @@ async def download_validation_report(
|
|
|
250
248
|
HTTPException: 404 if validation not found.
|
|
251
249
|
HTTPException: 400 if format or locale is not supported.
|
|
252
250
|
"""
|
|
253
|
-
# Get validation
|
|
254
|
-
validation = await service.get_validation(validation_id)
|
|
251
|
+
# Get validation with source eagerly loaded (required for report generation)
|
|
252
|
+
validation = await service.get_validation(validation_id, with_source=True)
|
|
255
253
|
if validation is None:
|
|
256
254
|
raise HTTPException(status_code=404, detail="Validation not found")
|
|
257
255
|
|
|
@@ -295,7 +293,6 @@ async def download_validation_report(
|
|
|
295
293
|
"text/html": {},
|
|
296
294
|
"text/csv": {},
|
|
297
295
|
"application/json": {},
|
|
298
|
-
"text/markdown": {},
|
|
299
296
|
},
|
|
300
297
|
},
|
|
301
298
|
404: {"description": "Validation not found"},
|
|
@@ -320,7 +317,7 @@ async def preview_validation_report(
|
|
|
320
317
|
Args:
|
|
321
318
|
service: Injected validation service.
|
|
322
319
|
validation_id: Validation to generate report for.
|
|
323
|
-
format: Report format (html, csv, json
|
|
320
|
+
format: Report format (html, csv, json).
|
|
324
321
|
theme: Visual theme for the report.
|
|
325
322
|
locale: Report language code.
|
|
326
323
|
|
|
@@ -331,8 +328,8 @@ async def preview_validation_report(
|
|
|
331
328
|
HTTPException: 404 if validation not found.
|
|
332
329
|
HTTPException: 400 if format or locale is not supported.
|
|
333
330
|
"""
|
|
334
|
-
# Get validation
|
|
335
|
-
validation = await service.get_validation(validation_id)
|
|
331
|
+
# Get validation with source eagerly loaded (required for report generation)
|
|
332
|
+
validation = await service.get_validation(validation_id, with_source=True)
|
|
336
333
|
if validation is None:
|
|
337
334
|
raise HTTPException(status_code=404, detail="Validation not found")
|
|
338
335
|
|
|
@@ -393,7 +390,7 @@ async def list_report_history(
|
|
|
393
390
|
source_id: Filter by source ID.
|
|
394
391
|
validation_id: Filter by validation ID.
|
|
395
392
|
reporter_id: Filter by reporter ID.
|
|
396
|
-
format: Filter by format (html,
|
|
393
|
+
format: Filter by format (html, csv, json).
|
|
397
394
|
status: Filter by status (pending, generating, completed, failed, expired).
|
|
398
395
|
include_expired: Include expired reports (default: false).
|
|
399
396
|
search: Search by report name.
|
|
@@ -602,8 +599,6 @@ async def delete_report_record(
|
|
|
602
599
|
"text/html": {},
|
|
603
600
|
"text/csv": {},
|
|
604
601
|
"application/json": {},
|
|
605
|
-
"text/markdown": {},
|
|
606
|
-
"application/pdf": {},
|
|
607
602
|
},
|
|
608
603
|
},
|
|
609
604
|
404: {"description": "Report not found or file missing"},
|
|
@@ -645,12 +640,8 @@ async def download_saved_report(
|
|
|
645
640
|
# Build filename
|
|
646
641
|
ext_map = {
|
|
647
642
|
"html": ".html",
|
|
648
|
-
"pdf": ".pdf",
|
|
649
643
|
"csv": ".csv",
|
|
650
644
|
"json": ".json",
|
|
651
|
-
"markdown": ".md",
|
|
652
|
-
"junit": ".xml",
|
|
653
|
-
"excel": ".xlsx",
|
|
654
645
|
}
|
|
655
646
|
fmt = report.format.value if hasattr(report.format, "value") else report.format
|
|
656
647
|
ext = ext_map.get(fmt, ".html")
|
|
@@ -9,6 +9,7 @@ from . import (
|
|
|
9
9
|
# Phase 1-4
|
|
10
10
|
alerts,
|
|
11
11
|
drift,
|
|
12
|
+
drift_monitor,
|
|
12
13
|
health,
|
|
13
14
|
history,
|
|
14
15
|
maintenance,
|
|
@@ -16,6 +17,7 @@ from . import (
|
|
|
16
17
|
notifications,
|
|
17
18
|
notifications_advanced,
|
|
18
19
|
profile,
|
|
20
|
+
quality_reporter,
|
|
19
21
|
reports,
|
|
20
22
|
rules,
|
|
21
23
|
scan,
|
|
@@ -34,6 +36,7 @@ from . import (
|
|
|
34
36
|
# Schema Evolution & Rule Suggestions
|
|
35
37
|
rule_suggestions,
|
|
36
38
|
schema_evolution,
|
|
39
|
+
schema_watcher,
|
|
37
40
|
# Phase 9: Plugin System
|
|
38
41
|
plugins,
|
|
39
42
|
# Phase 10: ML & Lineage
|
|
@@ -42,6 +45,12 @@ from . import (
|
|
|
42
45
|
model_monitoring,
|
|
43
46
|
# Cross-Feature Integration
|
|
44
47
|
cross_alerts,
|
|
48
|
+
# Storage Tiering (truthound 1.2.10+)
|
|
49
|
+
tiering,
|
|
50
|
+
# Enterprise Sampling (truthound 1.2.10+)
|
|
51
|
+
enterprise_sampling,
|
|
52
|
+
# Observability (truthound store observability)
|
|
53
|
+
observability,
|
|
45
54
|
)
|
|
46
55
|
|
|
47
56
|
api_router = APIRouter()
|
|
@@ -102,6 +111,12 @@ api_router.include_router(
|
|
|
102
111
|
tags=["drift"],
|
|
103
112
|
)
|
|
104
113
|
|
|
114
|
+
# Drift monitoring endpoints
|
|
115
|
+
api_router.include_router(
|
|
116
|
+
drift_monitor.router,
|
|
117
|
+
tags=["drift-monitor"],
|
|
118
|
+
)
|
|
119
|
+
|
|
105
120
|
# PII scan endpoints
|
|
106
121
|
api_router.include_router(
|
|
107
122
|
scan.router,
|
|
@@ -279,3 +294,54 @@ api_router.include_router(
|
|
|
279
294
|
plugins.router,
|
|
280
295
|
tags=["plugins"],
|
|
281
296
|
)
|
|
297
|
+
|
|
298
|
+
# =============================================================================
|
|
299
|
+
# Storage Tiering (truthound 1.2.10+)
|
|
300
|
+
# =============================================================================
|
|
301
|
+
|
|
302
|
+
# Storage tiering endpoints (tiers, policies, configs, migrations)
|
|
303
|
+
api_router.include_router(
|
|
304
|
+
tiering.router,
|
|
305
|
+
tags=["tiering"],
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# =============================================================================
|
|
309
|
+
# Quality Reporter (truthound 1.2.10+)
|
|
310
|
+
# =============================================================================
|
|
311
|
+
|
|
312
|
+
# Quality scoring and reporting endpoints
|
|
313
|
+
api_router.include_router(
|
|
314
|
+
quality_reporter.router,
|
|
315
|
+
tags=["quality-reporter"],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# =============================================================================
|
|
319
|
+
# Schema Watcher (truthound 1.2.10+)
|
|
320
|
+
# =============================================================================
|
|
321
|
+
|
|
322
|
+
# Schema watcher endpoints for continuous schema monitoring
|
|
323
|
+
api_router.include_router(
|
|
324
|
+
schema_watcher.router,
|
|
325
|
+
tags=["schema-watcher"],
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# =============================================================================
|
|
329
|
+
# Enterprise Sampling (truthound 1.2.10+)
|
|
330
|
+
# =============================================================================
|
|
331
|
+
|
|
332
|
+
# Enterprise sampling endpoints (block, multi-stage, column-aware, progressive)
|
|
333
|
+
api_router.include_router(
|
|
334
|
+
enterprise_sampling.router,
|
|
335
|
+
tags=["enterprise-sampling"],
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# =============================================================================
|
|
339
|
+
# Observability (truthound store observability)
|
|
340
|
+
# =============================================================================
|
|
341
|
+
|
|
342
|
+
# Observability endpoints (audit, metrics, tracing)
|
|
343
|
+
api_router.include_router(
|
|
344
|
+
observability.router,
|
|
345
|
+
prefix="/observability",
|
|
346
|
+
tags=["observability"],
|
|
347
|
+
)
|
|
@@ -72,7 +72,7 @@ async def suggest_rules(
|
|
|
72
72
|
Rule suggestion response with single-column and cross-column suggestions.
|
|
73
73
|
"""
|
|
74
74
|
# Verify source exists
|
|
75
|
-
source = await source_service.
|
|
75
|
+
source = await source_service.get_by_id(source_id)
|
|
76
76
|
if not source:
|
|
77
77
|
raise HTTPException(
|
|
78
78
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -100,7 +100,7 @@ async def suggest_rules(
|
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
# Get schema (optional)
|
|
103
|
-
schema = await schema_service.
|
|
103
|
+
schema = await schema_service.get_schema(source_id)
|
|
104
104
|
|
|
105
105
|
# Generate suggestions with advanced options including cross-column
|
|
106
106
|
result = await generator_service.generate_suggestions(
|
|
@@ -144,7 +144,7 @@ async def apply_rule_suggestions(
|
|
|
144
144
|
Apply rules response.
|
|
145
145
|
"""
|
|
146
146
|
# Verify source exists
|
|
147
|
-
source = await source_service.
|
|
147
|
+
source = await source_service.get_by_id(source_id)
|
|
148
148
|
if not source:
|
|
149
149
|
raise HTTPException(
|
|
150
150
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -200,7 +200,7 @@ async def export_rules(
|
|
|
200
200
|
Export response with content.
|
|
201
201
|
"""
|
|
202
202
|
# Verify source exists
|
|
203
|
-
source = await source_service.
|
|
203
|
+
source = await source_service.get_by_id(source_id)
|
|
204
204
|
if not source:
|
|
205
205
|
raise HTTPException(
|
|
206
206
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -250,7 +250,7 @@ async def download_exported_rules(
|
|
|
250
250
|
Plain text response with file content.
|
|
251
251
|
"""
|
|
252
252
|
# Verify source exists
|
|
253
|
-
source = await source_service.
|
|
253
|
+
source = await source_service.get_by_id(source_id)
|
|
254
254
|
if not source:
|
|
255
255
|
raise HTTPException(
|
|
256
256
|
status_code=status.HTTP_404_NOT_FOUND,
|
truthound_dashboard/api/scan.py
CHANGED
|
@@ -27,24 +27,27 @@ router = APIRouter()
|
|
|
27
27
|
"/sources/{source_id}/scan",
|
|
28
28
|
response_model=PIIScanResponse,
|
|
29
29
|
summary="Run PII scan",
|
|
30
|
-
description="
|
|
30
|
+
description="""
|
|
31
|
+
Scan data source for personally identifiable information (PII).
|
|
32
|
+
|
|
33
|
+
Note: truthound's th.scan() does not support configuration parameters.
|
|
34
|
+
The scan runs on all columns with default settings.
|
|
35
|
+
""",
|
|
31
36
|
)
|
|
32
37
|
async def run_pii_scan(
|
|
33
38
|
service: PIIScanServiceDep,
|
|
34
39
|
source_id: Annotated[str, Path(description="Source ID to scan")],
|
|
35
|
-
request: PIIScanRequest,
|
|
40
|
+
request: PIIScanRequest | None = None,
|
|
36
41
|
) -> PIIScanResponse:
|
|
37
42
|
"""Run PII scan on a data source.
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- regulations: Privacy regulations to check (gdpr, ccpa, lgpd)
|
|
42
|
-
- min_confidence: Confidence threshold for PII detection
|
|
44
|
+
Note: truthound's th.scan() does not support configuration parameters.
|
|
45
|
+
The scan runs on all columns with default settings.
|
|
43
46
|
|
|
44
47
|
Args:
|
|
45
48
|
service: Injected PII scan service.
|
|
46
49
|
source_id: Source to scan.
|
|
47
|
-
request:
|
|
50
|
+
request: Optional request body (not used, kept for API compatibility).
|
|
48
51
|
|
|
49
52
|
Returns:
|
|
50
53
|
PII scan result with findings and violations.
|
|
@@ -53,12 +56,7 @@ async def run_pii_scan(
|
|
|
53
56
|
HTTPException: 404 if source not found.
|
|
54
57
|
"""
|
|
55
58
|
try:
|
|
56
|
-
scan = await service.run_scan(
|
|
57
|
-
source_id,
|
|
58
|
-
columns=request.columns,
|
|
59
|
-
regulations=request.regulations,
|
|
60
|
-
min_confidence=request.min_confidence,
|
|
61
|
-
)
|
|
59
|
+
scan = await service.run_scan(source_id)
|
|
62
60
|
return PIIScanResponse.from_model(scan)
|
|
63
61
|
except ValueError as e:
|
|
64
62
|
raise HTTPException(status_code=404, detail=str(e))
|
|
@@ -134,15 +132,15 @@ async def list_source_pii_scans(
|
|
|
134
132
|
|
|
135
133
|
@router.get(
|
|
136
134
|
"/sources/{source_id}/scans/latest",
|
|
137
|
-
response_model=PIIScanResponse,
|
|
135
|
+
response_model=PIIScanResponse | None,
|
|
138
136
|
summary="Get latest PII scan",
|
|
139
|
-
description="Get the most recent PII scan for a source",
|
|
137
|
+
description="Get the most recent PII scan for a source, or null if no scans exist",
|
|
140
138
|
)
|
|
141
139
|
async def get_latest_pii_scan(
|
|
142
140
|
service: PIIScanServiceDep,
|
|
143
141
|
source_service: SourceServiceDep,
|
|
144
142
|
source_id: Annotated[str, Path(description="Source ID")],
|
|
145
|
-
) -> PIIScanResponse:
|
|
143
|
+
) -> PIIScanResponse | None:
|
|
146
144
|
"""Get the most recent PII scan for a source.
|
|
147
145
|
|
|
148
146
|
Args:
|
|
@@ -151,10 +149,10 @@ async def get_latest_pii_scan(
|
|
|
151
149
|
source_id: Source to get latest scan for.
|
|
152
150
|
|
|
153
151
|
Returns:
|
|
154
|
-
Latest PII scan result.
|
|
152
|
+
Latest PII scan result, or null if no scans exist.
|
|
155
153
|
|
|
156
154
|
Raises:
|
|
157
|
-
HTTPException: 404 if source
|
|
155
|
+
HTTPException: 404 if source not found.
|
|
158
156
|
"""
|
|
159
157
|
# Verify source exists
|
|
160
158
|
source = await source_service.get_by_id(source_id)
|
|
@@ -163,6 +161,6 @@ async def get_latest_pii_scan(
|
|
|
163
161
|
|
|
164
162
|
scan = await service.get_latest_for_source(source_id)
|
|
165
163
|
if scan is None:
|
|
166
|
-
|
|
164
|
+
return None
|
|
167
165
|
|
|
168
166
|
return PIIScanResponse.from_model(scan)
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""Schedule management API endpoints.
|
|
2
2
|
|
|
3
3
|
Provides CRUD endpoints for validation schedules.
|
|
4
|
+
|
|
5
|
+
API Design: Direct Response Style
|
|
6
|
+
- Single resources return the resource directly
|
|
7
|
+
- List endpoints return PaginatedResponse with data, total, offset, limit
|
|
8
|
+
- Errors are handled via HTTPException
|
|
4
9
|
"""
|
|
5
10
|
|
|
6
11
|
from __future__ import annotations
|
|
@@ -11,11 +16,14 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
11
16
|
|
|
12
17
|
from truthound_dashboard.core import ScheduleService, ValidationService
|
|
13
18
|
from truthound_dashboard.schemas import (
|
|
19
|
+
MessageResponse,
|
|
14
20
|
ScheduleActionResponse,
|
|
15
21
|
ScheduleCreate,
|
|
16
22
|
ScheduleListResponse,
|
|
23
|
+
ScheduleResponse,
|
|
17
24
|
ScheduleUpdate,
|
|
18
25
|
)
|
|
26
|
+
from truthound_dashboard.schemas.schedule import ScheduleListItem
|
|
19
27
|
|
|
20
28
|
from .deps import SessionDep
|
|
21
29
|
|
|
@@ -30,25 +38,49 @@ async def get_schedule_service(session: SessionDep) -> ScheduleService:
|
|
|
30
38
|
ScheduleServiceDep = Annotated[ScheduleService, Depends(get_schedule_service)]
|
|
31
39
|
|
|
32
40
|
|
|
33
|
-
def _schedule_to_response(schedule) ->
|
|
34
|
-
"""Convert schedule model to response
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
41
|
+
def _schedule_to_response(schedule) -> ScheduleResponse:
|
|
42
|
+
"""Convert schedule model to response schema."""
|
|
43
|
+
return ScheduleResponse(
|
|
44
|
+
id=schedule.id,
|
|
45
|
+
name=schedule.name,
|
|
46
|
+
source_id=schedule.source_id,
|
|
47
|
+
cron_expression=schedule.cron_expression,
|
|
48
|
+
trigger_type=getattr(schedule, "trigger_type", None) or "cron",
|
|
49
|
+
trigger_config=getattr(schedule, "trigger_config", None),
|
|
50
|
+
is_active=schedule.is_active,
|
|
51
|
+
notify_on_failure=schedule.notify_on_failure,
|
|
52
|
+
last_run_at=(
|
|
43
53
|
schedule.last_run_at.isoformat() if schedule.last_run_at else None
|
|
44
54
|
),
|
|
45
|
-
|
|
55
|
+
next_run_at=(
|
|
46
56
|
schedule.next_run_at.isoformat() if schedule.next_run_at else None
|
|
47
57
|
),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
config=schedule.config,
|
|
59
|
+
created_at=schedule.created_at,
|
|
60
|
+
updated_at=schedule.updated_at,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _schedule_to_list_item(schedule) -> ScheduleListItem:
|
|
65
|
+
"""Convert schedule model to list item schema."""
|
|
66
|
+
return ScheduleListItem(
|
|
67
|
+
id=schedule.id,
|
|
68
|
+
name=schedule.name,
|
|
69
|
+
source_id=schedule.source_id,
|
|
70
|
+
cron_expression=schedule.cron_expression,
|
|
71
|
+
trigger_type=getattr(schedule, "trigger_type", None) or "cron",
|
|
72
|
+
trigger_config=getattr(schedule, "trigger_config", None),
|
|
73
|
+
is_active=schedule.is_active,
|
|
74
|
+
notify_on_failure=schedule.notify_on_failure,
|
|
75
|
+
last_run_at=(
|
|
76
|
+
schedule.last_run_at.isoformat() if schedule.last_run_at else None
|
|
77
|
+
),
|
|
78
|
+
next_run_at=(
|
|
79
|
+
schedule.next_run_at.isoformat() if schedule.next_run_at else None
|
|
80
|
+
),
|
|
81
|
+
created_at=schedule.created_at,
|
|
82
|
+
updated_at=schedule.updated_at,
|
|
83
|
+
)
|
|
52
84
|
|
|
53
85
|
|
|
54
86
|
@router.get(
|
|
@@ -61,6 +93,7 @@ async def list_schedules(
|
|
|
61
93
|
service: ScheduleServiceDep,
|
|
62
94
|
source_id: str | None = Query(None, description="Filter by source ID"),
|
|
63
95
|
active_only: bool = Query(False, description="Only return active schedules"),
|
|
96
|
+
offset: int = Query(0, ge=0, description="Offset for pagination"),
|
|
64
97
|
limit: int = Query(100, ge=1, le=500, description="Maximum results"),
|
|
65
98
|
) -> ScheduleListResponse:
|
|
66
99
|
"""List validation schedules.
|
|
@@ -69,10 +102,11 @@ async def list_schedules(
|
|
|
69
102
|
service: Schedule service.
|
|
70
103
|
source_id: Optional source ID filter.
|
|
71
104
|
active_only: Only return active schedules.
|
|
105
|
+
offset: Offset for pagination.
|
|
72
106
|
limit: Maximum results.
|
|
73
107
|
|
|
74
108
|
Returns:
|
|
75
|
-
|
|
109
|
+
Paginated list of schedules.
|
|
76
110
|
"""
|
|
77
111
|
schedules = await service.list_schedules(
|
|
78
112
|
source_id=source_id,
|
|
@@ -81,15 +115,16 @@ async def list_schedules(
|
|
|
81
115
|
)
|
|
82
116
|
|
|
83
117
|
return ScheduleListResponse(
|
|
84
|
-
|
|
85
|
-
data=[_schedule_to_response(s) for s in schedules],
|
|
118
|
+
data=[_schedule_to_list_item(s) for s in schedules],
|
|
86
119
|
total=len(schedules),
|
|
120
|
+
offset=offset,
|
|
121
|
+
limit=limit,
|
|
87
122
|
)
|
|
88
123
|
|
|
89
124
|
|
|
90
125
|
@router.post(
|
|
91
126
|
"/schedules",
|
|
92
|
-
response_model=
|
|
127
|
+
response_model=ScheduleResponse,
|
|
93
128
|
status_code=201,
|
|
94
129
|
summary="Create schedule",
|
|
95
130
|
description="Create a new validation schedule.",
|
|
@@ -97,7 +132,7 @@ async def list_schedules(
|
|
|
97
132
|
async def create_schedule(
|
|
98
133
|
request: ScheduleCreate,
|
|
99
134
|
service: ScheduleServiceDep,
|
|
100
|
-
) ->
|
|
135
|
+
) -> ScheduleResponse:
|
|
101
136
|
"""Create a new schedule.
|
|
102
137
|
|
|
103
138
|
Args:
|
|
@@ -112,14 +147,12 @@ async def create_schedule(
|
|
|
112
147
|
source_id=request.source_id,
|
|
113
148
|
name=request.name,
|
|
114
149
|
cron_expression=request.cron_expression,
|
|
150
|
+
trigger_type=request.trigger_type,
|
|
151
|
+
trigger_config=request.trigger_config,
|
|
115
152
|
notify_on_failure=request.notify_on_failure,
|
|
116
153
|
config=request.config,
|
|
117
154
|
)
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
"success": True,
|
|
121
|
-
"data": _schedule_to_response(schedule),
|
|
122
|
-
}
|
|
155
|
+
return _schedule_to_response(schedule)
|
|
123
156
|
except ValueError as e:
|
|
124
157
|
raise HTTPException(status_code=400, detail=str(e))
|
|
125
158
|
except Exception as e:
|
|
@@ -128,14 +161,14 @@ async def create_schedule(
|
|
|
128
161
|
|
|
129
162
|
@router.get(
|
|
130
163
|
"/schedules/{schedule_id}",
|
|
131
|
-
response_model=
|
|
164
|
+
response_model=ScheduleResponse,
|
|
132
165
|
summary="Get schedule",
|
|
133
166
|
description="Get a specific schedule by ID.",
|
|
134
167
|
)
|
|
135
168
|
async def get_schedule(
|
|
136
169
|
schedule_id: str,
|
|
137
170
|
service: ScheduleServiceDep,
|
|
138
|
-
) ->
|
|
171
|
+
) -> ScheduleResponse:
|
|
139
172
|
"""Get a schedule by ID.
|
|
140
173
|
|
|
141
174
|
Args:
|
|
@@ -149,15 +182,12 @@ async def get_schedule(
|
|
|
149
182
|
if schedule is None:
|
|
150
183
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
|
151
184
|
|
|
152
|
-
return
|
|
153
|
-
"success": True,
|
|
154
|
-
"data": _schedule_to_response(schedule),
|
|
155
|
-
}
|
|
185
|
+
return _schedule_to_response(schedule)
|
|
156
186
|
|
|
157
187
|
|
|
158
188
|
@router.put(
|
|
159
189
|
"/schedules/{schedule_id}",
|
|
160
|
-
response_model=
|
|
190
|
+
response_model=ScheduleResponse,
|
|
161
191
|
summary="Update schedule",
|
|
162
192
|
description="Update an existing schedule.",
|
|
163
193
|
)
|
|
@@ -165,7 +195,7 @@ async def update_schedule(
|
|
|
165
195
|
schedule_id: str,
|
|
166
196
|
request: ScheduleUpdate,
|
|
167
197
|
service: ScheduleServiceDep,
|
|
168
|
-
) ->
|
|
198
|
+
) -> ScheduleResponse:
|
|
169
199
|
"""Update a schedule.
|
|
170
200
|
|
|
171
201
|
Args:
|
|
@@ -188,24 +218,21 @@ async def update_schedule(
|
|
|
188
218
|
if schedule is None:
|
|
189
219
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
|
190
220
|
|
|
191
|
-
return
|
|
192
|
-
"success": True,
|
|
193
|
-
"data": _schedule_to_response(schedule),
|
|
194
|
-
}
|
|
221
|
+
return _schedule_to_response(schedule)
|
|
195
222
|
except ValueError as e:
|
|
196
223
|
raise HTTPException(status_code=400, detail=str(e))
|
|
197
224
|
|
|
198
225
|
|
|
199
226
|
@router.delete(
|
|
200
227
|
"/schedules/{schedule_id}",
|
|
201
|
-
response_model=
|
|
228
|
+
response_model=MessageResponse,
|
|
202
229
|
summary="Delete schedule",
|
|
203
230
|
description="Delete a schedule.",
|
|
204
231
|
)
|
|
205
232
|
async def delete_schedule(
|
|
206
233
|
schedule_id: str,
|
|
207
234
|
service: ScheduleServiceDep,
|
|
208
|
-
) ->
|
|
235
|
+
) -> MessageResponse:
|
|
209
236
|
"""Delete a schedule.
|
|
210
237
|
|
|
211
238
|
Args:
|
|
@@ -219,7 +246,7 @@ async def delete_schedule(
|
|
|
219
246
|
if not deleted:
|
|
220
247
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
|
221
248
|
|
|
222
|
-
return
|
|
249
|
+
return MessageResponse(message="Schedule deleted")
|
|
223
250
|
|
|
224
251
|
|
|
225
252
|
@router.post(
|
|
@@ -246,7 +273,6 @@ async def pause_schedule(
|
|
|
246
273
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
|
247
274
|
|
|
248
275
|
return ScheduleActionResponse(
|
|
249
|
-
success=True,
|
|
250
276
|
message="Schedule paused",
|
|
251
277
|
schedule=_schedule_to_response(schedule),
|
|
252
278
|
)
|
|
@@ -276,15 +302,25 @@ async def resume_schedule(
|
|
|
276
302
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
|
277
303
|
|
|
278
304
|
return ScheduleActionResponse(
|
|
279
|
-
success=True,
|
|
280
305
|
message="Schedule resumed",
|
|
281
306
|
schedule=_schedule_to_response(schedule),
|
|
282
307
|
)
|
|
283
308
|
|
|
284
309
|
|
|
310
|
+
from pydantic import BaseModel
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ScheduleRunResponse(BaseModel):
|
|
314
|
+
"""Response for schedule run action."""
|
|
315
|
+
|
|
316
|
+
message: str
|
|
317
|
+
validation_id: str
|
|
318
|
+
passed: bool | None = None
|
|
319
|
+
|
|
320
|
+
|
|
285
321
|
@router.post(
|
|
286
322
|
"/schedules/{schedule_id}/run",
|
|
287
|
-
response_model=
|
|
323
|
+
response_model=ScheduleRunResponse,
|
|
288
324
|
summary="Run schedule now",
|
|
289
325
|
description="Trigger immediate execution of a scheduled validation.",
|
|
290
326
|
)
|
|
@@ -292,7 +328,7 @@ async def run_schedule_now(
|
|
|
292
328
|
schedule_id: str,
|
|
293
329
|
service: ScheduleServiceDep,
|
|
294
330
|
session: SessionDep,
|
|
295
|
-
) ->
|
|
331
|
+
) -> ScheduleRunResponse:
|
|
296
332
|
"""Run a scheduled validation immediately.
|
|
297
333
|
|
|
298
334
|
Args:
|
|
@@ -319,11 +355,10 @@ async def run_schedule_now(
|
|
|
319
355
|
auto_schema=config.get("auto_schema", False),
|
|
320
356
|
)
|
|
321
357
|
|
|
322
|
-
return
|
|
323
|
-
"
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
358
|
+
return ScheduleRunResponse(
|
|
359
|
+
message="Validation triggered",
|
|
360
|
+
validation_id=validation.id,
|
|
361
|
+
passed=validation.passed,
|
|
362
|
+
)
|
|
328
363
|
except Exception as e:
|
|
329
364
|
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -51,7 +51,7 @@ async def list_schema_versions(
|
|
|
51
51
|
List of schema versions.
|
|
52
52
|
"""
|
|
53
53
|
# Verify source exists
|
|
54
|
-
source = await source_service.
|
|
54
|
+
source = await source_service.get_by_id(source_id)
|
|
55
55
|
if not source:
|
|
56
56
|
raise HTTPException(
|
|
57
57
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -93,7 +93,7 @@ async def get_schema_version(
|
|
|
93
93
|
Schema version details.
|
|
94
94
|
"""
|
|
95
95
|
# Verify source exists
|
|
96
|
-
source = await source_service.
|
|
96
|
+
source = await source_service.get_by_id(source_id)
|
|
97
97
|
if not source:
|
|
98
98
|
raise HTTPException(
|
|
99
99
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -136,7 +136,7 @@ async def list_schema_changes(
|
|
|
136
136
|
List of schema changes.
|
|
137
137
|
"""
|
|
138
138
|
# Verify source exists
|
|
139
|
-
source = await source_service.
|
|
139
|
+
source = await source_service.get_by_id(source_id)
|
|
140
140
|
if not source:
|
|
141
141
|
raise HTTPException(
|
|
142
142
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -178,7 +178,7 @@ async def detect_schema_changes(
|
|
|
178
178
|
Schema evolution detection result.
|
|
179
179
|
"""
|
|
180
180
|
# Verify source exists
|
|
181
|
-
source = await source_service.
|
|
181
|
+
source = await source_service.get_by_id(source_id)
|
|
182
182
|
if not source:
|
|
183
183
|
raise HTTPException(
|
|
184
184
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -186,7 +186,7 @@ async def detect_schema_changes(
|
|
|
186
186
|
)
|
|
187
187
|
|
|
188
188
|
# Get current schema
|
|
189
|
-
schema = await schema_service.
|
|
189
|
+
schema = await schema_service.get_schema(source_id)
|
|
190
190
|
if not schema:
|
|
191
191
|
raise HTTPException(
|
|
192
192
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -221,7 +221,7 @@ async def get_evolution_summary(
|
|
|
221
221
|
Evolution summary.
|
|
222
222
|
"""
|
|
223
223
|
# Verify source exists
|
|
224
|
-
source = await source_service.
|
|
224
|
+
source = await source_service.get_by_id(source_id)
|
|
225
225
|
if not source:
|
|
226
226
|
raise HTTPException(
|
|
227
227
|
status_code=status.HTTP_404_NOT_FOUND,
|