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.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
  164. truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
  166. truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {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
+ ]