truthound-dashboard 1.4.3__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.3.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.3.dist-info/METADATA +0 -505
- {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Built-in reporter implementations.
|
|
2
|
+
|
|
3
|
+
These reporters provide dashboard-native report generation
|
|
4
|
+
without depending on external libraries.
|
|
5
|
+
|
|
6
|
+
They serve as fallbacks when truthound reporters are not available
|
|
7
|
+
and can be used independently.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .csv_reporter import BuiltinCSVReporter
|
|
11
|
+
from .html_reporter import BuiltinHTMLReporter
|
|
12
|
+
from .json_reporter import BuiltinJSONReporter
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BuiltinCSVReporter",
|
|
16
|
+
"BuiltinHTMLReporter",
|
|
17
|
+
"BuiltinJSONReporter",
|
|
18
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Built-in CSV reporter.
|
|
2
|
+
|
|
3
|
+
Generates CSV reports for spreadsheet analysis without external dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import io
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..interfaces import (
|
|
13
|
+
BaseReporter,
|
|
14
|
+
ReportData,
|
|
15
|
+
ReporterConfig,
|
|
16
|
+
ReportFormatType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BuiltinCSVReporter(BaseReporter[ReporterConfig]):
|
|
21
|
+
"""Built-in CSV report generator.
|
|
22
|
+
|
|
23
|
+
Produces CSV reports suitable for spreadsheet analysis.
|
|
24
|
+
Each row represents one validation issue.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
delimiter: str = ",",
|
|
30
|
+
quotechar: str = '"',
|
|
31
|
+
include_header: bool = True,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize CSV reporter.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
delimiter: Field delimiter character.
|
|
37
|
+
quotechar: Quote character for strings.
|
|
38
|
+
include_header: Whether to include header row.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__()
|
|
41
|
+
self._delimiter = delimiter
|
|
42
|
+
self._quotechar = quotechar
|
|
43
|
+
self._include_header = include_header
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def format(self) -> ReportFormatType:
|
|
47
|
+
return ReportFormatType.CSV
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def content_type(self) -> str:
|
|
51
|
+
return "text/csv; charset=utf-8"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def file_extension(self) -> str:
|
|
55
|
+
return ".csv"
|
|
56
|
+
|
|
57
|
+
async def _render_content(
|
|
58
|
+
self,
|
|
59
|
+
data: ReportData,
|
|
60
|
+
config: ReporterConfig,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Render CSV report content."""
|
|
63
|
+
output = io.StringIO()
|
|
64
|
+
writer = csv.writer(
|
|
65
|
+
output,
|
|
66
|
+
delimiter=self._delimiter,
|
|
67
|
+
quotechar=self._quotechar,
|
|
68
|
+
quoting=csv.QUOTE_MINIMAL,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Define columns
|
|
72
|
+
columns = [
|
|
73
|
+
"validation_id",
|
|
74
|
+
"source_id",
|
|
75
|
+
"column",
|
|
76
|
+
"issue_type",
|
|
77
|
+
"severity",
|
|
78
|
+
"message",
|
|
79
|
+
"count",
|
|
80
|
+
"validator_name",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
if config.include_samples:
|
|
84
|
+
columns.append("sample_values")
|
|
85
|
+
|
|
86
|
+
# Write header
|
|
87
|
+
if self._include_header:
|
|
88
|
+
writer.writerow(columns)
|
|
89
|
+
|
|
90
|
+
# Write issues
|
|
91
|
+
for issue in data.issues:
|
|
92
|
+
row = [
|
|
93
|
+
data.validation_id,
|
|
94
|
+
data.source_id,
|
|
95
|
+
issue.column or "",
|
|
96
|
+
issue.issue_type,
|
|
97
|
+
issue.severity,
|
|
98
|
+
issue.message,
|
|
99
|
+
str(issue.count),
|
|
100
|
+
issue.validator_name or "",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
if config.include_samples:
|
|
104
|
+
samples = ""
|
|
105
|
+
if issue.sample_values:
|
|
106
|
+
samples = "; ".join(str(v) for v in issue.sample_values[:config.max_sample_values])
|
|
107
|
+
row.append(samples)
|
|
108
|
+
|
|
109
|
+
writer.writerow(row)
|
|
110
|
+
|
|
111
|
+
return output.getvalue()
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Built-in HTML reporter.
|
|
2
|
+
|
|
3
|
+
Generates HTML reports with embedded CSS without external dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from html import escape
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..interfaces import (
|
|
13
|
+
BaseReporter,
|
|
14
|
+
ReportData,
|
|
15
|
+
ReporterConfig,
|
|
16
|
+
ReportFormatType,
|
|
17
|
+
ReportThemeType,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BuiltinHTMLReporter(BaseReporter[ReporterConfig]):
|
|
22
|
+
"""Built-in HTML report generator.
|
|
23
|
+
|
|
24
|
+
Produces self-contained HTML reports with embedded CSS styling.
|
|
25
|
+
Supports light, dark, and professional themes.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, locale: str = "en") -> None:
|
|
29
|
+
"""Initialize HTML reporter.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
locale: Locale for i18n text.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self._locale = locale
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def format(self) -> ReportFormatType:
|
|
39
|
+
return ReportFormatType.HTML
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def content_type(self) -> str:
|
|
43
|
+
return "text/html; charset=utf-8"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def file_extension(self) -> str:
|
|
47
|
+
return ".html"
|
|
48
|
+
|
|
49
|
+
async def _render_content(
|
|
50
|
+
self,
|
|
51
|
+
data: ReportData,
|
|
52
|
+
config: ReporterConfig,
|
|
53
|
+
) -> str:
|
|
54
|
+
"""Render HTML report content."""
|
|
55
|
+
theme_css = self._get_theme_css(config.theme)
|
|
56
|
+
|
|
57
|
+
html_parts = [
|
|
58
|
+
"<!DOCTYPE html>",
|
|
59
|
+
'<html lang="en">',
|
|
60
|
+
"<head>",
|
|
61
|
+
'<meta charset="UTF-8">',
|
|
62
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
63
|
+
f"<title>{escape(config.title)}</title>",
|
|
64
|
+
"<style>",
|
|
65
|
+
self._get_base_css(),
|
|
66
|
+
theme_css,
|
|
67
|
+
"</style>",
|
|
68
|
+
"</head>",
|
|
69
|
+
"<body>",
|
|
70
|
+
'<div class="container">',
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Header
|
|
74
|
+
html_parts.extend(self._render_header(data, config))
|
|
75
|
+
|
|
76
|
+
# Summary
|
|
77
|
+
html_parts.extend(self._render_summary(data))
|
|
78
|
+
|
|
79
|
+
# Statistics
|
|
80
|
+
if config.include_statistics:
|
|
81
|
+
html_parts.extend(self._render_statistics(data))
|
|
82
|
+
|
|
83
|
+
# Issues
|
|
84
|
+
html_parts.extend(self._render_issues(data, config))
|
|
85
|
+
|
|
86
|
+
# Footer
|
|
87
|
+
html_parts.extend(self._render_footer())
|
|
88
|
+
|
|
89
|
+
html_parts.extend([
|
|
90
|
+
"</div>",
|
|
91
|
+
"</body>",
|
|
92
|
+
"</html>",
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
return "\n".join(html_parts)
|
|
96
|
+
|
|
97
|
+
def _get_base_css(self) -> str:
|
|
98
|
+
"""Get base CSS styles."""
|
|
99
|
+
return """
|
|
100
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
101
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; line-height: 1.6; }
|
|
102
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
103
|
+
h1, h2, h3 { margin-bottom: 1rem; }
|
|
104
|
+
.header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid var(--border-color); }
|
|
105
|
+
.status-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px; font-weight: 600; margin-left: 1rem; }
|
|
106
|
+
.status-passed { background: #dcfce7; color: #166534; }
|
|
107
|
+
.status-failed { background: #fee2e2; color: #991b1b; }
|
|
108
|
+
.card { background: var(--card-bg); border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
109
|
+
.card-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; }
|
|
110
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; }
|
|
111
|
+
.stat-item { text-align: center; padding: 1rem; background: var(--stat-bg); border-radius: 6px; }
|
|
112
|
+
.stat-value { font-size: 2rem; font-weight: bold; color: var(--primary-color); }
|
|
113
|
+
.stat-label { font-size: 0.875rem; color: var(--muted-color); }
|
|
114
|
+
.issue-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
115
|
+
.issue-table th, .issue-table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid var(--border-color); }
|
|
116
|
+
.issue-table th { background: var(--header-bg); font-weight: 600; }
|
|
117
|
+
.severity-badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
|
|
118
|
+
.severity-critical { background: #fecaca; color: #991b1b; }
|
|
119
|
+
.severity-high { background: #fed7aa; color: #9a3412; }
|
|
120
|
+
.severity-medium { background: #fef08a; color: #854d0e; }
|
|
121
|
+
.severity-low { background: #bfdbfe; color: #1e40af; }
|
|
122
|
+
.footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border-color); text-align: center; font-size: 0.875rem; color: var(--muted-color); }
|
|
123
|
+
code { background: var(--code-bg); padding: 0.125rem 0.375rem; border-radius: 4px; font-family: monospace; }
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def _get_theme_css(self, theme: ReportThemeType) -> str:
|
|
127
|
+
"""Get theme-specific CSS variables."""
|
|
128
|
+
if theme == ReportThemeType.DARK:
|
|
129
|
+
return """
|
|
130
|
+
:root {
|
|
131
|
+
--bg-color: #1a1a2e;
|
|
132
|
+
--text-color: #e0e0e0;
|
|
133
|
+
--card-bg: #16213e;
|
|
134
|
+
--stat-bg: #0f3460;
|
|
135
|
+
--header-bg: #1a1a2e;
|
|
136
|
+
--border-color: #374151;
|
|
137
|
+
--primary-color: #fd9e4b;
|
|
138
|
+
--muted-color: #9ca3af;
|
|
139
|
+
--code-bg: #374151;
|
|
140
|
+
}
|
|
141
|
+
body { background: var(--bg-color); color: var(--text-color); }
|
|
142
|
+
"""
|
|
143
|
+
elif theme == ReportThemeType.HIGH_CONTRAST:
|
|
144
|
+
return """
|
|
145
|
+
:root {
|
|
146
|
+
--bg-color: #000000;
|
|
147
|
+
--text-color: #ffffff;
|
|
148
|
+
--card-bg: #1a1a1a;
|
|
149
|
+
--stat-bg: #333333;
|
|
150
|
+
--header-bg: #1a1a1a;
|
|
151
|
+
--border-color: #ffffff;
|
|
152
|
+
--primary-color: #ffff00;
|
|
153
|
+
--muted-color: #cccccc;
|
|
154
|
+
--code-bg: #333333;
|
|
155
|
+
}
|
|
156
|
+
body { background: var(--bg-color); color: var(--text-color); }
|
|
157
|
+
"""
|
|
158
|
+
else: # Light, Professional, Minimal
|
|
159
|
+
return """
|
|
160
|
+
:root {
|
|
161
|
+
--bg-color: #f8fafc;
|
|
162
|
+
--text-color: #1e293b;
|
|
163
|
+
--card-bg: #ffffff;
|
|
164
|
+
--stat-bg: #f1f5f9;
|
|
165
|
+
--header-bg: #f8fafc;
|
|
166
|
+
--border-color: #e2e8f0;
|
|
167
|
+
--primary-color: #fd9e4b;
|
|
168
|
+
--muted-color: #64748b;
|
|
169
|
+
--code-bg: #f1f5f9;
|
|
170
|
+
}
|
|
171
|
+
body { background: var(--bg-color); color: var(--text-color); }
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def _render_header(self, data: ReportData, config: ReporterConfig) -> list[str]:
|
|
175
|
+
"""Render header section."""
|
|
176
|
+
status_class = "status-passed" if data.summary.passed else "status-failed"
|
|
177
|
+
status_text = "Passed" if data.summary.passed else "Failed"
|
|
178
|
+
|
|
179
|
+
return [
|
|
180
|
+
'<div class="header">',
|
|
181
|
+
f'<h1>{escape(config.title)}<span class="status-badge {status_class}">{status_text}</span></h1>',
|
|
182
|
+
f'<p>Source: <code>{escape(data.source_name or data.source_id)}</code></p>',
|
|
183
|
+
'</div>',
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
def _render_summary(self, data: ReportData) -> list[str]:
|
|
187
|
+
"""Render summary card."""
|
|
188
|
+
summary = data.summary
|
|
189
|
+
return [
|
|
190
|
+
'<div class="card">',
|
|
191
|
+
'<h2 class="card-title">Summary</h2>',
|
|
192
|
+
'<div class="stats-grid">',
|
|
193
|
+
f'<div class="stat-item"><div class="stat-value">{summary.total_issues}</div><div class="stat-label">Total Issues</div></div>',
|
|
194
|
+
f'<div class="stat-item"><div class="stat-value" style="color:#dc2626">{summary.critical_issues}</div><div class="stat-label">Critical</div></div>',
|
|
195
|
+
f'<div class="stat-item"><div class="stat-value" style="color:#ea580c">{summary.high_issues}</div><div class="stat-label">High</div></div>',
|
|
196
|
+
f'<div class="stat-item"><div class="stat-value" style="color:#ca8a04">{summary.medium_issues}</div><div class="stat-label">Medium</div></div>',
|
|
197
|
+
f'<div class="stat-item"><div class="stat-value" style="color:#2563eb">{summary.low_issues}</div><div class="stat-label">Low</div></div>',
|
|
198
|
+
'</div>',
|
|
199
|
+
'</div>',
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
def _render_statistics(self, data: ReportData) -> list[str]:
|
|
203
|
+
"""Render statistics card."""
|
|
204
|
+
stats = data.statistics
|
|
205
|
+
parts = [
|
|
206
|
+
'<div class="card">',
|
|
207
|
+
'<h2 class="card-title">Statistics</h2>',
|
|
208
|
+
'<div class="stats-grid">',
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
if stats.row_count is not None:
|
|
212
|
+
parts.append(f'<div class="stat-item"><div class="stat-value">{stats.row_count:,}</div><div class="stat-label">Rows</div></div>')
|
|
213
|
+
if stats.column_count is not None:
|
|
214
|
+
parts.append(f'<div class="stat-item"><div class="stat-value">{stats.column_count}</div><div class="stat-label">Columns</div></div>')
|
|
215
|
+
if stats.duration_ms is not None:
|
|
216
|
+
parts.append(f'<div class="stat-item"><div class="stat-value">{stats.duration_ms}</div><div class="stat-label">Duration (ms)</div></div>')
|
|
217
|
+
|
|
218
|
+
parts.extend([
|
|
219
|
+
'</div>',
|
|
220
|
+
'</div>',
|
|
221
|
+
])
|
|
222
|
+
return parts
|
|
223
|
+
|
|
224
|
+
def _render_issues(self, data: ReportData, config: ReporterConfig) -> list[str]:
|
|
225
|
+
"""Render issues table."""
|
|
226
|
+
parts = [
|
|
227
|
+
'<div class="card">',
|
|
228
|
+
'<h2 class="card-title">Issues</h2>',
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
if not data.issues:
|
|
232
|
+
parts.append('<p>No issues found!</p>')
|
|
233
|
+
else:
|
|
234
|
+
parts.append('<table class="issue-table">')
|
|
235
|
+
parts.append('<thead><tr>')
|
|
236
|
+
parts.append('<th>Severity</th><th>Column</th><th>Type</th><th>Message</th><th>Count</th>')
|
|
237
|
+
if config.include_samples:
|
|
238
|
+
parts.append('<th>Samples</th>')
|
|
239
|
+
parts.append('</tr></thead>')
|
|
240
|
+
parts.append('<tbody>')
|
|
241
|
+
|
|
242
|
+
for issue in data.issues:
|
|
243
|
+
severity_class = f"severity-{issue.severity.lower()}"
|
|
244
|
+
parts.append('<tr>')
|
|
245
|
+
parts.append(f'<td><span class="severity-badge {severity_class}">{escape(issue.severity.upper())}</span></td>')
|
|
246
|
+
parts.append(f'<td><code>{escape(issue.column or "N/A")}</code></td>')
|
|
247
|
+
parts.append(f'<td>{escape(issue.issue_type)}</td>')
|
|
248
|
+
parts.append(f'<td>{escape(issue.message)}</td>')
|
|
249
|
+
parts.append(f'<td>{issue.count}</td>')
|
|
250
|
+
if config.include_samples:
|
|
251
|
+
samples = ""
|
|
252
|
+
if issue.sample_values:
|
|
253
|
+
samples = ", ".join(escape(str(v)) for v in issue.sample_values[:config.max_sample_values])
|
|
254
|
+
parts.append(f'<td><code>{samples}</code></td>')
|
|
255
|
+
parts.append('</tr>')
|
|
256
|
+
|
|
257
|
+
parts.append('</tbody></table>')
|
|
258
|
+
|
|
259
|
+
parts.append('</div>')
|
|
260
|
+
return parts
|
|
261
|
+
|
|
262
|
+
def _render_footer(self) -> list[str]:
|
|
263
|
+
"""Render footer."""
|
|
264
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
265
|
+
return [
|
|
266
|
+
'<div class="footer">',
|
|
267
|
+
f'<p>Generated at {timestamp}</p>',
|
|
268
|
+
'<p>Powered by Truthound Dashboard</p>',
|
|
269
|
+
'</div>',
|
|
270
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Built-in JSON reporter.
|
|
2
|
+
|
|
3
|
+
Generates machine-readable JSON reports without external dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..interfaces import (
|
|
13
|
+
BaseReporter,
|
|
14
|
+
ReportData,
|
|
15
|
+
ReporterConfig,
|
|
16
|
+
ReportFormatType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BuiltinJSONReporter(BaseReporter[ReporterConfig]):
|
|
21
|
+
"""Built-in JSON report generator.
|
|
22
|
+
|
|
23
|
+
Produces structured JSON reports with complete validation data.
|
|
24
|
+
This is a fallback when truthound's JSONReporter is not available.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
indent: int | None = 2,
|
|
30
|
+
ensure_ascii: bool = False,
|
|
31
|
+
locale: str = "en",
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize JSON reporter.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
indent: Indentation for pretty printing. None for compact output.
|
|
37
|
+
ensure_ascii: Whether to escape non-ASCII characters.
|
|
38
|
+
locale: Locale (not used for JSON, but kept for interface consistency).
|
|
39
|
+
"""
|
|
40
|
+
super().__init__()
|
|
41
|
+
self._indent = indent
|
|
42
|
+
self._ensure_ascii = ensure_ascii
|
|
43
|
+
self._locale = locale
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def format(self) -> ReportFormatType:
|
|
47
|
+
return ReportFormatType.JSON
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def content_type(self) -> str:
|
|
51
|
+
return "application/json; charset=utf-8"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def file_extension(self) -> str:
|
|
55
|
+
return ".json"
|
|
56
|
+
|
|
57
|
+
async def _render_content(
|
|
58
|
+
self,
|
|
59
|
+
data: ReportData,
|
|
60
|
+
config: ReporterConfig,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Render JSON report content."""
|
|
63
|
+
# Process issues
|
|
64
|
+
issues = []
|
|
65
|
+
for issue in data.issues:
|
|
66
|
+
issue_dict = issue.to_dict()
|
|
67
|
+
# Remove sample values if not requested
|
|
68
|
+
if not config.include_samples:
|
|
69
|
+
issue_dict.pop("sample_values", None)
|
|
70
|
+
issues.append(issue_dict)
|
|
71
|
+
|
|
72
|
+
report_data: dict[str, Any] = {
|
|
73
|
+
"metadata": {
|
|
74
|
+
"title": config.title,
|
|
75
|
+
"generated_at": datetime.utcnow().isoformat(),
|
|
76
|
+
"format": self.format.value,
|
|
77
|
+
"theme": config.theme.value,
|
|
78
|
+
"locale": config.locale,
|
|
79
|
+
},
|
|
80
|
+
"validation": {
|
|
81
|
+
"id": data.validation_id,
|
|
82
|
+
"source_id": data.source_id,
|
|
83
|
+
"source_name": data.source_name,
|
|
84
|
+
"status": data.status,
|
|
85
|
+
"passed": data.summary.passed,
|
|
86
|
+
},
|
|
87
|
+
"summary": data.summary.to_dict(),
|
|
88
|
+
"issues": issues,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Add statistics if requested
|
|
92
|
+
if config.include_statistics:
|
|
93
|
+
report_data["statistics"] = data.statistics.to_dict()
|
|
94
|
+
|
|
95
|
+
# Add error info if present
|
|
96
|
+
if data.error_message:
|
|
97
|
+
report_data["error"] = {
|
|
98
|
+
"message": data.error_message,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Add custom metadata
|
|
102
|
+
if config.include_metadata and data.metadata:
|
|
103
|
+
report_data["metadata"].update(data.metadata)
|
|
104
|
+
|
|
105
|
+
return json.dumps(
|
|
106
|
+
report_data,
|
|
107
|
+
indent=self._indent,
|
|
108
|
+
ensure_ascii=self._ensure_ascii,
|
|
109
|
+
default=self._json_serializer,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _json_serializer(self, obj: Any) -> Any:
|
|
113
|
+
"""Custom JSON serializer for non-serializable objects."""
|
|
114
|
+
if isinstance(obj, datetime):
|
|
115
|
+
return obj.isoformat()
|
|
116
|
+
if hasattr(obj, "to_dict"):
|
|
117
|
+
return obj.to_dict()
|
|
118
|
+
if hasattr(obj, "__dict__"):
|
|
119
|
+
return obj.__dict__
|
|
120
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CompactJSONReporter(BuiltinJSONReporter):
|
|
124
|
+
"""Compact JSON reporter without formatting."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, locale: str = "en") -> None:
|
|
127
|
+
super().__init__(indent=None, ensure_ascii=False, locale=locale)
|