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.
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 +437 -10
  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 +11 -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.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.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.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,745 @@
1
+ """Reporter interface definitions.
2
+
3
+ This module defines the abstract interfaces for the reporter system,
4
+ enabling loose coupling with truthound and other reporting backends.
5
+
6
+ The interface design follows these principles:
7
+ 1. Protocol-based typing for flexible duck typing
8
+ 2. Backend-agnostic data structures
9
+ 3. Adapter pattern for external reporter integration
10
+ 4. Factory pattern for reporter instantiation
11
+
12
+ Example:
13
+ from truthound_dashboard.core.reporters.interfaces import (
14
+ ReporterProtocol,
15
+ ReporterConfig,
16
+ ReportData,
17
+ )
18
+
19
+ class MyReporter:
20
+ # Duck typing - no inheritance required
21
+ def render(self, data: ReportData, config: ReporterConfig) -> str:
22
+ ...
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from abc import ABC, abstractmethod
28
+ from dataclasses import dataclass, field
29
+ from datetime import datetime
30
+ from enum import Enum
31
+ from typing import (
32
+ TYPE_CHECKING,
33
+ Any,
34
+ Generic,
35
+ Protocol,
36
+ TypeVar,
37
+ runtime_checkable,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ pass
42
+
43
+
44
+ class ReportFormatType(str, Enum):
45
+ """Supported report output formats.
46
+
47
+ This enum is backend-agnostic and maps to specific implementations.
48
+ Includes both standard formats and CI-specific formats.
49
+ """
50
+
51
+ # Standard formats
52
+ HTML = "html"
53
+ CSV = "csv"
54
+ JSON = "json"
55
+ YAML = "yaml"
56
+ NDJSON = "ndjson"
57
+ CONSOLE = "console"
58
+ TABLE = "table"
59
+
60
+ # CI platform formats (auto-detected or specific)
61
+ CI = "ci" # Auto-detect CI platform
62
+ GITHUB = "github" # GitHub Actions
63
+ GITLAB = "gitlab" # GitLab CI
64
+ JENKINS = "jenkins" # Jenkins
65
+ AZURE = "azure" # Azure DevOps
66
+ CIRCLECI = "circleci" # CircleCI
67
+ BITBUCKET = "bitbucket" # Bitbucket Pipelines
68
+ TRAVIS = "travis" # Travis CI
69
+ TEAMCITY = "teamcity" # TeamCity
70
+ BUILDKITE = "buildkite" # Buildkite
71
+ DRONE = "drone" # Drone CI
72
+
73
+ @classmethod
74
+ def from_string(cls, value: str) -> ReportFormatType:
75
+ """Parse format from string (case-insensitive)."""
76
+ value_lower = value.lower()
77
+ for fmt in cls:
78
+ if fmt.value == value_lower:
79
+ return fmt
80
+ raise ValueError(
81
+ f"Unknown report format: {value}. "
82
+ f"Supported formats: {[f.value for f in cls]}"
83
+ )
84
+
85
+ @classmethod
86
+ def is_ci_format(cls, format: "ReportFormatType") -> bool:
87
+ """Check if this is a CI-specific format."""
88
+ return format in {
89
+ cls.CI,
90
+ cls.GITHUB,
91
+ cls.GITLAB,
92
+ cls.JENKINS,
93
+ cls.AZURE,
94
+ cls.CIRCLECI,
95
+ cls.BITBUCKET,
96
+ cls.TRAVIS,
97
+ cls.TEAMCITY,
98
+ cls.BUILDKITE,
99
+ cls.DRONE,
100
+ }
101
+
102
+
103
+ class ReportThemeType(str, Enum):
104
+ """Report visual themes."""
105
+
106
+ LIGHT = "light"
107
+ DARK = "dark"
108
+ PROFESSIONAL = "professional"
109
+ MINIMAL = "minimal"
110
+ HIGH_CONTRAST = "high_contrast"
111
+
112
+
113
+ @dataclass
114
+ class ReporterConfig:
115
+ """Configuration for report generation.
116
+
117
+ This is a backend-agnostic configuration that can be
118
+ translated to specific backend configurations.
119
+
120
+ Attributes:
121
+ title: Report title.
122
+ theme: Visual theme.
123
+ locale: Language locale code (e.g., 'en', 'ko', 'ja').
124
+ include_samples: Include sample values in output.
125
+ include_statistics: Include statistics section.
126
+ include_metadata: Include report metadata.
127
+ max_sample_values: Maximum sample values to include.
128
+ timestamp_format: Date/time format string.
129
+ custom_options: Backend-specific options.
130
+ """
131
+
132
+ title: str = "Validation Report"
133
+ theme: ReportThemeType = ReportThemeType.PROFESSIONAL
134
+ locale: str = "en"
135
+ include_samples: bool = True
136
+ include_statistics: bool = True
137
+ include_metadata: bool = True
138
+ max_sample_values: int = 5
139
+ timestamp_format: str = "%Y-%m-%d %H:%M:%S"
140
+ custom_options: dict[str, Any] = field(default_factory=dict)
141
+
142
+ def with_option(self, key: str, value: Any) -> ReporterConfig:
143
+ """Return a new config with an additional option."""
144
+ new_options = {**self.custom_options, key: value}
145
+ return ReporterConfig(
146
+ title=self.title,
147
+ theme=self.theme,
148
+ locale=self.locale,
149
+ include_samples=self.include_samples,
150
+ include_statistics=self.include_statistics,
151
+ include_metadata=self.include_metadata,
152
+ max_sample_values=self.max_sample_values,
153
+ timestamp_format=self.timestamp_format,
154
+ custom_options=new_options,
155
+ )
156
+
157
+
158
+ @dataclass
159
+ class ValidationIssueData:
160
+ """Backend-agnostic validation issue representation.
161
+
162
+ This data class standardizes issue data from various sources
163
+ (truthound, custom validators, external systems).
164
+ """
165
+
166
+ column: str | None
167
+ issue_type: str
168
+ severity: str # low, medium, high, critical
169
+ message: str
170
+ count: int = 1
171
+ expected: Any = None
172
+ actual: Any = None
173
+ sample_values: list[Any] | None = None
174
+ validator_name: str | None = None
175
+ details: dict[str, Any] | None = None
176
+
177
+ def to_dict(self) -> dict[str, Any]:
178
+ """Convert to dictionary."""
179
+ result = {
180
+ "column": self.column,
181
+ "issue_type": self.issue_type,
182
+ "severity": self.severity,
183
+ "message": self.message,
184
+ "count": self.count,
185
+ }
186
+ if self.expected is not None:
187
+ result["expected"] = self.expected
188
+ if self.actual is not None:
189
+ result["actual"] = self.actual
190
+ if self.sample_values:
191
+ result["sample_values"] = self.sample_values
192
+ if self.validator_name:
193
+ result["validator_name"] = self.validator_name
194
+ if self.details:
195
+ result["details"] = self.details
196
+ return result
197
+
198
+
199
+ @dataclass
200
+ class ValidationSummary:
201
+ """Summary statistics for validation results."""
202
+
203
+ total_issues: int = 0
204
+ critical_issues: int = 0
205
+ high_issues: int = 0
206
+ medium_issues: int = 0
207
+ low_issues: int = 0
208
+ passed: bool = True
209
+
210
+ @property
211
+ def has_critical(self) -> bool:
212
+ return self.critical_issues > 0
213
+
214
+ @property
215
+ def has_high(self) -> bool:
216
+ return self.high_issues > 0
217
+
218
+ def to_dict(self) -> dict[str, Any]:
219
+ """Convert to dictionary."""
220
+ return {
221
+ "total_issues": self.total_issues,
222
+ "critical_issues": self.critical_issues,
223
+ "high_issues": self.high_issues,
224
+ "medium_issues": self.medium_issues,
225
+ "low_issues": self.low_issues,
226
+ "passed": self.passed,
227
+ "has_critical": self.has_critical,
228
+ "has_high": self.has_high,
229
+ }
230
+
231
+
232
+ @dataclass
233
+ class DataStatistics:
234
+ """Data statistics for reports."""
235
+
236
+ row_count: int | None = None
237
+ column_count: int | None = None
238
+ duration_ms: int | None = None
239
+ started_at: datetime | None = None
240
+ completed_at: datetime | None = None
241
+
242
+ def to_dict(self) -> dict[str, Any]:
243
+ """Convert to dictionary."""
244
+ return {
245
+ "row_count": self.row_count,
246
+ "column_count": self.column_count,
247
+ "duration_ms": self.duration_ms,
248
+ "started_at": self.started_at.isoformat() if self.started_at else None,
249
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
250
+ }
251
+
252
+
253
+ @dataclass
254
+ class ReportData:
255
+ """Backend-agnostic data container for report generation.
256
+
257
+ This data class serves as the standardized input for all reporters,
258
+ regardless of the original data source (truthound, database, etc.).
259
+
260
+ Attributes:
261
+ validation_id: Unique identifier for the validation run.
262
+ source_id: Data source identifier.
263
+ source_name: Human-readable source name.
264
+ issues: List of validation issues.
265
+ summary: Validation summary statistics.
266
+ statistics: Data statistics.
267
+ status: Validation status string.
268
+ error_message: Error message if validation failed.
269
+ metadata: Additional metadata.
270
+ raw_data: Original raw data (for backends that need it).
271
+ """
272
+
273
+ validation_id: str
274
+ source_id: str
275
+ source_name: str | None = None
276
+ issues: list[ValidationIssueData] = field(default_factory=list)
277
+ summary: ValidationSummary = field(default_factory=ValidationSummary)
278
+ statistics: DataStatistics = field(default_factory=DataStatistics)
279
+ status: str = "completed"
280
+ error_message: str | None = None
281
+ metadata: dict[str, Any] = field(default_factory=dict)
282
+ raw_data: Any = None # Original truthound Report/ValidationResult if available
283
+
284
+ @classmethod
285
+ def from_validation_model(cls, validation: Any) -> ReportData:
286
+ """Create ReportData from a Validation database model.
287
+
288
+ Args:
289
+ validation: Validation model from database.
290
+
291
+ Returns:
292
+ ReportData instance.
293
+ """
294
+ # Extract issues from result_json
295
+ issues = []
296
+ if validation.result_json and "issues" in validation.result_json:
297
+ for issue_dict in validation.result_json["issues"]:
298
+ issues.append(
299
+ ValidationIssueData(
300
+ column=issue_dict.get("column"),
301
+ issue_type=issue_dict.get("issue_type", "unknown"),
302
+ severity=issue_dict.get("severity", "medium"),
303
+ message=issue_dict.get("message", ""),
304
+ count=issue_dict.get("count", 1),
305
+ expected=issue_dict.get("expected"),
306
+ actual=issue_dict.get("actual"),
307
+ sample_values=issue_dict.get("sample_values"),
308
+ validator_name=issue_dict.get("validator_name"),
309
+ details=issue_dict.get("details"),
310
+ )
311
+ )
312
+
313
+ # Build summary
314
+ summary = ValidationSummary(
315
+ total_issues=validation.total_issues or 0,
316
+ critical_issues=validation.critical_issues or 0,
317
+ high_issues=validation.high_issues or 0,
318
+ medium_issues=validation.medium_issues or 0,
319
+ low_issues=validation.low_issues or 0,
320
+ passed=validation.passed if validation.passed is not None else True,
321
+ )
322
+
323
+ # Build statistics
324
+ statistics = DataStatistics(
325
+ row_count=validation.row_count,
326
+ column_count=validation.column_count,
327
+ duration_ms=validation.duration_ms,
328
+ started_at=validation.started_at,
329
+ completed_at=validation.completed_at,
330
+ )
331
+
332
+ # Get source name
333
+ source_name = None
334
+ if hasattr(validation, "source") and validation.source:
335
+ source_name = validation.source.name
336
+
337
+ return cls(
338
+ validation_id=str(validation.id),
339
+ source_id=str(validation.source_id),
340
+ source_name=source_name,
341
+ issues=issues,
342
+ summary=summary,
343
+ statistics=statistics,
344
+ status=validation.status or "completed",
345
+ error_message=validation.error_message,
346
+ metadata={
347
+ "created_at": validation.created_at.isoformat()
348
+ if validation.created_at
349
+ else None,
350
+ },
351
+ raw_data=validation,
352
+ )
353
+
354
+ @classmethod
355
+ def from_check_result(
356
+ cls,
357
+ check_result: Any,
358
+ source_id: str | None = None,
359
+ ) -> ReportData:
360
+ """Create ReportData from a TruthoundAdapter CheckResult.
361
+
362
+ This enables direct report generation from validation results
363
+ without storing them in the database first.
364
+
365
+ Args:
366
+ check_result: CheckResult from TruthoundAdapter.
367
+ source_id: Optional source identifier.
368
+
369
+ Returns:
370
+ ReportData instance.
371
+ """
372
+ # Convert issues
373
+ issues = []
374
+ for issue_dict in check_result.issues:
375
+ issues.append(
376
+ ValidationIssueData(
377
+ column=issue_dict.get("column"),
378
+ issue_type=issue_dict.get("issue_type", "unknown"),
379
+ severity=issue_dict.get("severity", "medium"),
380
+ message=issue_dict.get("message", ""),
381
+ count=issue_dict.get("count", 1),
382
+ expected=issue_dict.get("expected"),
383
+ actual=issue_dict.get("actual"),
384
+ sample_values=issue_dict.get("sample_values"),
385
+ validator_name=issue_dict.get("validator_name"),
386
+ details=issue_dict.get("details"),
387
+ )
388
+ )
389
+
390
+ # Build summary
391
+ summary = ValidationSummary(
392
+ total_issues=check_result.total_issues,
393
+ critical_issues=check_result.critical_issues,
394
+ high_issues=check_result.high_issues,
395
+ medium_issues=check_result.medium_issues,
396
+ low_issues=check_result.low_issues,
397
+ passed=check_result.passed,
398
+ )
399
+
400
+ # Build statistics
401
+ run_time = check_result.run_time
402
+ statistics = DataStatistics(
403
+ row_count=check_result.row_count,
404
+ column_count=check_result.column_count,
405
+ started_at=run_time if run_time else None,
406
+ completed_at=datetime.now(),
407
+ )
408
+
409
+ # Use run_id if available, otherwise generate one
410
+ validation_id = check_result.run_id or f"check-{id(check_result)}"
411
+
412
+ return cls(
413
+ validation_id=validation_id,
414
+ source_id=source_id or check_result.source,
415
+ source_name=check_result.source,
416
+ issues=issues,
417
+ summary=summary,
418
+ statistics=statistics,
419
+ status="passed" if check_result.passed else "failed",
420
+ raw_data=check_result._raw_result if hasattr(check_result, "_raw_result") else None,
421
+ )
422
+
423
+ def to_dict(self) -> dict[str, Any]:
424
+ """Convert to dictionary for serialization."""
425
+ return {
426
+ "validation_id": self.validation_id,
427
+ "source_id": self.source_id,
428
+ "source_name": self.source_name,
429
+ "issues": [issue.to_dict() for issue in self.issues],
430
+ "summary": self.summary.to_dict(),
431
+ "statistics": self.statistics.to_dict(),
432
+ "status": self.status,
433
+ "error_message": self.error_message,
434
+ "metadata": self.metadata,
435
+ }
436
+
437
+
438
+ @dataclass
439
+ class ReportOutput:
440
+ """Output from report generation.
441
+
442
+ Attributes:
443
+ content: Generated report content (string or bytes).
444
+ content_type: MIME type of the content.
445
+ filename: Suggested filename for download.
446
+ format: Report format that was used.
447
+ size_bytes: Size of content in bytes.
448
+ generation_time_ms: Time taken to generate in milliseconds.
449
+ metadata: Additional metadata about the report.
450
+ """
451
+
452
+ content: str | bytes
453
+ content_type: str
454
+ filename: str
455
+ format: ReportFormatType
456
+ size_bytes: int = 0
457
+ generation_time_ms: int = 0
458
+ metadata: dict[str, Any] = field(default_factory=dict)
459
+
460
+ def __post_init__(self) -> None:
461
+ """Calculate size if not set."""
462
+ if self.size_bytes == 0:
463
+ if isinstance(self.content, str):
464
+ self.size_bytes = len(self.content.encode("utf-8"))
465
+ else:
466
+ self.size_bytes = len(self.content)
467
+
468
+
469
+ # Type variable for config
470
+ ConfigT = TypeVar("ConfigT", bound=ReporterConfig)
471
+
472
+
473
+ @runtime_checkable
474
+ class ReporterProtocol(Protocol):
475
+ """Protocol for reporter implementations.
476
+
477
+ This protocol enables duck typing for reporters from any source.
478
+ Implementations don't need to inherit from any base class.
479
+
480
+ Example:
481
+ class MyReporter:
482
+ @property
483
+ def format(self) -> ReportFormatType:
484
+ return ReportFormatType.HTML
485
+
486
+ @property
487
+ def content_type(self) -> str:
488
+ return "text/html"
489
+
490
+ @property
491
+ def file_extension(self) -> str:
492
+ return ".html"
493
+
494
+ async def generate(
495
+ self,
496
+ data: ReportData,
497
+ config: ReporterConfig | None = None,
498
+ ) -> ReportOutput:
499
+ ...
500
+ """
501
+
502
+ @property
503
+ def format(self) -> ReportFormatType:
504
+ """Get the report format this reporter produces."""
505
+ ...
506
+
507
+ @property
508
+ def content_type(self) -> str:
509
+ """Get the MIME content type for this format."""
510
+ ...
511
+
512
+ @property
513
+ def file_extension(self) -> str:
514
+ """Get the file extension for this format."""
515
+ ...
516
+
517
+ async def generate(
518
+ self,
519
+ data: ReportData,
520
+ config: ReporterConfig | None = None,
521
+ ) -> ReportOutput:
522
+ """Generate a report.
523
+
524
+ Args:
525
+ data: Standardized report data.
526
+ config: Optional configuration.
527
+
528
+ Returns:
529
+ ReportOutput with generated content.
530
+ """
531
+ ...
532
+
533
+
534
+ class BaseReporter(ABC, Generic[ConfigT]):
535
+ """Abstract base class for reporter implementations.
536
+
537
+ This provides a common base for dashboard-specific reporters
538
+ while maintaining compatibility with the ReporterProtocol.
539
+
540
+ Subclasses should implement:
541
+ - format (property)
542
+ - content_type (property)
543
+ - file_extension (property)
544
+ - _render_content (method)
545
+
546
+ Example:
547
+ class MyHTMLReporter(BaseReporter[ReporterConfig]):
548
+ @property
549
+ def format(self) -> ReportFormatType:
550
+ return ReportFormatType.HTML
551
+
552
+ @property
553
+ def content_type(self) -> str:
554
+ return "text/html"
555
+
556
+ @property
557
+ def file_extension(self) -> str:
558
+ return ".html"
559
+
560
+ async def _render_content(
561
+ self,
562
+ data: ReportData,
563
+ config: ConfigT,
564
+ ) -> str:
565
+ return f"<html>...</html>"
566
+ """
567
+
568
+ def __init__(self, default_config: ConfigT | None = None) -> None:
569
+ """Initialize reporter.
570
+
571
+ Args:
572
+ default_config: Default configuration for this reporter.
573
+ """
574
+ self._default_config = default_config or self._create_default_config()
575
+
576
+ @property
577
+ @abstractmethod
578
+ def format(self) -> ReportFormatType:
579
+ """Get the report format this reporter produces."""
580
+ ...
581
+
582
+ @property
583
+ @abstractmethod
584
+ def content_type(self) -> str:
585
+ """Get the MIME content type for this format."""
586
+ ...
587
+
588
+ @property
589
+ @abstractmethod
590
+ def file_extension(self) -> str:
591
+ """Get the file extension for this format."""
592
+ ...
593
+
594
+ def _create_default_config(self) -> ConfigT:
595
+ """Create default configuration.
596
+
597
+ Override in subclasses to provide format-specific defaults.
598
+ """
599
+ return ReporterConfig() # type: ignore
600
+
601
+ async def generate(
602
+ self,
603
+ data: ReportData,
604
+ config: ReporterConfig | None = None,
605
+ ) -> ReportOutput:
606
+ """Generate a report.
607
+
608
+ This is the main entry point (Template Method pattern).
609
+ Subclasses implement _render_content() for format-specific logic.
610
+
611
+ Args:
612
+ data: Standardized report data.
613
+ config: Optional configuration (uses default if not provided).
614
+
615
+ Returns:
616
+ ReportOutput with generated content.
617
+ """
618
+ import time
619
+
620
+ start_time = time.time()
621
+
622
+ # Merge with default config
623
+ effective_config = config or self._default_config
624
+
625
+ # Generate filename
626
+ timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
627
+ filename = f"validation_report_{timestamp}{self.file_extension}"
628
+
629
+ # Render content
630
+ content = await self._render_content(data, effective_config)
631
+
632
+ generation_time_ms = int((time.time() - start_time) * 1000)
633
+
634
+ return ReportOutput(
635
+ content=content,
636
+ content_type=self.content_type,
637
+ filename=filename,
638
+ format=self.format,
639
+ generation_time_ms=generation_time_ms,
640
+ metadata={
641
+ "title": effective_config.title,
642
+ "theme": effective_config.theme.value,
643
+ "locale": effective_config.locale,
644
+ "validation_id": data.validation_id,
645
+ "source_id": data.source_id,
646
+ "source_name": data.source_name,
647
+ },
648
+ )
649
+
650
+ @abstractmethod
651
+ async def _render_content(
652
+ self,
653
+ data: ReportData,
654
+ config: ConfigT,
655
+ ) -> str | bytes:
656
+ """Render the report content.
657
+
658
+ Subclasses implement this to produce format-specific output.
659
+
660
+ Args:
661
+ data: Report data.
662
+ config: Reporter configuration.
663
+
664
+ Returns:
665
+ Rendered report content.
666
+ """
667
+ ...
668
+
669
+ def _get_severity_color(self, severity: str, theme: ReportThemeType) -> str:
670
+ """Get color for severity level based on theme.
671
+
672
+ Args:
673
+ severity: Severity level (critical, high, medium, low).
674
+ theme: Current theme.
675
+
676
+ Returns:
677
+ CSS color value.
678
+ """
679
+ if theme == ReportThemeType.DARK:
680
+ colors = {
681
+ "critical": "#ef4444",
682
+ "high": "#f97316",
683
+ "medium": "#eab308",
684
+ "low": "#3b82f6",
685
+ }
686
+ else:
687
+ colors = {
688
+ "critical": "#dc2626",
689
+ "high": "#ea580c",
690
+ "medium": "#ca8a04",
691
+ "low": "#2563eb",
692
+ }
693
+ return colors.get(severity.lower(), "#6b7280")
694
+
695
+
696
+ @runtime_checkable
697
+ class ReporterAdapterProtocol(Protocol):
698
+ """Protocol for reporter adapters.
699
+
700
+ Adapters wrap external reporter implementations (e.g., truthound)
701
+ and translate them to the ReporterProtocol interface.
702
+ """
703
+
704
+ def adapt(self, external_reporter: Any) -> ReporterProtocol:
705
+ """Adapt an external reporter to the ReporterProtocol.
706
+
707
+ Args:
708
+ external_reporter: External reporter instance.
709
+
710
+ Returns:
711
+ Reporter that implements ReporterProtocol.
712
+ """
713
+ ...
714
+
715
+
716
+ @runtime_checkable
717
+ class ReporterFactoryProtocol(Protocol):
718
+ """Protocol for reporter factories.
719
+
720
+ Factories create reporter instances based on format and configuration.
721
+ """
722
+
723
+ def get_reporter(
724
+ self,
725
+ format: ReportFormatType | str,
726
+ config: ReporterConfig | None = None,
727
+ ) -> ReporterProtocol:
728
+ """Get a reporter for the specified format.
729
+
730
+ Args:
731
+ format: Report format.
732
+ config: Optional configuration.
733
+
734
+ Returns:
735
+ Reporter instance.
736
+ """
737
+ ...
738
+
739
+ def get_available_formats(self) -> list[str]:
740
+ """Get list of available format names."""
741
+ ...
742
+
743
+ def is_format_available(self, format: ReportFormatType | str) -> bool:
744
+ """Check if a format is available."""
745
+ ...