truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +645 -23
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +15 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -22,9 +22,6 @@ class ReportFormat(str, Enum):
22
22
  HTML = "html"
23
23
  CSV = "csv"
24
24
  JSON = "json"
25
- MARKDOWN = "markdown"
26
- PDF = "pdf"
27
- JUNIT = "junit" # JUnit XML for CI/CD integration
28
25
 
29
26
  @classmethod
30
27
  def from_string(cls, value: str) -> ReportFormat:
@@ -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)