truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Base classes for report generation system.
|
|
2
|
+
|
|
3
|
+
This module defines the abstract interfaces and data structures for
|
|
4
|
+
the reporter system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from truthound_dashboard.db.models import Validation
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReportFormat(str, Enum):
|
|
20
|
+
"""Supported report output formats."""
|
|
21
|
+
|
|
22
|
+
HTML = "html"
|
|
23
|
+
CSV = "csv"
|
|
24
|
+
JSON = "json"
|
|
25
|
+
MARKDOWN = "markdown"
|
|
26
|
+
PDF = "pdf"
|
|
27
|
+
JUNIT = "junit" # JUnit XML for CI/CD integration
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_string(cls, value: str) -> ReportFormat:
|
|
31
|
+
"""Parse format from string.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
value: Format string (case-insensitive).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
ReportFormat enum value.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If format is not recognized.
|
|
41
|
+
"""
|
|
42
|
+
value_lower = value.lower()
|
|
43
|
+
for fmt in cls:
|
|
44
|
+
if fmt.value == value_lower:
|
|
45
|
+
return fmt
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Unknown report format: {value}. "
|
|
48
|
+
f"Supported formats: {[f.value for f in cls]}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ReportTheme(str, Enum):
|
|
53
|
+
"""Report visual themes."""
|
|
54
|
+
|
|
55
|
+
LIGHT = "light"
|
|
56
|
+
DARK = "dark"
|
|
57
|
+
PROFESSIONAL = "professional"
|
|
58
|
+
MINIMAL = "minimal"
|
|
59
|
+
HIGH_CONTRAST = "high_contrast"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ReportMetadata:
|
|
64
|
+
"""Metadata for generated reports.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
title: Report title.
|
|
68
|
+
generated_at: Timestamp when report was generated.
|
|
69
|
+
source_name: Name of the data source.
|
|
70
|
+
validation_id: ID of the validation run.
|
|
71
|
+
theme: Visual theme used.
|
|
72
|
+
format: Output format.
|
|
73
|
+
custom_fields: Additional custom metadata.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
title: str = "Validation Report"
|
|
77
|
+
generated_at: datetime = field(default_factory=datetime.utcnow)
|
|
78
|
+
source_name: str | None = None
|
|
79
|
+
source_id: str | None = None
|
|
80
|
+
validation_id: str | None = None
|
|
81
|
+
theme: ReportTheme = ReportTheme.PROFESSIONAL
|
|
82
|
+
format: ReportFormat = ReportFormat.HTML
|
|
83
|
+
custom_fields: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict[str, Any]:
|
|
86
|
+
"""Convert to dictionary."""
|
|
87
|
+
return {
|
|
88
|
+
"title": self.title,
|
|
89
|
+
"generated_at": self.generated_at.isoformat(),
|
|
90
|
+
"source_name": self.source_name,
|
|
91
|
+
"source_id": self.source_id,
|
|
92
|
+
"validation_id": self.validation_id,
|
|
93
|
+
"theme": self.theme.value,
|
|
94
|
+
"format": self.format.value,
|
|
95
|
+
**self.custom_fields,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ReportResult:
|
|
101
|
+
"""Result of report generation.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
content: Generated report content (string or bytes).
|
|
105
|
+
metadata: Report metadata.
|
|
106
|
+
content_type: MIME type of the content.
|
|
107
|
+
filename: Suggested filename for download.
|
|
108
|
+
size_bytes: Size of content in bytes.
|
|
109
|
+
generation_time_ms: Time taken to generate in milliseconds.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
content: str | bytes
|
|
113
|
+
metadata: ReportMetadata
|
|
114
|
+
content_type: str
|
|
115
|
+
filename: str
|
|
116
|
+
size_bytes: int = 0
|
|
117
|
+
generation_time_ms: int = 0
|
|
118
|
+
|
|
119
|
+
def __post_init__(self) -> None:
|
|
120
|
+
"""Calculate size if not set."""
|
|
121
|
+
if self.size_bytes == 0:
|
|
122
|
+
if isinstance(self.content, str):
|
|
123
|
+
self.size_bytes = len(self.content.encode("utf-8"))
|
|
124
|
+
else:
|
|
125
|
+
self.size_bytes = len(self.content)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Reporter(ABC):
|
|
129
|
+
"""Abstract base class for report generators.
|
|
130
|
+
|
|
131
|
+
Subclass this to implement custom report formats.
|
|
132
|
+
Uses Template Method pattern: generate() orchestrates the process,
|
|
133
|
+
while subclasses implement format-specific rendering.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def format(self) -> ReportFormat:
|
|
139
|
+
"""Get the report format this reporter produces."""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def content_type(self) -> str:
|
|
145
|
+
"""Get the MIME content type for this format."""
|
|
146
|
+
...
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def file_extension(self) -> str:
|
|
151
|
+
"""Get the file extension for this format."""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
async def generate(
|
|
155
|
+
self,
|
|
156
|
+
validation: Validation,
|
|
157
|
+
*,
|
|
158
|
+
theme: ReportTheme = ReportTheme.PROFESSIONAL,
|
|
159
|
+
title: str | None = None,
|
|
160
|
+
include_samples: bool = True,
|
|
161
|
+
include_statistics: bool = True,
|
|
162
|
+
custom_metadata: dict[str, Any] | None = None,
|
|
163
|
+
) -> ReportResult:
|
|
164
|
+
"""Generate a report for a validation result.
|
|
165
|
+
|
|
166
|
+
This is the main entry point (Template Method). Override
|
|
167
|
+
_render_content() in subclasses for format-specific logic.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
validation: Validation model with results.
|
|
171
|
+
theme: Visual theme for the report.
|
|
172
|
+
title: Custom report title.
|
|
173
|
+
include_samples: Include sample problematic values.
|
|
174
|
+
include_statistics: Include data statistics section.
|
|
175
|
+
custom_metadata: Additional metadata to include.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
ReportResult with generated content.
|
|
179
|
+
"""
|
|
180
|
+
import time
|
|
181
|
+
|
|
182
|
+
start_time = time.time()
|
|
183
|
+
|
|
184
|
+
# Build metadata
|
|
185
|
+
metadata = ReportMetadata(
|
|
186
|
+
title=title or f"Validation Report - {validation.source_id}",
|
|
187
|
+
source_id=validation.source_id,
|
|
188
|
+
validation_id=validation.id,
|
|
189
|
+
theme=theme,
|
|
190
|
+
format=self.format,
|
|
191
|
+
custom_fields=custom_metadata or {},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Try to get source name
|
|
195
|
+
if hasattr(validation, "source") and validation.source:
|
|
196
|
+
metadata.source_name = validation.source.name
|
|
197
|
+
|
|
198
|
+
# Generate filename
|
|
199
|
+
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
|
200
|
+
filename = f"validation_report_{timestamp}{self.file_extension}"
|
|
201
|
+
|
|
202
|
+
# Render content (subclass implementation)
|
|
203
|
+
content = await self._render_content(
|
|
204
|
+
validation=validation,
|
|
205
|
+
metadata=metadata,
|
|
206
|
+
include_samples=include_samples,
|
|
207
|
+
include_statistics=include_statistics,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
generation_time_ms = int((time.time() - start_time) * 1000)
|
|
211
|
+
|
|
212
|
+
return ReportResult(
|
|
213
|
+
content=content,
|
|
214
|
+
metadata=metadata,
|
|
215
|
+
content_type=self.content_type,
|
|
216
|
+
filename=filename,
|
|
217
|
+
generation_time_ms=generation_time_ms,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@abstractmethod
|
|
221
|
+
async def _render_content(
|
|
222
|
+
self,
|
|
223
|
+
validation: Validation,
|
|
224
|
+
metadata: ReportMetadata,
|
|
225
|
+
include_samples: bool,
|
|
226
|
+
include_statistics: bool,
|
|
227
|
+
) -> str | bytes:
|
|
228
|
+
"""Render the report content.
|
|
229
|
+
|
|
230
|
+
Subclasses implement this to produce format-specific output.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
validation: Validation model with results.
|
|
234
|
+
metadata: Report metadata.
|
|
235
|
+
include_samples: Whether to include sample values.
|
|
236
|
+
include_statistics: Whether to include statistics.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Rendered report content.
|
|
240
|
+
"""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
def _extract_issues(self, validation: Validation) -> list[dict[str, Any]]:
|
|
244
|
+
"""Extract issues from validation result.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
validation: Validation model.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
List of issue dictionaries.
|
|
251
|
+
"""
|
|
252
|
+
if validation.result_json and "issues" in validation.result_json:
|
|
253
|
+
return validation.result_json["issues"]
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
def _get_severity_color(self, severity: str, theme: ReportTheme) -> str:
|
|
257
|
+
"""Get color for severity level based on theme.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
severity: Severity level (critical, high, medium, low).
|
|
261
|
+
theme: Current theme.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
CSS color value.
|
|
265
|
+
"""
|
|
266
|
+
# Base colors (work for most themes)
|
|
267
|
+
colors = {
|
|
268
|
+
"critical": "#dc2626", # Red
|
|
269
|
+
"high": "#ea580c", # Orange
|
|
270
|
+
"medium": "#ca8a04", # Yellow/Gold
|
|
271
|
+
"low": "#2563eb", # Blue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Adjust for dark theme
|
|
275
|
+
if theme == ReportTheme.DARK:
|
|
276
|
+
colors = {
|
|
277
|
+
"critical": "#ef4444",
|
|
278
|
+
"high": "#f97316",
|
|
279
|
+
"medium": "#eab308",
|
|
280
|
+
"low": "#3b82f6",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return colors.get(severity.lower(), "#6b7280")
|
|
284
|
+
|
|
285
|
+
def _get_status_indicator(self, passed: bool | None) -> str:
|
|
286
|
+
"""Get status indicator text.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
passed: Whether validation passed.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Status indicator string.
|
|
293
|
+
"""
|
|
294
|
+
if passed is None:
|
|
295
|
+
return "⏳ Pending"
|
|
296
|
+
return "✅ Passed" if passed else "❌ Failed"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""CSV report generator.
|
|
2
|
+
|
|
3
|
+
Generates CSV reports suitable for data analysis and spreadsheet tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import io
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from .base import Reporter, ReportFormat, ReportMetadata
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from truthound_dashboard.db.models import Validation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CSVReporter(Reporter):
|
|
19
|
+
"""CSV report generator.
|
|
20
|
+
|
|
21
|
+
Produces CSV files with validation issues and statistics.
|
|
22
|
+
Supports customizable delimiters and encoding.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
delimiter: str = ",",
|
|
28
|
+
include_header: bool = True,
|
|
29
|
+
encoding: str = "utf-8",
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize CSV reporter.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
delimiter: CSV delimiter character.
|
|
35
|
+
include_header: Whether to include column headers.
|
|
36
|
+
encoding: Output encoding.
|
|
37
|
+
"""
|
|
38
|
+
self._delimiter = delimiter
|
|
39
|
+
self._include_header = include_header
|
|
40
|
+
self._encoding = encoding
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def format(self) -> ReportFormat:
|
|
44
|
+
return ReportFormat.CSV
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def content_type(self) -> str:
|
|
48
|
+
return f"text/csv; charset={self._encoding}"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def file_extension(self) -> str:
|
|
52
|
+
return ".csv"
|
|
53
|
+
|
|
54
|
+
async def _render_content(
|
|
55
|
+
self,
|
|
56
|
+
validation: Validation,
|
|
57
|
+
metadata: ReportMetadata,
|
|
58
|
+
include_samples: bool,
|
|
59
|
+
include_statistics: bool,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Render CSV report content."""
|
|
62
|
+
output = io.StringIO()
|
|
63
|
+
writer = csv.writer(output, delimiter=self._delimiter)
|
|
64
|
+
|
|
65
|
+
# Write metadata section
|
|
66
|
+
writer.writerow(["# Validation Report"])
|
|
67
|
+
writer.writerow(["# Source", metadata.source_name or validation.source_id])
|
|
68
|
+
writer.writerow(["# Validation ID", validation.id])
|
|
69
|
+
writer.writerow(["# Generated At", metadata.generated_at.isoformat()])
|
|
70
|
+
writer.writerow(
|
|
71
|
+
[
|
|
72
|
+
"# Status",
|
|
73
|
+
"PASSED" if validation.passed else "FAILED",
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
writer.writerow([]) # Empty row separator
|
|
77
|
+
|
|
78
|
+
# Write statistics section if requested
|
|
79
|
+
if include_statistics:
|
|
80
|
+
writer.writerow(["# Statistics"])
|
|
81
|
+
writer.writerow(["Metric", "Value"])
|
|
82
|
+
writer.writerow(["Row Count", validation.row_count or "N/A"])
|
|
83
|
+
writer.writerow(["Column Count", validation.column_count or "N/A"])
|
|
84
|
+
writer.writerow(["Total Issues", validation.total_issues or 0])
|
|
85
|
+
writer.writerow(["Critical Issues", validation.critical_issues or 0])
|
|
86
|
+
writer.writerow(["High Issues", validation.high_issues or 0])
|
|
87
|
+
writer.writerow(["Medium Issues", validation.medium_issues or 0])
|
|
88
|
+
writer.writerow(["Low Issues", validation.low_issues or 0])
|
|
89
|
+
writer.writerow(
|
|
90
|
+
[
|
|
91
|
+
"Duration (ms)",
|
|
92
|
+
validation.duration_ms if validation.duration_ms else "N/A",
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
writer.writerow([]) # Empty row separator
|
|
96
|
+
|
|
97
|
+
# Write issues section
|
|
98
|
+
issues = self._extract_issues(validation)
|
|
99
|
+
writer.writerow(["# Issues"])
|
|
100
|
+
|
|
101
|
+
# Define headers based on options
|
|
102
|
+
headers = ["Column", "Issue Type", "Severity", "Count", "Details"]
|
|
103
|
+
if include_samples:
|
|
104
|
+
headers.append("Sample Values")
|
|
105
|
+
|
|
106
|
+
if self._include_header:
|
|
107
|
+
writer.writerow(headers)
|
|
108
|
+
|
|
109
|
+
# Write issue rows
|
|
110
|
+
for issue in issues:
|
|
111
|
+
row = [
|
|
112
|
+
issue.get("column", ""),
|
|
113
|
+
issue.get("issue_type", ""),
|
|
114
|
+
issue.get("severity", ""),
|
|
115
|
+
issue.get("count", 0),
|
|
116
|
+
issue.get("details", "") or "",
|
|
117
|
+
]
|
|
118
|
+
if include_samples:
|
|
119
|
+
samples = issue.get("sample_values", [])
|
|
120
|
+
samples_str = "; ".join(str(v)[:50] for v in samples[:5])
|
|
121
|
+
row.append(samples_str)
|
|
122
|
+
|
|
123
|
+
writer.writerow(row)
|
|
124
|
+
|
|
125
|
+
return output.getvalue()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ExcelCSVReporter(CSVReporter):
|
|
129
|
+
"""CSV reporter optimized for Microsoft Excel.
|
|
130
|
+
|
|
131
|
+
Uses UTF-8 BOM for proper encoding detection and semicolon delimiter
|
|
132
|
+
for better compatibility with Excel's regional settings.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self) -> None:
|
|
136
|
+
"""Initialize Excel-optimized CSV reporter."""
|
|
137
|
+
super().__init__(delimiter=";", include_header=True, encoding="utf-8-sig")
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def file_extension(self) -> str:
|
|
141
|
+
return ".csv"
|
|
142
|
+
|
|
143
|
+
async def _render_content(
|
|
144
|
+
self,
|
|
145
|
+
validation: Validation,
|
|
146
|
+
metadata: ReportMetadata,
|
|
147
|
+
include_samples: bool,
|
|
148
|
+
include_statistics: bool,
|
|
149
|
+
) -> str:
|
|
150
|
+
"""Render CSV with BOM for Excel."""
|
|
151
|
+
content = await super()._render_content(
|
|
152
|
+
validation, metadata, include_samples, include_statistics
|
|
153
|
+
)
|
|
154
|
+
# Add UTF-8 BOM for Excel
|
|
155
|
+
return "\ufeff" + content
|