truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +437 -10
- 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 +11 -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.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.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.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
"""Reporter adapters for external backends.
|
|
2
|
+
|
|
3
|
+
This module provides adapters that wrap external reporter implementations
|
|
4
|
+
(e.g., truthound reporters) to conform to our ReporterProtocol interface.
|
|
5
|
+
|
|
6
|
+
The adapter pattern enables:
|
|
7
|
+
1. Loose coupling with external dependencies
|
|
8
|
+
2. Easy testing with mock implementations
|
|
9
|
+
3. Graceful fallback when external libraries unavailable
|
|
10
|
+
4. Version compatibility across truthound updates
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
from truthound_dashboard.core.reporters.adapters import (
|
|
14
|
+
TruthoundReporterAdapter,
|
|
15
|
+
create_truthound_reporter,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Using the factory function
|
|
19
|
+
reporter = create_truthound_reporter("json")
|
|
20
|
+
output = await reporter.generate(data)
|
|
21
|
+
|
|
22
|
+
# Using the adapter directly
|
|
23
|
+
from truthound.reporters import get_reporter
|
|
24
|
+
th_reporter = get_reporter("json")
|
|
25
|
+
adapter = TruthoundReporterAdapter(th_reporter)
|
|
26
|
+
output = await adapter.generate(data)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import logging
|
|
33
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from functools import partial
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
37
|
+
|
|
38
|
+
from .interfaces import (
|
|
39
|
+
BaseReporter,
|
|
40
|
+
ReportData,
|
|
41
|
+
ReporterConfig,
|
|
42
|
+
ReporterProtocol,
|
|
43
|
+
ReportFormatType,
|
|
44
|
+
ReportOutput,
|
|
45
|
+
ReportThemeType,
|
|
46
|
+
ValidationIssueData,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
# Thread pool for running sync truthound reporters
|
|
55
|
+
_executor: ThreadPoolExecutor | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_executor() -> ThreadPoolExecutor:
|
|
59
|
+
"""Get shared thread pool executor."""
|
|
60
|
+
global _executor
|
|
61
|
+
if _executor is None:
|
|
62
|
+
_executor = ThreadPoolExecutor(max_workers=4)
|
|
63
|
+
return _executor
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _shutdown_executor() -> None:
|
|
67
|
+
"""Shutdown the executor (for testing/cleanup)."""
|
|
68
|
+
global _executor
|
|
69
|
+
if _executor is not None:
|
|
70
|
+
_executor.shutdown(wait=False)
|
|
71
|
+
_executor = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TruthoundReporterAdapter(BaseReporter[ReporterConfig]):
|
|
75
|
+
"""Adapter for truthound's reporter implementations.
|
|
76
|
+
|
|
77
|
+
This adapter wraps truthound reporters to conform to our ReporterProtocol,
|
|
78
|
+
enabling seamless integration with the dashboard's report generation system.
|
|
79
|
+
|
|
80
|
+
The adapter handles:
|
|
81
|
+
1. Data conversion from ReportData to truthound's expected format
|
|
82
|
+
2. Config translation between our ReporterConfig and truthound's config
|
|
83
|
+
3. Async execution of synchronous truthound reporters
|
|
84
|
+
4. Format and content type mapping
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
from truthound.reporters import get_reporter
|
|
88
|
+
|
|
89
|
+
# Wrap a truthound reporter
|
|
90
|
+
th_reporter = get_reporter("json")
|
|
91
|
+
adapter = TruthoundReporterAdapter(th_reporter)
|
|
92
|
+
|
|
93
|
+
# Generate report using our interface
|
|
94
|
+
output = await adapter.generate(report_data)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# Mapping from truthound format names to our ReportFormatType
|
|
98
|
+
FORMAT_MAPPING: dict[str, ReportFormatType] = {
|
|
99
|
+
"json": ReportFormatType.JSON,
|
|
100
|
+
"html": ReportFormatType.HTML,
|
|
101
|
+
"csv": ReportFormatType.CSV,
|
|
102
|
+
"yaml": ReportFormatType.YAML,
|
|
103
|
+
"ndjson": ReportFormatType.NDJSON,
|
|
104
|
+
"console": ReportFormatType.CONSOLE,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Content type mapping
|
|
108
|
+
CONTENT_TYPE_MAPPING: dict[ReportFormatType, str] = {
|
|
109
|
+
ReportFormatType.JSON: "application/json; charset=utf-8",
|
|
110
|
+
ReportFormatType.HTML: "text/html; charset=utf-8",
|
|
111
|
+
ReportFormatType.CSV: "text/csv; charset=utf-8",
|
|
112
|
+
ReportFormatType.YAML: "application/x-yaml; charset=utf-8",
|
|
113
|
+
ReportFormatType.NDJSON: "application/x-ndjson; charset=utf-8",
|
|
114
|
+
ReportFormatType.CONSOLE: "text/plain; charset=utf-8",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# File extension mapping
|
|
118
|
+
EXTENSION_MAPPING: dict[ReportFormatType, str] = {
|
|
119
|
+
ReportFormatType.JSON: ".json",
|
|
120
|
+
ReportFormatType.HTML: ".html",
|
|
121
|
+
ReportFormatType.CSV: ".csv",
|
|
122
|
+
ReportFormatType.YAML: ".yaml",
|
|
123
|
+
ReportFormatType.NDJSON: ".ndjson",
|
|
124
|
+
ReportFormatType.CONSOLE: ".txt",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
truthound_reporter: Any,
|
|
130
|
+
format_override: ReportFormatType | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Initialize adapter.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
truthound_reporter: A truthound reporter instance
|
|
136
|
+
(e.g., JSONReporter, HTMLReporter from truthound.reporters).
|
|
137
|
+
format_override: Override the detected format. Useful when
|
|
138
|
+
the truthound reporter doesn't expose format information.
|
|
139
|
+
"""
|
|
140
|
+
super().__init__()
|
|
141
|
+
self._th_reporter = truthound_reporter
|
|
142
|
+
self._format_override = format_override
|
|
143
|
+
|
|
144
|
+
# Try to detect format from truthound reporter
|
|
145
|
+
self._detected_format = self._detect_format()
|
|
146
|
+
|
|
147
|
+
def _detect_format(self) -> ReportFormatType:
|
|
148
|
+
"""Detect the format from the truthound reporter."""
|
|
149
|
+
if self._format_override:
|
|
150
|
+
return self._format_override
|
|
151
|
+
|
|
152
|
+
# Try getting format from truthound reporter
|
|
153
|
+
if hasattr(self._th_reporter, "name"):
|
|
154
|
+
name = self._th_reporter.name.lower()
|
|
155
|
+
if name in self.FORMAT_MAPPING:
|
|
156
|
+
return self.FORMAT_MAPPING[name]
|
|
157
|
+
|
|
158
|
+
# Try from file_extension
|
|
159
|
+
if hasattr(self._th_reporter, "file_extension"):
|
|
160
|
+
ext = self._th_reporter.file_extension.lower()
|
|
161
|
+
for fmt, mapped_ext in self.EXTENSION_MAPPING.items():
|
|
162
|
+
if ext == mapped_ext or ext == mapped_ext[1:]:
|
|
163
|
+
return fmt
|
|
164
|
+
|
|
165
|
+
# Default to JSON
|
|
166
|
+
logger.warning(
|
|
167
|
+
f"Could not detect format for {type(self._th_reporter).__name__}, "
|
|
168
|
+
"defaulting to JSON"
|
|
169
|
+
)
|
|
170
|
+
return ReportFormatType.JSON
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def format(self) -> ReportFormatType:
|
|
174
|
+
"""Get the report format."""
|
|
175
|
+
return self._detected_format
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def content_type(self) -> str:
|
|
179
|
+
"""Get the MIME content type."""
|
|
180
|
+
# Try to get from truthound reporter first
|
|
181
|
+
if hasattr(self._th_reporter, "content_type"):
|
|
182
|
+
return self._th_reporter.content_type
|
|
183
|
+
return self.CONTENT_TYPE_MAPPING.get(
|
|
184
|
+
self._detected_format,
|
|
185
|
+
"application/octet-stream",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def file_extension(self) -> str:
|
|
190
|
+
"""Get the file extension."""
|
|
191
|
+
# Try to get from truthound reporter first
|
|
192
|
+
if hasattr(self._th_reporter, "file_extension"):
|
|
193
|
+
return self._th_reporter.file_extension
|
|
194
|
+
return self.EXTENSION_MAPPING.get(self._detected_format, ".txt")
|
|
195
|
+
|
|
196
|
+
async def _render_content(
|
|
197
|
+
self,
|
|
198
|
+
data: ReportData,
|
|
199
|
+
config: ReporterConfig,
|
|
200
|
+
) -> str | bytes:
|
|
201
|
+
"""Render content using the truthound reporter.
|
|
202
|
+
|
|
203
|
+
This method converts our ReportData to the format expected by
|
|
204
|
+
truthound reporters and executes the rendering in a thread pool.
|
|
205
|
+
"""
|
|
206
|
+
# Convert ReportData to format expected by truthound
|
|
207
|
+
th_input = self._convert_to_truthound_input(data, config)
|
|
208
|
+
|
|
209
|
+
# Run truthound reporter in thread pool (it's synchronous)
|
|
210
|
+
loop = asyncio.get_event_loop()
|
|
211
|
+
executor = _get_executor()
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
# Try the new truthound API first (render method)
|
|
215
|
+
if hasattr(self._th_reporter, "render"):
|
|
216
|
+
func = partial(self._th_reporter.render, th_input)
|
|
217
|
+
result = await loop.run_in_executor(executor, func)
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
# Fall back to __call__ method
|
|
221
|
+
if callable(self._th_reporter):
|
|
222
|
+
func = partial(self._th_reporter, th_input)
|
|
223
|
+
result = await loop.run_in_executor(executor, func)
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Truthound reporter {type(self._th_reporter).__name__} "
|
|
228
|
+
"does not have a render() method or is not callable"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error rendering with truthound reporter: {e}")
|
|
233
|
+
# Return a fallback output
|
|
234
|
+
return self._render_fallback(data, config, str(e))
|
|
235
|
+
|
|
236
|
+
def _convert_to_truthound_input(
|
|
237
|
+
self,
|
|
238
|
+
data: ReportData,
|
|
239
|
+
config: ReporterConfig,
|
|
240
|
+
) -> Any:
|
|
241
|
+
"""Convert ReportData to truthound's expected input format.
|
|
242
|
+
|
|
243
|
+
Truthound reporters expect a ValidationResult object from
|
|
244
|
+
truthound.stores.results with these attributes:
|
|
245
|
+
- run_id: str
|
|
246
|
+
- run_time: datetime
|
|
247
|
+
- data_asset: str
|
|
248
|
+
- status: ResultStatus
|
|
249
|
+
- results: list[ValidatorResult]
|
|
250
|
+
- statistics: ResultStatistics
|
|
251
|
+
- tags: dict
|
|
252
|
+
|
|
253
|
+
Since we don't have direct access to these, we create a
|
|
254
|
+
mock object that provides the same interface.
|
|
255
|
+
"""
|
|
256
|
+
# If raw_data is available and is a truthound Report or ValidationResult, use it
|
|
257
|
+
if data.raw_data is not None:
|
|
258
|
+
# Check if it looks like a ValidationResult (preferred by reporters)
|
|
259
|
+
if hasattr(data.raw_data, "results") and hasattr(data.raw_data, "run_id"):
|
|
260
|
+
return data.raw_data
|
|
261
|
+
# Check if it looks like a truthound Report (some reporters accept this)
|
|
262
|
+
if hasattr(data.raw_data, "issues") and hasattr(data.raw_data, "has_issues"):
|
|
263
|
+
return data.raw_data
|
|
264
|
+
|
|
265
|
+
# Create a mock ValidationResult object that provides the truthound interface
|
|
266
|
+
return _TruthoundValidationResultMock(data, config)
|
|
267
|
+
|
|
268
|
+
def _render_fallback(
|
|
269
|
+
self,
|
|
270
|
+
data: ReportData,
|
|
271
|
+
config: ReporterConfig,
|
|
272
|
+
error: str,
|
|
273
|
+
) -> str:
|
|
274
|
+
"""Render a fallback output when truthound reporter fails."""
|
|
275
|
+
if self._detected_format == ReportFormatType.JSON:
|
|
276
|
+
import json
|
|
277
|
+
|
|
278
|
+
return json.dumps(
|
|
279
|
+
{
|
|
280
|
+
"error": error,
|
|
281
|
+
"fallback": True,
|
|
282
|
+
"data": data.to_dict(),
|
|
283
|
+
},
|
|
284
|
+
indent=2,
|
|
285
|
+
)
|
|
286
|
+
elif self._detected_format == ReportFormatType.HTML:
|
|
287
|
+
return f"""
|
|
288
|
+
<html>
|
|
289
|
+
<head><title>Report Error</title></head>
|
|
290
|
+
<body>
|
|
291
|
+
<h1>Report Generation Error</h1>
|
|
292
|
+
<p>Error: {error}</p>
|
|
293
|
+
<h2>Validation ID: {data.validation_id}</h2>
|
|
294
|
+
<h2>Issues: {data.summary.total_issues}</h2>
|
|
295
|
+
</body>
|
|
296
|
+
</html>
|
|
297
|
+
"""
|
|
298
|
+
else:
|
|
299
|
+
return f"Report generation error: {error}\nValidation ID: {data.validation_id}"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class _TruthoundReportMock:
|
|
303
|
+
"""Mock object that mimics truthound's Report interface.
|
|
304
|
+
|
|
305
|
+
This allows us to use truthound reporters without having
|
|
306
|
+
a real truthound Report object.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def __init__(self, data: ReportData, config: ReporterConfig) -> None:
|
|
310
|
+
self._data = data
|
|
311
|
+
self._config = config
|
|
312
|
+
|
|
313
|
+
# Convert issues to mock ValidationIssue objects
|
|
314
|
+
self._issues = [
|
|
315
|
+
_MockValidationIssue(issue) for issue in data.issues
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def issues(self) -> list[_MockValidationIssue]:
|
|
320
|
+
return self._issues
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def source(self) -> str:
|
|
324
|
+
return self._data.source_name or self._data.source_id
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def row_count(self) -> int:
|
|
328
|
+
return self._data.statistics.row_count or 0
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def column_count(self) -> int:
|
|
332
|
+
return self._data.statistics.column_count or 0
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def has_issues(self) -> bool:
|
|
336
|
+
return self._data.summary.total_issues > 0
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def has_critical(self) -> bool:
|
|
340
|
+
return self._data.summary.has_critical
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def has_high(self) -> bool:
|
|
344
|
+
return self._data.summary.has_high
|
|
345
|
+
|
|
346
|
+
def to_json(self, indent: int | None = 2) -> str:
|
|
347
|
+
"""Convert to JSON string."""
|
|
348
|
+
import json
|
|
349
|
+
|
|
350
|
+
return json.dumps(self._data.to_dict(), indent=indent)
|
|
351
|
+
|
|
352
|
+
def to_dict(self) -> dict[str, Any]:
|
|
353
|
+
"""Convert to dictionary."""
|
|
354
|
+
return self._data.to_dict()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class _MockValidationIssue:
|
|
358
|
+
"""Mock ValidationIssue for truthound reporters."""
|
|
359
|
+
|
|
360
|
+
def __init__(self, issue: ValidationIssueData) -> None:
|
|
361
|
+
self._issue = issue
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def column(self) -> str | None:
|
|
365
|
+
return self._issue.column
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def issue_type(self) -> str:
|
|
369
|
+
return self._issue.issue_type
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def severity(self) -> _MockSeverity:
|
|
373
|
+
return _MockSeverity(self._issue.severity)
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def message(self) -> str:
|
|
377
|
+
return self._issue.message
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def count(self) -> int:
|
|
381
|
+
return self._issue.count
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def expected(self) -> Any:
|
|
385
|
+
return self._issue.expected
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def actual(self) -> Any:
|
|
389
|
+
return self._issue.actual
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def details(self) -> dict[str, Any] | None:
|
|
393
|
+
return self._issue.details
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class _MockSeverity:
|
|
397
|
+
"""Mock Severity enum for truthound reporters."""
|
|
398
|
+
|
|
399
|
+
def __init__(self, value: str) -> None:
|
|
400
|
+
self._value = value.lower()
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def value(self) -> str:
|
|
404
|
+
return self._value
|
|
405
|
+
|
|
406
|
+
def __str__(self) -> str:
|
|
407
|
+
return self._value
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class _TruthoundValidationResultMock:
|
|
411
|
+
"""Mock object that mimics truthound's ValidationResult interface.
|
|
412
|
+
|
|
413
|
+
Truthound reporters expect ValidationResult which has:
|
|
414
|
+
- run_id: str
|
|
415
|
+
- run_time: datetime
|
|
416
|
+
- data_asset: str
|
|
417
|
+
- status: ResultStatus
|
|
418
|
+
- results: list[ValidatorResult]
|
|
419
|
+
- statistics: ResultStatistics
|
|
420
|
+
- tags: dict
|
|
421
|
+
- success: bool
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
def __init__(self, data: ReportData, config: ReporterConfig) -> None:
|
|
425
|
+
from datetime import datetime as dt
|
|
426
|
+
|
|
427
|
+
self._data = data
|
|
428
|
+
self._config = config
|
|
429
|
+
|
|
430
|
+
# Convert issues to mock ValidatorResult objects
|
|
431
|
+
self._results = [
|
|
432
|
+
_MockValidatorResult(issue) for issue in data.issues
|
|
433
|
+
]
|
|
434
|
+
self._statistics = _MockResultStatistics(data)
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def run_id(self) -> str:
|
|
438
|
+
return self._data.validation_id
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def run_time(self) -> Any:
|
|
442
|
+
from datetime import datetime as dt
|
|
443
|
+
|
|
444
|
+
if self._data.statistics.started_at:
|
|
445
|
+
return self._data.statistics.started_at
|
|
446
|
+
return dt.utcnow()
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def data_asset(self) -> str:
|
|
450
|
+
return self._data.source_name or self._data.source_id
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def status(self) -> _MockResultStatus:
|
|
454
|
+
return _MockResultStatus(self._data.summary.passed)
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def success(self) -> bool:
|
|
458
|
+
"""Whether the validation passed."""
|
|
459
|
+
return self._data.summary.passed
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def results(self) -> list[_MockValidatorResult]:
|
|
463
|
+
return self._results
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def statistics(self) -> _MockResultStatistics:
|
|
467
|
+
return self._statistics
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def tags(self) -> dict[str, Any]:
|
|
471
|
+
return self._data.metadata
|
|
472
|
+
|
|
473
|
+
# Additional properties that truthound reporters might expect
|
|
474
|
+
@property
|
|
475
|
+
def suite_name(self) -> str:
|
|
476
|
+
"""Test suite name for JUnit-style reporters."""
|
|
477
|
+
return self._config.title or "Truthound Validation"
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def source(self) -> str:
|
|
481
|
+
"""Alias for data_asset."""
|
|
482
|
+
return self.data_asset
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def row_count(self) -> int:
|
|
486
|
+
"""Row count for Report-style access."""
|
|
487
|
+
return self._data.statistics.row_count or 0
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def column_count(self) -> int:
|
|
491
|
+
"""Column count for Report-style access."""
|
|
492
|
+
return self._data.statistics.column_count or 0
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def issues(self) -> list[_MockValidatorResult]:
|
|
496
|
+
"""Alias for results (Report-style access)."""
|
|
497
|
+
return self._results
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def has_issues(self) -> bool:
|
|
501
|
+
"""Report-style check for issues."""
|
|
502
|
+
return self._data.summary.total_issues > 0
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def has_critical(self) -> bool:
|
|
506
|
+
"""Report-style check for critical issues."""
|
|
507
|
+
return self._data.summary.has_critical
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def has_high(self) -> bool:
|
|
511
|
+
"""Report-style check for high severity issues."""
|
|
512
|
+
return self._data.summary.has_high
|
|
513
|
+
|
|
514
|
+
def to_dict(self) -> dict[str, Any]:
|
|
515
|
+
"""Convert to dictionary."""
|
|
516
|
+
return {
|
|
517
|
+
"run_id": self.run_id,
|
|
518
|
+
"run_time": self.run_time.isoformat() if hasattr(self.run_time, 'isoformat') else str(self.run_time),
|
|
519
|
+
"data_asset": self.data_asset,
|
|
520
|
+
"status": self.status.value,
|
|
521
|
+
"success": self.success,
|
|
522
|
+
"results": [r.to_dict() for r in self.results],
|
|
523
|
+
"statistics": self.statistics.to_dict(),
|
|
524
|
+
"tags": self.tags,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
def to_json(self, indent: int | None = 2) -> str:
|
|
528
|
+
"""Convert to JSON string (Report-style)."""
|
|
529
|
+
import json
|
|
530
|
+
|
|
531
|
+
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class _MockResultStatus:
|
|
535
|
+
"""Mock ResultStatus enum."""
|
|
536
|
+
|
|
537
|
+
def __init__(self, passed: bool) -> None:
|
|
538
|
+
self._passed = passed
|
|
539
|
+
|
|
540
|
+
@property
|
|
541
|
+
def value(self) -> str:
|
|
542
|
+
return "SUCCESS" if self._passed else "FAILURE"
|
|
543
|
+
|
|
544
|
+
def __str__(self) -> str:
|
|
545
|
+
return self.value
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class _MockResultStatistics:
|
|
549
|
+
"""Mock ResultStatistics object."""
|
|
550
|
+
|
|
551
|
+
def __init__(self, data: ReportData) -> None:
|
|
552
|
+
self._data = data
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def total_issues(self) -> int:
|
|
556
|
+
return self._data.summary.total_issues
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def total_rows(self) -> int:
|
|
560
|
+
return self._data.statistics.row_count or 0
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def total_columns(self) -> int:
|
|
564
|
+
return self._data.statistics.column_count or 0
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def critical_count(self) -> int:
|
|
568
|
+
return self._data.summary.critical_issues
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def high_count(self) -> int:
|
|
572
|
+
return self._data.summary.high_issues
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def medium_count(self) -> int:
|
|
576
|
+
return self._data.summary.medium_issues
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def low_count(self) -> int:
|
|
580
|
+
return self._data.summary.low_issues
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def passed(self) -> bool:
|
|
584
|
+
return self._data.summary.passed
|
|
585
|
+
|
|
586
|
+
def to_dict(self) -> dict[str, Any]:
|
|
587
|
+
return {
|
|
588
|
+
"total_issues": self.total_issues,
|
|
589
|
+
"total_rows": self.total_rows,
|
|
590
|
+
"total_columns": self.total_columns,
|
|
591
|
+
"critical_count": self.critical_count,
|
|
592
|
+
"high_count": self.high_count,
|
|
593
|
+
"medium_count": self.medium_count,
|
|
594
|
+
"low_count": self.low_count,
|
|
595
|
+
"passed": self.passed,
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class _MockValidatorResult:
|
|
600
|
+
"""Mock ValidatorResult object."""
|
|
601
|
+
|
|
602
|
+
def __init__(self, issue: ValidationIssueData) -> None:
|
|
603
|
+
self._issue = issue
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def validator_name(self) -> str:
|
|
607
|
+
return self._issue.validator_name or self._issue.issue_type
|
|
608
|
+
|
|
609
|
+
@property
|
|
610
|
+
def column(self) -> str | None:
|
|
611
|
+
return self._issue.column
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def issue_type(self) -> str:
|
|
615
|
+
return self._issue.issue_type
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def severity(self) -> _MockSeverity:
|
|
619
|
+
return _MockSeverity(self._issue.severity)
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def message(self) -> str:
|
|
623
|
+
return self._issue.message
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def count(self) -> int:
|
|
627
|
+
return self._issue.count
|
|
628
|
+
|
|
629
|
+
@property
|
|
630
|
+
def success(self) -> bool:
|
|
631
|
+
return False # All issues are failures
|
|
632
|
+
|
|
633
|
+
@property
|
|
634
|
+
def expected(self) -> Any:
|
|
635
|
+
return self._issue.expected
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def actual(self) -> Any:
|
|
639
|
+
return self._issue.actual
|
|
640
|
+
|
|
641
|
+
@property
|
|
642
|
+
def details(self) -> dict[str, Any]:
|
|
643
|
+
return self._issue.details or {}
|
|
644
|
+
|
|
645
|
+
@property
|
|
646
|
+
def sample_values(self) -> list[Any]:
|
|
647
|
+
return self._issue.sample_values or []
|
|
648
|
+
|
|
649
|
+
def to_dict(self) -> dict[str, Any]:
|
|
650
|
+
return {
|
|
651
|
+
"validator_name": self.validator_name,
|
|
652
|
+
"column": self.column,
|
|
653
|
+
"issue_type": self.issue_type,
|
|
654
|
+
"severity": self.severity.value,
|
|
655
|
+
"message": self.message,
|
|
656
|
+
"count": self.count,
|
|
657
|
+
"success": self.success,
|
|
658
|
+
"expected": self.expected,
|
|
659
|
+
"actual": self.actual,
|
|
660
|
+
"details": self.details,
|
|
661
|
+
"sample_values": self.sample_values,
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def create_truthound_reporter(
|
|
666
|
+
format_name: str,
|
|
667
|
+
locale: str = "en",
|
|
668
|
+
**config_options: Any,
|
|
669
|
+
) -> TruthoundReporterAdapter | None:
|
|
670
|
+
"""Factory function to create a truthound reporter adapter.
|
|
671
|
+
|
|
672
|
+
This function attempts to import and instantiate a truthound reporter,
|
|
673
|
+
returning None if truthound is not available.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
format_name: Format name (e.g., 'json', 'html', 'markdown').
|
|
677
|
+
locale: Locale for i18n (e.g., 'en', 'ko', 'ja').
|
|
678
|
+
**config_options: Additional configuration passed to truthound reporter.
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
TruthoundReporterAdapter or None if truthound unavailable.
|
|
682
|
+
|
|
683
|
+
Example:
|
|
684
|
+
reporter = create_truthound_reporter("json")
|
|
685
|
+
if reporter:
|
|
686
|
+
output = await reporter.generate(data)
|
|
687
|
+
"""
|
|
688
|
+
try:
|
|
689
|
+
from truthound.reporters import get_reporter
|
|
690
|
+
|
|
691
|
+
# Get truthound reporter with locale support
|
|
692
|
+
try:
|
|
693
|
+
th_reporter = get_reporter(format_name, locale=locale, **config_options)
|
|
694
|
+
except TypeError:
|
|
695
|
+
# Fallback for reporters without locale support
|
|
696
|
+
th_reporter = get_reporter(format_name, **config_options)
|
|
697
|
+
|
|
698
|
+
return TruthoundReporterAdapter(th_reporter)
|
|
699
|
+
|
|
700
|
+
except ImportError:
|
|
701
|
+
logger.warning(
|
|
702
|
+
f"truthound.reporters not available, cannot create {format_name} reporter"
|
|
703
|
+
)
|
|
704
|
+
return None
|
|
705
|
+
except ValueError as e:
|
|
706
|
+
logger.warning(f"Failed to create truthound reporter for {format_name}: {e}")
|
|
707
|
+
return None
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def is_truthound_available() -> bool:
|
|
711
|
+
"""Check if truthound reporters are available.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
True if truthound.reporters can be imported.
|
|
715
|
+
"""
|
|
716
|
+
try:
|
|
717
|
+
from truthound.reporters import get_reporter # noqa: F401
|
|
718
|
+
|
|
719
|
+
return True
|
|
720
|
+
except ImportError:
|
|
721
|
+
return False
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def get_truthound_formats() -> list[str]:
|
|
725
|
+
"""Get list of available truthound report formats.
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
List of format names available in truthound, or empty list if
|
|
729
|
+
truthound is not available.
|
|
730
|
+
"""
|
|
731
|
+
try:
|
|
732
|
+
from truthound.reporters.factory import list_available_formats
|
|
733
|
+
|
|
734
|
+
return list_available_formats()
|
|
735
|
+
except ImportError:
|
|
736
|
+
return []
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def create_ci_reporter(
|
|
740
|
+
platform: str | None = None,
|
|
741
|
+
**config_options: Any,
|
|
742
|
+
) -> TruthoundReporterAdapter | None:
|
|
743
|
+
"""Create a CI platform reporter adapter.
|
|
744
|
+
|
|
745
|
+
This function creates a reporter for CI/CD platforms. If no platform
|
|
746
|
+
is specified, it attempts to auto-detect the current CI environment.
|
|
747
|
+
|
|
748
|
+
Supported platforms:
|
|
749
|
+
- github: GitHub Actions (::error::, ::warning::, step summary)
|
|
750
|
+
- gitlab: GitLab CI (section markers, ANSI colors)
|
|
751
|
+
- jenkins: Jenkins (JUnit XML compatible)
|
|
752
|
+
- azure: Azure DevOps (##vso commands)
|
|
753
|
+
- circleci: CircleCI
|
|
754
|
+
- bitbucket: Bitbucket Pipelines
|
|
755
|
+
- travis: Travis CI
|
|
756
|
+
- teamcity: TeamCity (service messages)
|
|
757
|
+
- buildkite: Buildkite (annotations)
|
|
758
|
+
- drone: Drone CI
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
platform: CI platform name, or None for auto-detection.
|
|
762
|
+
**config_options: Additional configuration passed to truthound reporter.
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
TruthoundReporterAdapter for the CI platform, or None if unavailable.
|
|
766
|
+
|
|
767
|
+
Example:
|
|
768
|
+
# Auto-detect CI platform
|
|
769
|
+
reporter = create_ci_reporter()
|
|
770
|
+
|
|
771
|
+
# Specific platform
|
|
772
|
+
reporter = create_ci_reporter("github")
|
|
773
|
+
"""
|
|
774
|
+
try:
|
|
775
|
+
# Try to auto-detect if no platform specified
|
|
776
|
+
if platform is None:
|
|
777
|
+
from truthound.checkpoint.ci import detect_ci_platform
|
|
778
|
+
|
|
779
|
+
detected = detect_ci_platform()
|
|
780
|
+
if detected:
|
|
781
|
+
platform = detected.value.lower()
|
|
782
|
+
else:
|
|
783
|
+
logger.debug("No CI platform detected")
|
|
784
|
+
return None
|
|
785
|
+
else:
|
|
786
|
+
platform = platform.lower()
|
|
787
|
+
|
|
788
|
+
# Try new CI reporters API
|
|
789
|
+
try:
|
|
790
|
+
from truthound.reporters.ci import get_ci_reporter
|
|
791
|
+
|
|
792
|
+
ci_reporter = get_ci_reporter(platform, **config_options)
|
|
793
|
+
return TruthoundReporterAdapter(
|
|
794
|
+
ci_reporter,
|
|
795
|
+
format_override=_get_ci_format_type(platform),
|
|
796
|
+
)
|
|
797
|
+
except (ImportError, AttributeError):
|
|
798
|
+
pass
|
|
799
|
+
|
|
800
|
+
# Fallback: Try the generic CI reporter factory
|
|
801
|
+
try:
|
|
802
|
+
from truthound.reporters import get_reporter
|
|
803
|
+
|
|
804
|
+
ci_reporter = get_reporter(platform, **config_options)
|
|
805
|
+
return TruthoundReporterAdapter(
|
|
806
|
+
ci_reporter,
|
|
807
|
+
format_override=_get_ci_format_type(platform),
|
|
808
|
+
)
|
|
809
|
+
except ValueError:
|
|
810
|
+
logger.warning(f"CI reporter for {platform} not available")
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
except ImportError:
|
|
814
|
+
logger.warning("truthound CI reporters not available")
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _get_ci_format_type(platform: str) -> ReportFormatType:
|
|
819
|
+
"""Map CI platform name to ReportFormatType."""
|
|
820
|
+
platform_lower = platform.lower()
|
|
821
|
+
mapping = {
|
|
822
|
+
"github": ReportFormatType.GITHUB,
|
|
823
|
+
"gitlab": ReportFormatType.GITLAB,
|
|
824
|
+
"jenkins": ReportFormatType.JENKINS,
|
|
825
|
+
"azure": ReportFormatType.AZURE,
|
|
826
|
+
"circleci": ReportFormatType.CIRCLECI,
|
|
827
|
+
"bitbucket": ReportFormatType.BITBUCKET,
|
|
828
|
+
"travis": ReportFormatType.TRAVIS,
|
|
829
|
+
"teamcity": ReportFormatType.TEAMCITY,
|
|
830
|
+
"buildkite": ReportFormatType.BUILDKITE,
|
|
831
|
+
"drone": ReportFormatType.DRONE,
|
|
832
|
+
}
|
|
833
|
+
return mapping.get(platform_lower, ReportFormatType.CI)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def is_ci_environment() -> bool:
|
|
837
|
+
"""Check if running in a CI environment.
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
True if a CI environment is detected.
|
|
841
|
+
"""
|
|
842
|
+
try:
|
|
843
|
+
from truthound.checkpoint.ci import is_ci_environment as _is_ci
|
|
844
|
+
|
|
845
|
+
return _is_ci()
|
|
846
|
+
except ImportError:
|
|
847
|
+
# Fallback: check common CI environment variables
|
|
848
|
+
import os
|
|
849
|
+
|
|
850
|
+
ci_vars = [
|
|
851
|
+
"CI",
|
|
852
|
+
"GITHUB_ACTIONS",
|
|
853
|
+
"GITLAB_CI",
|
|
854
|
+
"JENKINS_URL",
|
|
855
|
+
"CIRCLECI",
|
|
856
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
857
|
+
"TRAVIS",
|
|
858
|
+
"TEAMCITY_VERSION",
|
|
859
|
+
"BUILDKITE",
|
|
860
|
+
"DRONE",
|
|
861
|
+
"TF_BUILD", # Azure DevOps
|
|
862
|
+
"CODEBUILD_BUILD_ID", # AWS CodeBuild
|
|
863
|
+
]
|
|
864
|
+
return any(os.environ.get(var) for var in ci_vars)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def get_detected_ci_platform() -> str | None:
|
|
868
|
+
"""Detect the current CI platform.
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Platform name string or None if not in CI.
|
|
872
|
+
"""
|
|
873
|
+
try:
|
|
874
|
+
from truthound.checkpoint.ci import detect_ci_platform
|
|
875
|
+
|
|
876
|
+
platform = detect_ci_platform()
|
|
877
|
+
return platform.value.lower() if platform else None
|
|
878
|
+
except ImportError:
|
|
879
|
+
# Fallback detection
|
|
880
|
+
import os
|
|
881
|
+
|
|
882
|
+
if os.environ.get("GITHUB_ACTIONS"):
|
|
883
|
+
return "github"
|
|
884
|
+
if os.environ.get("GITLAB_CI"):
|
|
885
|
+
return "gitlab"
|
|
886
|
+
if os.environ.get("JENKINS_URL"):
|
|
887
|
+
return "jenkins"
|
|
888
|
+
if os.environ.get("TF_BUILD"):
|
|
889
|
+
return "azure"
|
|
890
|
+
if os.environ.get("CIRCLECI"):
|
|
891
|
+
return "circleci"
|
|
892
|
+
if os.environ.get("BITBUCKET_BUILD_NUMBER"):
|
|
893
|
+
return "bitbucket"
|
|
894
|
+
if os.environ.get("TRAVIS"):
|
|
895
|
+
return "travis"
|
|
896
|
+
if os.environ.get("TEAMCITY_VERSION"):
|
|
897
|
+
return "teamcity"
|
|
898
|
+
if os.environ.get("BUILDKITE"):
|
|
899
|
+
return "buildkite"
|
|
900
|
+
if os.environ.get("DRONE"):
|
|
901
|
+
return "drone"
|
|
902
|
+
return None
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class ValidationModelAdapter:
|
|
906
|
+
"""Adapter for converting database Validation models to ReportData.
|
|
907
|
+
|
|
908
|
+
This adapter handles the conversion from our SQLAlchemy Validation
|
|
909
|
+
model to the backend-agnostic ReportData format.
|
|
910
|
+
|
|
911
|
+
Example:
|
|
912
|
+
validation = await service.get_validation(validation_id)
|
|
913
|
+
data = ValidationModelAdapter.to_report_data(validation)
|
|
914
|
+
output = await reporter.generate(data)
|
|
915
|
+
"""
|
|
916
|
+
|
|
917
|
+
@staticmethod
|
|
918
|
+
def to_report_data(validation: Any) -> ReportData:
|
|
919
|
+
"""Convert a Validation model to ReportData.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
validation: Validation model from database.
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
ReportData instance.
|
|
926
|
+
"""
|
|
927
|
+
return ReportData.from_validation_model(validation)
|
|
928
|
+
|
|
929
|
+
@staticmethod
|
|
930
|
+
def to_truthound_result(validation: Any) -> Any:
|
|
931
|
+
"""Convert a Validation model to a truthound-compatible result.
|
|
932
|
+
|
|
933
|
+
This is useful when you need to use truthound reporters directly
|
|
934
|
+
without going through our adapter.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
validation: Validation model from database.
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
Object that mimics truthound's ValidationResult interface.
|
|
941
|
+
"""
|
|
942
|
+
data = ReportData.from_validation_model(validation)
|
|
943
|
+
return _TruthoundReportMock(data, ReporterConfig())
|