truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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.
- 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.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.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.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""PDF report generator.
|
|
2
|
+
|
|
3
|
+
Generates professional PDF reports using HTML-to-PDF conversion.
|
|
4
|
+
Supports multiple themes and includes all validation details.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from .base import Reporter, ReportFormat, ReportMetadata, ReportTheme
|
|
13
|
+
from .html_reporter import HTMLReporter
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from truthound_dashboard.db.models import Validation
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Check if weasyprint is available
|
|
21
|
+
_WEASYPRINT_AVAILABLE = False
|
|
22
|
+
try:
|
|
23
|
+
import weasyprint # noqa: F401
|
|
24
|
+
_WEASYPRINT_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
logger.debug("weasyprint not available, PDF generation will use HTML fallback")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PDFReporter(Reporter):
|
|
30
|
+
"""PDF report generator using HTML-to-PDF conversion.
|
|
31
|
+
|
|
32
|
+
Uses weasyprint for high-quality PDF generation.
|
|
33
|
+
Falls back to HTML if weasyprint is not installed.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
"""Initialize PDF reporter with HTML reporter for content generation."""
|
|
38
|
+
self._html_reporter = HTMLReporter()
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def format(self) -> ReportFormat:
|
|
42
|
+
return ReportFormat.PDF
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def content_type(self) -> str:
|
|
46
|
+
return "application/pdf"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def file_extension(self) -> str:
|
|
50
|
+
return ".pdf"
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def is_available(cls) -> bool:
|
|
54
|
+
"""Check if PDF generation is available."""
|
|
55
|
+
return _WEASYPRINT_AVAILABLE
|
|
56
|
+
|
|
57
|
+
async def _render_content(
|
|
58
|
+
self,
|
|
59
|
+
validation: Validation,
|
|
60
|
+
metadata: ReportMetadata,
|
|
61
|
+
include_samples: bool,
|
|
62
|
+
include_statistics: bool,
|
|
63
|
+
) -> bytes:
|
|
64
|
+
"""Render PDF report content.
|
|
65
|
+
|
|
66
|
+
Uses HTML reporter to generate content, then converts to PDF.
|
|
67
|
+
"""
|
|
68
|
+
# Generate HTML content using the HTML reporter
|
|
69
|
+
html_content = await self._html_reporter._render_content(
|
|
70
|
+
validation=validation,
|
|
71
|
+
metadata=metadata,
|
|
72
|
+
include_samples=include_samples,
|
|
73
|
+
include_statistics=include_statistics,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Add PDF-specific styles for better printing
|
|
77
|
+
pdf_styles = self._get_pdf_styles(metadata.theme)
|
|
78
|
+
html_content = html_content.replace(
|
|
79
|
+
"</style>",
|
|
80
|
+
f"{pdf_styles}\n </style>"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Convert HTML to PDF
|
|
84
|
+
if _WEASYPRINT_AVAILABLE:
|
|
85
|
+
return self._convert_to_pdf(html_content)
|
|
86
|
+
else:
|
|
87
|
+
# Return HTML as bytes if weasyprint is not available
|
|
88
|
+
logger.warning(
|
|
89
|
+
"weasyprint not installed. Install with: pip install weasyprint"
|
|
90
|
+
)
|
|
91
|
+
return html_content.encode("utf-8")
|
|
92
|
+
|
|
93
|
+
def _get_pdf_styles(self, theme: ReportTheme) -> str:
|
|
94
|
+
"""Get additional CSS styles for PDF output."""
|
|
95
|
+
return """
|
|
96
|
+
/* PDF-specific styles */
|
|
97
|
+
@page {
|
|
98
|
+
size: A4;
|
|
99
|
+
margin: 1.5cm;
|
|
100
|
+
|
|
101
|
+
@top-center {
|
|
102
|
+
content: "Truthound Validation Report";
|
|
103
|
+
font-size: 10pt;
|
|
104
|
+
color: #64748b;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@bottom-right {
|
|
108
|
+
content: counter(page) " / " counter(pages);
|
|
109
|
+
font-size: 10pt;
|
|
110
|
+
color: #64748b;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@page :first {
|
|
115
|
+
@top-center { content: none; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
body {
|
|
119
|
+
font-size: 11pt;
|
|
120
|
+
line-height: 1.5;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.container {
|
|
124
|
+
max-width: 100%;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.header {
|
|
128
|
+
page-break-after: avoid;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.card {
|
|
132
|
+
page-break-inside: avoid;
|
|
133
|
+
margin-bottom: 0.75cm;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
table {
|
|
137
|
+
font-size: 10pt;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
th, td {
|
|
141
|
+
padding: 0.4rem 0.6rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.summary-grid {
|
|
145
|
+
grid-template-columns: repeat(5, 1fr);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.summary-item .value {
|
|
149
|
+
font-size: 1.5rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.footer {
|
|
153
|
+
page-break-before: avoid;
|
|
154
|
+
font-size: 9pt;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Remove hover effects for PDF */
|
|
158
|
+
tr:hover {
|
|
159
|
+
background: transparent !important;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Ensure badges print with colors */
|
|
163
|
+
.severity-badge,
|
|
164
|
+
.status-badge {
|
|
165
|
+
-webkit-print-color-adjust: exact;
|
|
166
|
+
print-color-adjust: exact;
|
|
167
|
+
}
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def _convert_to_pdf(self, html_content: str) -> bytes:
|
|
171
|
+
"""Convert HTML content to PDF bytes.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
html_content: HTML string to convert.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
PDF content as bytes.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
import weasyprint
|
|
181
|
+
from weasyprint import CSS
|
|
182
|
+
|
|
183
|
+
# Additional CSS for better PDF rendering
|
|
184
|
+
extra_css = CSS(string="""
|
|
185
|
+
@page { margin: 1.5cm; }
|
|
186
|
+
body { -webkit-print-color-adjust: exact; }
|
|
187
|
+
""")
|
|
188
|
+
|
|
189
|
+
# Create PDF
|
|
190
|
+
html = weasyprint.HTML(string=html_content, base_url=".")
|
|
191
|
+
pdf_bytes = html.write_pdf(stylesheets=[extra_css])
|
|
192
|
+
|
|
193
|
+
return pdf_bytes
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Failed to convert HTML to PDF: {e}")
|
|
197
|
+
raise RuntimeError(f"PDF generation failed: {e}") from e
|
|
198
|
+
|
|
199
|
+
def _extract_issues(self, validation: Validation) -> list[dict[str, Any]]:
|
|
200
|
+
"""Extract issues from validation result."""
|
|
201
|
+
return self._html_reporter._extract_issues(validation)
|
|
202
|
+
|
|
203
|
+
def _get_severity_color(self, severity: str, theme: ReportTheme) -> str:
|
|
204
|
+
"""Get color for severity level."""
|
|
205
|
+
return self._html_reporter._get_severity_color(severity, theme)
|
|
206
|
+
|
|
207
|
+
def _get_status_indicator(self, passed: bool | None) -> str:
|
|
208
|
+
"""Get status indicator text."""
|
|
209
|
+
return self._html_reporter._get_status_indicator(passed)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Reporter registry and factory functions.
|
|
2
|
+
|
|
3
|
+
This module provides a central registry for report generators and
|
|
4
|
+
convenience functions for generating reports with i18n support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from .base import Reporter, ReportFormat, ReportResult, ReportTheme
|
|
13
|
+
from .i18n import SupportedLocale, get_supported_locales
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from truthound_dashboard.db.models import Validation
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReporterRegistry:
|
|
22
|
+
"""Registry for report generators.
|
|
23
|
+
|
|
24
|
+
Maintains a mapping of formats to reporter implementations.
|
|
25
|
+
Supports custom reporter registration for extensibility.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize empty registry."""
|
|
30
|
+
self._reporters: dict[ReportFormat, type[Reporter]] = {}
|
|
31
|
+
|
|
32
|
+
def register(
|
|
33
|
+
self,
|
|
34
|
+
format_type: ReportFormat,
|
|
35
|
+
reporter_class: type[Reporter],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Register a reporter for a format.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
format_type: Report format this reporter handles.
|
|
41
|
+
reporter_class: Reporter class to register.
|
|
42
|
+
"""
|
|
43
|
+
self._reporters[format_type] = reporter_class
|
|
44
|
+
logger.debug(f"Registered reporter for format: {format_type.value}")
|
|
45
|
+
|
|
46
|
+
def get(self, format_type: ReportFormat) -> Reporter:
|
|
47
|
+
"""Get a reporter instance for a format.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
format_type: Desired report format.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Reporter instance.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If no reporter is registered for the format.
|
|
57
|
+
"""
|
|
58
|
+
reporter_class = self._reporters.get(format_type)
|
|
59
|
+
if reporter_class is None:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"No reporter registered for format: {format_type.value}. "
|
|
62
|
+
f"Available formats: {self.available_formats}"
|
|
63
|
+
)
|
|
64
|
+
return reporter_class()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def available_formats(self) -> list[str]:
|
|
68
|
+
"""Get list of available format names."""
|
|
69
|
+
return [fmt.value for fmt in self._reporters.keys()]
|
|
70
|
+
|
|
71
|
+
def is_registered(self, format_type: ReportFormat) -> bool:
|
|
72
|
+
"""Check if a format has a registered reporter.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
format_type: Format to check.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if format is registered.
|
|
79
|
+
"""
|
|
80
|
+
return format_type in self._reporters
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Global registry instance
|
|
84
|
+
_registry: ReporterRegistry | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_registry() -> ReporterRegistry:
|
|
88
|
+
"""Get the global reporter registry.
|
|
89
|
+
|
|
90
|
+
Initializes with default reporters on first call.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Global ReporterRegistry instance.
|
|
94
|
+
"""
|
|
95
|
+
global _registry
|
|
96
|
+
if _registry is None:
|
|
97
|
+
_registry = ReporterRegistry()
|
|
98
|
+
_register_default_reporters(_registry)
|
|
99
|
+
return _registry
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _register_default_reporters(registry: ReporterRegistry) -> None:
|
|
103
|
+
"""Register all built-in reporters.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
registry: Registry to populate.
|
|
107
|
+
|
|
108
|
+
Registers 6 reporters:
|
|
109
|
+
- HTML: Rich visual reports with themes
|
|
110
|
+
- CSV: Spreadsheet-compatible format
|
|
111
|
+
- JSON: Machine-readable structured data
|
|
112
|
+
- Markdown: Documentation-friendly format
|
|
113
|
+
- PDF: Print-ready documents
|
|
114
|
+
- JUnit: CI/CD integration (Jenkins, GitHub Actions, etc.)
|
|
115
|
+
"""
|
|
116
|
+
from .csv_reporter import CSVReporter
|
|
117
|
+
from .html_reporter import HTMLReporter
|
|
118
|
+
from .json_reporter import JSONReporter
|
|
119
|
+
from .junit_reporter import JUnitReporter
|
|
120
|
+
from .markdown_reporter import MarkdownReporter
|
|
121
|
+
from .pdf_reporter import PDFReporter
|
|
122
|
+
|
|
123
|
+
registry.register(ReportFormat.HTML, HTMLReporter)
|
|
124
|
+
registry.register(ReportFormat.CSV, CSVReporter)
|
|
125
|
+
registry.register(ReportFormat.JSON, JSONReporter)
|
|
126
|
+
registry.register(ReportFormat.MARKDOWN, MarkdownReporter)
|
|
127
|
+
registry.register(ReportFormat.PDF, PDFReporter)
|
|
128
|
+
registry.register(ReportFormat.JUNIT, JUnitReporter)
|
|
129
|
+
|
|
130
|
+
logger.debug(f"Registered {len(registry.available_formats)} default reporters")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def register_reporter(
|
|
134
|
+
format_type: ReportFormat,
|
|
135
|
+
reporter_class: type[Reporter],
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Register a custom reporter.
|
|
138
|
+
|
|
139
|
+
Convenience function for registering custom reporters.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
format_type: Report format to handle.
|
|
143
|
+
reporter_class: Reporter class to register.
|
|
144
|
+
"""
|
|
145
|
+
get_registry().register(format_type, reporter_class)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_reporter(
|
|
149
|
+
format_type: ReportFormat | str,
|
|
150
|
+
locale: SupportedLocale | str = SupportedLocale.ENGLISH,
|
|
151
|
+
) -> Reporter:
|
|
152
|
+
"""Get a reporter for a specific format with locale support.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
format_type: Report format (enum or string).
|
|
156
|
+
locale: Target locale for report generation.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Reporter instance for the format.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If format is not recognized or not registered.
|
|
163
|
+
"""
|
|
164
|
+
if isinstance(format_type, str):
|
|
165
|
+
format_type = ReportFormat.from_string(format_type)
|
|
166
|
+
|
|
167
|
+
# Get the reporter class from registry
|
|
168
|
+
registry = get_registry()
|
|
169
|
+
reporter_class = registry._reporters.get(format_type)
|
|
170
|
+
|
|
171
|
+
if reporter_class is None:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"No reporter registered for format: {format_type.value}. "
|
|
174
|
+
f"Available formats: {registry.available_formats}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Convert locale string to enum
|
|
178
|
+
if isinstance(locale, str):
|
|
179
|
+
locale = SupportedLocale.from_string(locale)
|
|
180
|
+
|
|
181
|
+
# Create reporter with locale if supported
|
|
182
|
+
try:
|
|
183
|
+
# Try to create with locale (for reporters that support it)
|
|
184
|
+
return reporter_class(locale=locale)
|
|
185
|
+
except TypeError:
|
|
186
|
+
# Fall back to no-arg constructor for reporters without locale
|
|
187
|
+
return reporter_class()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_available_formats() -> list[str]:
|
|
191
|
+
"""Get list of available report formats.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of format name strings.
|
|
195
|
+
"""
|
|
196
|
+
return get_registry().available_formats
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def generate_report(
|
|
200
|
+
validation: Validation,
|
|
201
|
+
*,
|
|
202
|
+
format: ReportFormat | str = ReportFormat.HTML,
|
|
203
|
+
theme: ReportTheme | str = ReportTheme.PROFESSIONAL,
|
|
204
|
+
locale: SupportedLocale | str = SupportedLocale.ENGLISH,
|
|
205
|
+
title: str | None = None,
|
|
206
|
+
include_samples: bool = True,
|
|
207
|
+
include_statistics: bool = True,
|
|
208
|
+
custom_metadata: dict[str, Any] | None = None,
|
|
209
|
+
) -> ReportResult:
|
|
210
|
+
"""Generate a report for a validation result.
|
|
211
|
+
|
|
212
|
+
High-level convenience function for report generation with i18n support.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
validation: Validation model with results.
|
|
216
|
+
format: Output format (enum or string).
|
|
217
|
+
theme: Visual theme (enum or string).
|
|
218
|
+
locale: Report language (supports 15 languages).
|
|
219
|
+
title: Custom report title.
|
|
220
|
+
include_samples: Include sample problematic values.
|
|
221
|
+
include_statistics: Include data statistics.
|
|
222
|
+
custom_metadata: Additional metadata to include.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
ReportResult with generated content.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
# Generate Korean HTML report with dark theme
|
|
229
|
+
report = await generate_report(
|
|
230
|
+
validation,
|
|
231
|
+
format="html",
|
|
232
|
+
theme="dark",
|
|
233
|
+
locale="ko"
|
|
234
|
+
)
|
|
235
|
+
with open(report.filename, "w") as f:
|
|
236
|
+
f.write(report.content)
|
|
237
|
+
"""
|
|
238
|
+
# Convert string arguments to enums
|
|
239
|
+
if isinstance(format, str):
|
|
240
|
+
format = ReportFormat.from_string(format)
|
|
241
|
+
if isinstance(theme, str):
|
|
242
|
+
theme = ReportTheme(theme)
|
|
243
|
+
|
|
244
|
+
reporter = get_reporter(format, locale=locale)
|
|
245
|
+
|
|
246
|
+
return await reporter.generate(
|
|
247
|
+
validation,
|
|
248
|
+
theme=theme,
|
|
249
|
+
title=title,
|
|
250
|
+
include_samples=include_samples,
|
|
251
|
+
include_statistics=include_statistics,
|
|
252
|
+
custom_metadata=custom_metadata,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_report_locales() -> list[dict[str, Any]]:
|
|
257
|
+
"""Get list of supported report locales.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of locale info dictionaries with code, name, and metadata.
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
locales = get_report_locales()
|
|
264
|
+
# [{"code": "en", "english_name": "English", "native_name": "English", ...}, ...]
|
|
265
|
+
"""
|
|
266
|
+
return get_supported_locales()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def reset_registry() -> None:
|
|
270
|
+
"""Reset the global registry (for testing)."""
|
|
271
|
+
global _registry
|
|
272
|
+
_registry = None
|