truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

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