truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"""HTML report generator.
|
|
2
|
+
|
|
3
|
+
Generates professional HTML reports with responsive design,
|
|
4
|
+
theme support, internationalization, and interactive features.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from html import escape
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from .base import Reporter, ReportFormat, ReportMetadata, ReportTheme
|
|
13
|
+
from .i18n import ReportLocalizer, SupportedLocale, get_localizer
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from truthound_dashboard.db.models import Validation
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTMLReporter(Reporter):
|
|
20
|
+
"""HTML report generator with theme and i18n support.
|
|
21
|
+
|
|
22
|
+
Produces standalone HTML documents with embedded CSS.
|
|
23
|
+
Supports multiple themes, responsive design, and 15 languages.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
reporter = HTMLReporter(locale="ko")
|
|
27
|
+
result = await reporter.generate(validation, theme=ReportTheme.DARK)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, locale: SupportedLocale | str = SupportedLocale.ENGLISH) -> None:
|
|
31
|
+
"""Initialize the HTML reporter.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
locale: Target locale for report generation.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
if isinstance(locale, str):
|
|
38
|
+
locale = SupportedLocale.from_string(locale)
|
|
39
|
+
self._locale = locale
|
|
40
|
+
self._localizer = get_localizer(locale)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def format(self) -> ReportFormat:
|
|
44
|
+
return ReportFormat.HTML
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def content_type(self) -> str:
|
|
48
|
+
return "text/html; charset=utf-8"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def file_extension(self) -> str:
|
|
52
|
+
return ".html"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def locale(self) -> SupportedLocale:
|
|
56
|
+
"""Get the current locale."""
|
|
57
|
+
return self._locale
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def localizer(self) -> ReportLocalizer:
|
|
61
|
+
"""Get the localizer instance."""
|
|
62
|
+
return self._localizer
|
|
63
|
+
|
|
64
|
+
async def _render_content(
|
|
65
|
+
self,
|
|
66
|
+
validation: Validation,
|
|
67
|
+
metadata: ReportMetadata,
|
|
68
|
+
include_samples: bool,
|
|
69
|
+
include_statistics: bool,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Render HTML report content."""
|
|
72
|
+
issues = self._extract_issues(validation)
|
|
73
|
+
theme = metadata.theme
|
|
74
|
+
t = self._localizer # Shorthand for translations
|
|
75
|
+
|
|
76
|
+
# Build HTML sections
|
|
77
|
+
css = self._generate_css(theme)
|
|
78
|
+
header = self._render_header(validation, metadata, t)
|
|
79
|
+
summary = self._render_summary(validation, theme, t)
|
|
80
|
+
statistics = (
|
|
81
|
+
self._render_statistics(validation, theme, t) if include_statistics else ""
|
|
82
|
+
)
|
|
83
|
+
issues_section = self._render_issues(issues, theme, include_samples, t)
|
|
84
|
+
footer = self._render_footer(metadata, t)
|
|
85
|
+
|
|
86
|
+
# Determine text direction for RTL languages
|
|
87
|
+
text_dir = t.text_direction
|
|
88
|
+
lang_code = self._locale.value
|
|
89
|
+
|
|
90
|
+
return f"""<!DOCTYPE html>
|
|
91
|
+
<html lang="{lang_code}" dir="{text_dir}">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="UTF-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
95
|
+
<title>{escape(metadata.title)}</title>
|
|
96
|
+
<style>
|
|
97
|
+
{css}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body class="theme-{theme.value}">
|
|
101
|
+
<div class="container">
|
|
102
|
+
{header}
|
|
103
|
+
{summary}
|
|
104
|
+
{statistics}
|
|
105
|
+
{issues_section}
|
|
106
|
+
{footer}
|
|
107
|
+
</div>
|
|
108
|
+
</body>
|
|
109
|
+
</html>"""
|
|
110
|
+
|
|
111
|
+
def _generate_css(self, theme: ReportTheme) -> str:
|
|
112
|
+
"""Generate CSS based on theme."""
|
|
113
|
+
# Base colors for themes
|
|
114
|
+
theme_colors = {
|
|
115
|
+
ReportTheme.LIGHT: {
|
|
116
|
+
"bg": "#ffffff",
|
|
117
|
+
"text": "#1f2937",
|
|
118
|
+
"text-muted": "#6b7280",
|
|
119
|
+
"border": "#e5e7eb",
|
|
120
|
+
"card-bg": "#f9fafb",
|
|
121
|
+
"primary": "#fd9e4b",
|
|
122
|
+
"success": "#10b981",
|
|
123
|
+
"danger": "#ef4444",
|
|
124
|
+
},
|
|
125
|
+
ReportTheme.DARK: {
|
|
126
|
+
"bg": "#1f2937",
|
|
127
|
+
"text": "#f9fafb",
|
|
128
|
+
"text-muted": "#9ca3af",
|
|
129
|
+
"border": "#374151",
|
|
130
|
+
"card-bg": "#111827",
|
|
131
|
+
"primary": "#fd9e4b",
|
|
132
|
+
"success": "#34d399",
|
|
133
|
+
"danger": "#f87171",
|
|
134
|
+
},
|
|
135
|
+
ReportTheme.PROFESSIONAL: {
|
|
136
|
+
"bg": "#f8fafc",
|
|
137
|
+
"text": "#0f172a",
|
|
138
|
+
"text-muted": "#64748b",
|
|
139
|
+
"border": "#cbd5e1",
|
|
140
|
+
"card-bg": "#ffffff",
|
|
141
|
+
"primary": "#fd9e4b",
|
|
142
|
+
"success": "#059669",
|
|
143
|
+
"danger": "#dc2626",
|
|
144
|
+
},
|
|
145
|
+
ReportTheme.MINIMAL: {
|
|
146
|
+
"bg": "#ffffff",
|
|
147
|
+
"text": "#000000",
|
|
148
|
+
"text-muted": "#666666",
|
|
149
|
+
"border": "#e0e0e0",
|
|
150
|
+
"card-bg": "#fafafa",
|
|
151
|
+
"primary": "#fd9e4b",
|
|
152
|
+
"success": "#22c55e",
|
|
153
|
+
"danger": "#ef4444",
|
|
154
|
+
},
|
|
155
|
+
ReportTheme.HIGH_CONTRAST: {
|
|
156
|
+
"bg": "#000000",
|
|
157
|
+
"text": "#ffffff",
|
|
158
|
+
"text-muted": "#e0e0e0",
|
|
159
|
+
"border": "#ffffff",
|
|
160
|
+
"card-bg": "#1a1a1a",
|
|
161
|
+
"primary": "#ffb347",
|
|
162
|
+
"success": "#00ff00",
|
|
163
|
+
"danger": "#ff0000",
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
c = theme_colors.get(theme, theme_colors[ReportTheme.PROFESSIONAL])
|
|
168
|
+
|
|
169
|
+
# RTL support
|
|
170
|
+
rtl_css = ""
|
|
171
|
+
if self._locale.is_rtl:
|
|
172
|
+
rtl_css = """
|
|
173
|
+
[dir="rtl"] .stat-row {
|
|
174
|
+
flex-direction: row-reverse;
|
|
175
|
+
}
|
|
176
|
+
[dir="rtl"] th, [dir="rtl"] td {
|
|
177
|
+
text-align: right;
|
|
178
|
+
}
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
return f"""
|
|
182
|
+
* {{
|
|
183
|
+
margin: 0;
|
|
184
|
+
padding: 0;
|
|
185
|
+
box-sizing: border-box;
|
|
186
|
+
}}
|
|
187
|
+
|
|
188
|
+
body {{
|
|
189
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
190
|
+
'Helvetica Neue', Arial, sans-serif;
|
|
191
|
+
background-color: {c['bg']};
|
|
192
|
+
color: {c['text']};
|
|
193
|
+
line-height: 1.6;
|
|
194
|
+
padding: 2rem;
|
|
195
|
+
}}
|
|
196
|
+
|
|
197
|
+
.container {{
|
|
198
|
+
max-width: 1200px;
|
|
199
|
+
margin: 0 auto;
|
|
200
|
+
}}
|
|
201
|
+
|
|
202
|
+
.header {{
|
|
203
|
+
text-align: center;
|
|
204
|
+
margin-bottom: 2rem;
|
|
205
|
+
padding-bottom: 1rem;
|
|
206
|
+
border-bottom: 2px solid {c['primary']};
|
|
207
|
+
}}
|
|
208
|
+
|
|
209
|
+
.header h1 {{
|
|
210
|
+
font-size: 2rem;
|
|
211
|
+
margin-bottom: 0.5rem;
|
|
212
|
+
color: {c['primary']};
|
|
213
|
+
}}
|
|
214
|
+
|
|
215
|
+
.header .subtitle {{
|
|
216
|
+
color: {c['text-muted']};
|
|
217
|
+
font-size: 0.95rem;
|
|
218
|
+
}}
|
|
219
|
+
|
|
220
|
+
.card {{
|
|
221
|
+
background: {c['card-bg']};
|
|
222
|
+
border: 1px solid {c['border']};
|
|
223
|
+
border-radius: 8px;
|
|
224
|
+
padding: 1.5rem;
|
|
225
|
+
margin-bottom: 1.5rem;
|
|
226
|
+
}}
|
|
227
|
+
|
|
228
|
+
.card-title {{
|
|
229
|
+
font-size: 1.25rem;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
margin-bottom: 1rem;
|
|
232
|
+
padding-bottom: 0.5rem;
|
|
233
|
+
border-bottom: 1px solid {c['border']};
|
|
234
|
+
}}
|
|
235
|
+
|
|
236
|
+
.status-badge {{
|
|
237
|
+
display: inline-block;
|
|
238
|
+
padding: 0.5rem 1rem;
|
|
239
|
+
border-radius: 9999px;
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
font-size: 1.1rem;
|
|
242
|
+
}}
|
|
243
|
+
|
|
244
|
+
.status-passed {{
|
|
245
|
+
background: {c['success']}20;
|
|
246
|
+
color: {c['success']};
|
|
247
|
+
}}
|
|
248
|
+
|
|
249
|
+
.status-failed {{
|
|
250
|
+
background: {c['danger']}20;
|
|
251
|
+
color: {c['danger']};
|
|
252
|
+
}}
|
|
253
|
+
|
|
254
|
+
.summary-grid {{
|
|
255
|
+
display: grid;
|
|
256
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
257
|
+
gap: 1rem;
|
|
258
|
+
margin-top: 1rem;
|
|
259
|
+
}}
|
|
260
|
+
|
|
261
|
+
.summary-item {{
|
|
262
|
+
text-align: center;
|
|
263
|
+
padding: 1rem;
|
|
264
|
+
background: {c['bg']};
|
|
265
|
+
border-radius: 8px;
|
|
266
|
+
border: 1px solid {c['border']};
|
|
267
|
+
}}
|
|
268
|
+
|
|
269
|
+
.summary-item .value {{
|
|
270
|
+
font-size: 2rem;
|
|
271
|
+
font-weight: 700;
|
|
272
|
+
display: block;
|
|
273
|
+
}}
|
|
274
|
+
|
|
275
|
+
.summary-item .label {{
|
|
276
|
+
color: {c['text-muted']};
|
|
277
|
+
font-size: 0.875rem;
|
|
278
|
+
}}
|
|
279
|
+
|
|
280
|
+
.severity-critical {{ color: #dc2626; }}
|
|
281
|
+
.severity-high {{ color: #ea580c; }}
|
|
282
|
+
.severity-medium {{ color: #ca8a04; }}
|
|
283
|
+
.severity-low {{ color: #2563eb; }}
|
|
284
|
+
|
|
285
|
+
table {{
|
|
286
|
+
width: 100%;
|
|
287
|
+
border-collapse: collapse;
|
|
288
|
+
margin-top: 1rem;
|
|
289
|
+
}}
|
|
290
|
+
|
|
291
|
+
th, td {{
|
|
292
|
+
padding: 0.75rem 1rem;
|
|
293
|
+
text-align: left;
|
|
294
|
+
border-bottom: 1px solid {c['border']};
|
|
295
|
+
}}
|
|
296
|
+
|
|
297
|
+
th {{
|
|
298
|
+
background: {c['bg']};
|
|
299
|
+
font-weight: 600;
|
|
300
|
+
color: {c['text-muted']};
|
|
301
|
+
font-size: 0.875rem;
|
|
302
|
+
text-transform: uppercase;
|
|
303
|
+
letter-spacing: 0.05em;
|
|
304
|
+
}}
|
|
305
|
+
|
|
306
|
+
tr:hover {{
|
|
307
|
+
background: {c['card-bg']};
|
|
308
|
+
}}
|
|
309
|
+
|
|
310
|
+
.severity-badge {{
|
|
311
|
+
display: inline-block;
|
|
312
|
+
padding: 0.25rem 0.75rem;
|
|
313
|
+
border-radius: 9999px;
|
|
314
|
+
font-size: 0.75rem;
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
text-transform: uppercase;
|
|
317
|
+
}}
|
|
318
|
+
|
|
319
|
+
.badge-critical {{
|
|
320
|
+
background: #dc262620;
|
|
321
|
+
color: #dc2626;
|
|
322
|
+
}}
|
|
323
|
+
|
|
324
|
+
.badge-high {{
|
|
325
|
+
background: #ea580c20;
|
|
326
|
+
color: #ea580c;
|
|
327
|
+
}}
|
|
328
|
+
|
|
329
|
+
.badge-medium {{
|
|
330
|
+
background: #ca8a0420;
|
|
331
|
+
color: #ca8a04;
|
|
332
|
+
}}
|
|
333
|
+
|
|
334
|
+
.badge-low {{
|
|
335
|
+
background: #2563eb20;
|
|
336
|
+
color: #2563eb;
|
|
337
|
+
}}
|
|
338
|
+
|
|
339
|
+
.samples {{
|
|
340
|
+
margin-top: 0.5rem;
|
|
341
|
+
padding: 0.5rem;
|
|
342
|
+
background: {c['bg']};
|
|
343
|
+
border-radius: 4px;
|
|
344
|
+
font-family: monospace;
|
|
345
|
+
font-size: 0.875rem;
|
|
346
|
+
color: {c['text-muted']};
|
|
347
|
+
}}
|
|
348
|
+
|
|
349
|
+
.footer {{
|
|
350
|
+
text-align: center;
|
|
351
|
+
padding-top: 1.5rem;
|
|
352
|
+
margin-top: 2rem;
|
|
353
|
+
border-top: 1px solid {c['border']};
|
|
354
|
+
color: {c['text-muted']};
|
|
355
|
+
font-size: 0.875rem;
|
|
356
|
+
}}
|
|
357
|
+
|
|
358
|
+
.stats-grid {{
|
|
359
|
+
display: grid;
|
|
360
|
+
grid-template-columns: repeat(2, 1fr);
|
|
361
|
+
gap: 0.5rem;
|
|
362
|
+
}}
|
|
363
|
+
|
|
364
|
+
.stat-row {{
|
|
365
|
+
display: flex;
|
|
366
|
+
justify-content: space-between;
|
|
367
|
+
padding: 0.5rem 0;
|
|
368
|
+
border-bottom: 1px solid {c['border']};
|
|
369
|
+
}}
|
|
370
|
+
|
|
371
|
+
.stat-row:last-child {{
|
|
372
|
+
border-bottom: none;
|
|
373
|
+
}}
|
|
374
|
+
|
|
375
|
+
@media (max-width: 768px) {{
|
|
376
|
+
body {{
|
|
377
|
+
padding: 1rem;
|
|
378
|
+
}}
|
|
379
|
+
|
|
380
|
+
.summary-grid {{
|
|
381
|
+
grid-template-columns: repeat(2, 1fr);
|
|
382
|
+
}}
|
|
383
|
+
|
|
384
|
+
.stats-grid {{
|
|
385
|
+
grid-template-columns: 1fr;
|
|
386
|
+
}}
|
|
387
|
+
|
|
388
|
+
table {{
|
|
389
|
+
display: block;
|
|
390
|
+
overflow-x: auto;
|
|
391
|
+
}}
|
|
392
|
+
}}
|
|
393
|
+
|
|
394
|
+
@media print {{
|
|
395
|
+
body {{
|
|
396
|
+
padding: 0;
|
|
397
|
+
}}
|
|
398
|
+
|
|
399
|
+
.card {{
|
|
400
|
+
break-inside: avoid;
|
|
401
|
+
}}
|
|
402
|
+
}}
|
|
403
|
+
{rtl_css}
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def _render_header(
|
|
407
|
+
self,
|
|
408
|
+
validation: Validation,
|
|
409
|
+
metadata: ReportMetadata,
|
|
410
|
+
t: ReportLocalizer,
|
|
411
|
+
) -> str:
|
|
412
|
+
"""Render report header."""
|
|
413
|
+
source_name = escape(metadata.source_name or validation.source_id)
|
|
414
|
+
generated = t.format_date(metadata.generated_at)
|
|
415
|
+
|
|
416
|
+
return f"""
|
|
417
|
+
<header class="header">
|
|
418
|
+
<h1>{escape(metadata.title)}</h1>
|
|
419
|
+
<p class="subtitle">
|
|
420
|
+
{t.t("report.source")}: <strong>{source_name}</strong> |
|
|
421
|
+
{t.t("report.generated_at")}: {generated}
|
|
422
|
+
</p>
|
|
423
|
+
</header>
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
def _render_summary(
|
|
427
|
+
self,
|
|
428
|
+
validation: Validation,
|
|
429
|
+
theme: ReportTheme,
|
|
430
|
+
t: ReportLocalizer,
|
|
431
|
+
) -> str:
|
|
432
|
+
"""Render validation summary card."""
|
|
433
|
+
passed = validation.passed
|
|
434
|
+
status_class = "status-passed" if passed else "status-failed"
|
|
435
|
+
|
|
436
|
+
if passed is None:
|
|
437
|
+
status_text = f"⏳ {t.t('status.pending')}"
|
|
438
|
+
elif passed:
|
|
439
|
+
status_text = f"✅ {t.t('status.passed')}"
|
|
440
|
+
else:
|
|
441
|
+
status_text = f"❌ {t.t('status.failed')}"
|
|
442
|
+
|
|
443
|
+
return f"""
|
|
444
|
+
<section class="card">
|
|
445
|
+
<h2 class="card-title">{t.t("summary.title")}</h2>
|
|
446
|
+
<div style="text-align: center; margin-bottom: 1rem;">
|
|
447
|
+
<span class="status-badge {status_class}">{status_text}</span>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="summary-grid">
|
|
450
|
+
<div class="summary-item">
|
|
451
|
+
<span class="value">{t.format_number(validation.total_issues or 0)}</span>
|
|
452
|
+
<span class="label">{t.t("summary.total_issues")}</span>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="summary-item">
|
|
455
|
+
<span class="value severity-critical">{t.format_number(validation.critical_issues or 0)}</span>
|
|
456
|
+
<span class="label">{t.t("severity.critical")}</span>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="summary-item">
|
|
459
|
+
<span class="value severity-high">{t.format_number(validation.high_issues or 0)}</span>
|
|
460
|
+
<span class="label">{t.t("severity.high")}</span>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="summary-item">
|
|
463
|
+
<span class="value severity-medium">{t.format_number(validation.medium_issues or 0)}</span>
|
|
464
|
+
<span class="label">{t.t("severity.medium")}</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="summary-item">
|
|
467
|
+
<span class="value severity-low">{t.format_number(validation.low_issues or 0)}</span>
|
|
468
|
+
<span class="label">{t.t("severity.low")}</span>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
</section>
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
def _render_statistics(
|
|
475
|
+
self,
|
|
476
|
+
validation: Validation,
|
|
477
|
+
theme: ReportTheme,
|
|
478
|
+
t: ReportLocalizer,
|
|
479
|
+
) -> str:
|
|
480
|
+
"""Render data statistics card."""
|
|
481
|
+
duration = validation.duration_ms
|
|
482
|
+
na_text = t.t("statistics.na")
|
|
483
|
+
|
|
484
|
+
if duration:
|
|
485
|
+
duration_str = t.format("time.seconds", value=f"{duration / 1000:.2f}")
|
|
486
|
+
else:
|
|
487
|
+
duration_str = na_text
|
|
488
|
+
|
|
489
|
+
started = t.format_date(validation.started_at) if validation.started_at else na_text
|
|
490
|
+
completed = t.format_date(validation.completed_at) if validation.completed_at else na_text
|
|
491
|
+
|
|
492
|
+
row_count = t.format_number(validation.row_count) if validation.row_count else na_text
|
|
493
|
+
col_count = t.format_number(validation.column_count) if validation.column_count else na_text
|
|
494
|
+
|
|
495
|
+
return f"""
|
|
496
|
+
<section class="card">
|
|
497
|
+
<h2 class="card-title">{t.t("statistics.title")}</h2>
|
|
498
|
+
<div class="stats-grid">
|
|
499
|
+
<div class="stat-row">
|
|
500
|
+
<span>{t.t("statistics.row_count")}</span>
|
|
501
|
+
<strong>{row_count}</strong>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="stat-row">
|
|
504
|
+
<span>{t.t("statistics.column_count")}</span>
|
|
505
|
+
<strong>{col_count}</strong>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="stat-row">
|
|
508
|
+
<span>{t.t("statistics.duration")}</span>
|
|
509
|
+
<strong>{duration_str}</strong>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="stat-row">
|
|
512
|
+
<span>{t.t("summary.status")}</span>
|
|
513
|
+
<strong>{escape(validation.status)}</strong>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="stat-row">
|
|
516
|
+
<span>{t.t("statistics.started_at")}</span>
|
|
517
|
+
<strong>{started}</strong>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="stat-row">
|
|
520
|
+
<span>{t.t("statistics.completed_at")}</span>
|
|
521
|
+
<strong>{completed}</strong>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
</section>
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
def _render_issues(
|
|
528
|
+
self,
|
|
529
|
+
issues: list[dict[str, Any]],
|
|
530
|
+
theme: ReportTheme,
|
|
531
|
+
include_samples: bool,
|
|
532
|
+
t: ReportLocalizer,
|
|
533
|
+
) -> str:
|
|
534
|
+
"""Render issues table."""
|
|
535
|
+
if not issues:
|
|
536
|
+
return f"""
|
|
537
|
+
<section class="card">
|
|
538
|
+
<h2 class="card-title">{t.t("issues.title")}</h2>
|
|
539
|
+
<p style="text-align: center; color: var(--text-muted); padding: 2rem;">
|
|
540
|
+
{t.t("issues.no_issues")}
|
|
541
|
+
</p>
|
|
542
|
+
</section>
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
rows = []
|
|
546
|
+
for issue in issues:
|
|
547
|
+
severity = issue.get("severity", "medium").lower()
|
|
548
|
+
badge_class = f"badge-{severity}"
|
|
549
|
+
severity_label = t.t(f"severity.{severity}")
|
|
550
|
+
|
|
551
|
+
samples_html = ""
|
|
552
|
+
if include_samples and issue.get("sample_values"):
|
|
553
|
+
samples = [str(v)[:50] for v in issue["sample_values"][:5]]
|
|
554
|
+
samples_html = f'<div class="samples">{escape(", ".join(samples))}</div>'
|
|
555
|
+
|
|
556
|
+
rows.append(f"""
|
|
557
|
+
<tr>
|
|
558
|
+
<td>{escape(issue.get('column', t.t('statistics.na')))}</td>
|
|
559
|
+
<td>{escape(issue.get('issue_type', 'Unknown'))}</td>
|
|
560
|
+
<td><span class="severity-badge {badge_class}">{severity_label}</span></td>
|
|
561
|
+
<td>{t.format_number(issue.get('count', 0))}</td>
|
|
562
|
+
<td>
|
|
563
|
+
{escape(issue.get('details', '') or '')}
|
|
564
|
+
{samples_html}
|
|
565
|
+
</td>
|
|
566
|
+
</tr>
|
|
567
|
+
""")
|
|
568
|
+
|
|
569
|
+
issue_count_text = t.plural("issues.count", len(issues))
|
|
570
|
+
|
|
571
|
+
return f"""
|
|
572
|
+
<section class="card">
|
|
573
|
+
<h2 class="card-title">{t.t("issues.title")} ({issue_count_text})</h2>
|
|
574
|
+
<table>
|
|
575
|
+
<thead>
|
|
576
|
+
<tr>
|
|
577
|
+
<th>{t.t("issues.column")}</th>
|
|
578
|
+
<th>{t.t("issues.type")}</th>
|
|
579
|
+
<th>{t.t("issues.severity")}</th>
|
|
580
|
+
<th>{t.t("issues.count")}</th>
|
|
581
|
+
<th>{t.t("issues.details")}</th>
|
|
582
|
+
</tr>
|
|
583
|
+
</thead>
|
|
584
|
+
<tbody>
|
|
585
|
+
{''.join(rows)}
|
|
586
|
+
</tbody>
|
|
587
|
+
</table>
|
|
588
|
+
</section>
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
def _render_footer(self, metadata: ReportMetadata, t: ReportLocalizer) -> str:
|
|
592
|
+
"""Render report footer."""
|
|
593
|
+
return f"""
|
|
594
|
+
<footer class="footer">
|
|
595
|
+
<p>{t.t("report.generated_by")}</p>
|
|
596
|
+
<p>{t.t("report.validation_id")}: {escape(metadata.validation_id or t.t("statistics.na"))}</p>
|
|
597
|
+
</footer>
|
|
598
|
+
"""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Report internationalization (i18n) module.
|
|
2
|
+
|
|
3
|
+
This module provides localization support for report generation,
|
|
4
|
+
allowing reports to be generated in multiple languages.
|
|
5
|
+
|
|
6
|
+
Supports 15 languages as specified in truthound documentation:
|
|
7
|
+
en, ko, ja, zh, de, fr, es, pt, it, ru, ar, th, vi, id, tr
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
from truthound_dashboard.core.reporters.i18n import get_localizer, SupportedLocale
|
|
11
|
+
|
|
12
|
+
localizer = get_localizer(SupportedLocale.KOREAN)
|
|
13
|
+
title = localizer.get("report.title") # "검증 리포트"
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .base import (
|
|
17
|
+
ReportLocalizer,
|
|
18
|
+
SupportedLocale,
|
|
19
|
+
get_localizer,
|
|
20
|
+
get_supported_locales,
|
|
21
|
+
LocalizationRegistry,
|
|
22
|
+
)
|
|
23
|
+
from .catalogs import (
|
|
24
|
+
ENGLISH_CATALOG,
|
|
25
|
+
KOREAN_CATALOG,
|
|
26
|
+
JAPANESE_CATALOG,
|
|
27
|
+
CHINESE_CATALOG,
|
|
28
|
+
GERMAN_CATALOG,
|
|
29
|
+
FRENCH_CATALOG,
|
|
30
|
+
SPANISH_CATALOG,
|
|
31
|
+
PORTUGUESE_CATALOG,
|
|
32
|
+
ITALIAN_CATALOG,
|
|
33
|
+
RUSSIAN_CATALOG,
|
|
34
|
+
ARABIC_CATALOG,
|
|
35
|
+
THAI_CATALOG,
|
|
36
|
+
VIETNAMESE_CATALOG,
|
|
37
|
+
INDONESIAN_CATALOG,
|
|
38
|
+
TURKISH_CATALOG,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Core classes
|
|
43
|
+
"ReportLocalizer",
|
|
44
|
+
"SupportedLocale",
|
|
45
|
+
"LocalizationRegistry",
|
|
46
|
+
# Factory functions
|
|
47
|
+
"get_localizer",
|
|
48
|
+
"get_supported_locales",
|
|
49
|
+
# Catalogs (for extension)
|
|
50
|
+
"ENGLISH_CATALOG",
|
|
51
|
+
"KOREAN_CATALOG",
|
|
52
|
+
"JAPANESE_CATALOG",
|
|
53
|
+
"CHINESE_CATALOG",
|
|
54
|
+
"GERMAN_CATALOG",
|
|
55
|
+
"FRENCH_CATALOG",
|
|
56
|
+
"SPANISH_CATALOG",
|
|
57
|
+
"PORTUGUESE_CATALOG",
|
|
58
|
+
"ITALIAN_CATALOG",
|
|
59
|
+
"RUSSIAN_CATALOG",
|
|
60
|
+
"ARABIC_CATALOG",
|
|
61
|
+
"THAI_CATALOG",
|
|
62
|
+
"VIETNAMESE_CATALOG",
|
|
63
|
+
"INDONESIAN_CATALOG",
|
|
64
|
+
"TURKISH_CATALOG",
|
|
65
|
+
]
|