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,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