truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.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.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +645 -23
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +15 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,526 @@
1
+ """Reporter factory with flexible backend selection.
2
+
3
+ This module provides a unified factory for creating reporters from
4
+ various backends (dashboard-native, truthound, custom).
5
+
6
+ The factory supports:
7
+ 1. Multiple backend priority (truthound first, then fallback)
8
+ 2. Dynamic reporter registration
9
+ 3. Lazy loading of backends
10
+ 4. Configuration-based reporter creation
11
+
12
+ Example:
13
+ from truthound_dashboard.core.reporters.factory import (
14
+ get_reporter_factory,
15
+ ReporterFactory,
16
+ )
17
+
18
+ factory = get_reporter_factory()
19
+
20
+ # Get reporter by format
21
+ reporter = factory.get_reporter("json")
22
+
23
+ # Generate report
24
+ output = await reporter.generate(data)
25
+
26
+ # Check available formats
27
+ formats = factory.get_available_formats()
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ from typing import Any, Callable, TypeVar
34
+
35
+ from .interfaces import (
36
+ BaseReporter,
37
+ ReportData,
38
+ ReporterConfig,
39
+ ReporterProtocol,
40
+ ReportFormatType,
41
+ ReportOutput,
42
+ ReportThemeType,
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ ReporterT = TypeVar("ReporterT", bound=ReporterProtocol)
48
+
49
+
50
+ class ReporterFactory:
51
+ """Factory for creating reporter instances.
52
+
53
+ This factory supports multiple backends with priority ordering:
54
+ 1. Explicitly registered reporters (highest priority)
55
+ 2. Truthound reporters (if available)
56
+ 3. Dashboard built-in reporters (fallback)
57
+
58
+ The factory uses lazy loading to avoid importing unnecessary
59
+ dependencies until they're actually needed.
60
+
61
+ Attributes:
62
+ backends: List of backend names in priority order.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ use_truthound: bool = True,
68
+ default_locale: str = "en",
69
+ ) -> None:
70
+ """Initialize factory.
71
+
72
+ Args:
73
+ use_truthound: Whether to try truthound reporters first.
74
+ default_locale: Default locale for i18n.
75
+ """
76
+ self._use_truthound = use_truthound
77
+ self._default_locale = default_locale
78
+
79
+ # Registered reporter classes (format -> class)
80
+ self._registered: dict[ReportFormatType, type[ReporterProtocol]] = {}
81
+
82
+ # Registered reporter instances (format -> instance)
83
+ self._instances: dict[str, ReporterProtocol] = {}
84
+
85
+ # Factory functions for custom reporters
86
+ self._factories: dict[ReportFormatType, Callable[..., ReporterProtocol]] = {}
87
+
88
+ # Cached truthound availability
89
+ self._truthound_available: bool | None = None
90
+
91
+ @property
92
+ def backends(self) -> list[str]:
93
+ """Get list of backend names in priority order."""
94
+ backends = ["registered"]
95
+ if self._use_truthound and self._is_truthound_available():
96
+ backends.append("truthound")
97
+ backends.append("builtin")
98
+ return backends
99
+
100
+ def _is_truthound_available(self) -> bool:
101
+ """Check if truthound is available (cached)."""
102
+ if self._truthound_available is None:
103
+ from .adapters import is_truthound_available
104
+
105
+ self._truthound_available = is_truthound_available()
106
+ return self._truthound_available
107
+
108
+ def register(
109
+ self,
110
+ format_type: ReportFormatType | str,
111
+ reporter_class: type[ReporterProtocol],
112
+ ) -> None:
113
+ """Register a reporter class for a format.
114
+
115
+ Registered reporters have highest priority and will be used
116
+ instead of truthound or built-in reporters.
117
+
118
+ Args:
119
+ format_type: Report format to register for.
120
+ reporter_class: Reporter class to use.
121
+ """
122
+ if isinstance(format_type, str):
123
+ format_type = ReportFormatType.from_string(format_type)
124
+
125
+ self._registered[format_type] = reporter_class
126
+ logger.debug(f"Registered reporter {reporter_class.__name__} for {format_type.value}")
127
+
128
+ def register_factory(
129
+ self,
130
+ format_type: ReportFormatType | str,
131
+ factory_func: Callable[..., ReporterProtocol],
132
+ ) -> None:
133
+ """Register a factory function for creating reporters.
134
+
135
+ This is useful for reporters that need complex initialization
136
+ or dependency injection.
137
+
138
+ Args:
139
+ format_type: Report format.
140
+ factory_func: Factory function that returns a reporter.
141
+ """
142
+ if isinstance(format_type, str):
143
+ format_type = ReportFormatType.from_string(format_type)
144
+
145
+ self._factories[format_type] = factory_func
146
+ logger.debug(f"Registered factory for {format_type.value}")
147
+
148
+ def register_instance(
149
+ self,
150
+ format_name: str,
151
+ reporter: ReporterProtocol,
152
+ ) -> None:
153
+ """Register a specific reporter instance.
154
+
155
+ This is useful for sharing pre-configured reporter instances.
156
+
157
+ Args:
158
+ format_name: Format name (used as key).
159
+ reporter: Reporter instance.
160
+ """
161
+ self._instances[format_name.lower()] = reporter
162
+ logger.debug(f"Registered instance for {format_name}")
163
+
164
+ def unregister(self, format_type: ReportFormatType | str) -> None:
165
+ """Unregister a reporter for a format.
166
+
167
+ Args:
168
+ format_type: Format to unregister.
169
+ """
170
+ if isinstance(format_type, str):
171
+ format_type = ReportFormatType.from_string(format_type)
172
+
173
+ self._registered.pop(format_type, None)
174
+ self._factories.pop(format_type, None)
175
+ self._instances.pop(format_type.value, None)
176
+
177
+ def get_reporter(
178
+ self,
179
+ format: ReportFormatType | str,
180
+ config: ReporterConfig | None = None,
181
+ locale: str | None = None,
182
+ prefer_truthound: bool = False,
183
+ **kwargs: Any,
184
+ ) -> ReporterProtocol:
185
+ """Get a reporter for the specified format.
186
+
187
+ The factory tries backends in this order:
188
+ 1. Registered instance (exact match by format name)
189
+ 2. Registered factory function
190
+ 3. Registered class
191
+ 4. Built-in dashboard reporter (default)
192
+ 5. Truthound reporter (if prefer_truthound=True or format not in builtin)
193
+
194
+ Note: By default, built-in reporters are preferred because they work
195
+ reliably with our ReportData format. Truthound reporters require
196
+ a real truthound Report/ValidationResult object for full compatibility.
197
+
198
+ Args:
199
+ format: Report format (enum or string).
200
+ config: Optional configuration.
201
+ locale: Locale override (uses default if not specified).
202
+ prefer_truthound: If True, try truthound reporters before built-in.
203
+ **kwargs: Additional arguments passed to reporter constructor.
204
+
205
+ Returns:
206
+ Reporter instance.
207
+
208
+ Raises:
209
+ ValueError: If no reporter is available for the format.
210
+ """
211
+ # Normalize format
212
+ if isinstance(format, str):
213
+ format_str = format.lower()
214
+ try:
215
+ format_type = ReportFormatType.from_string(format_str)
216
+ except ValueError:
217
+ format_type = None
218
+ else:
219
+ format_type = format
220
+ format_str = format.value
221
+
222
+ effective_locale = locale or self._default_locale
223
+
224
+ # 1. Check for registered instance
225
+ if format_str in self._instances:
226
+ return self._instances[format_str]
227
+
228
+ # 2. Check for factory function
229
+ if format_type and format_type in self._factories:
230
+ return self._factories[format_type](
231
+ config=config,
232
+ locale=effective_locale,
233
+ **kwargs,
234
+ )
235
+
236
+ # 3. Check for registered class
237
+ if format_type and format_type in self._registered:
238
+ reporter_class = self._registered[format_type]
239
+ return reporter_class(**kwargs)
240
+
241
+ # 4 & 5. Try built-in or truthound based on preference
242
+ if prefer_truthound and self._use_truthound and self._is_truthound_available():
243
+ reporter = self._create_truthound_reporter(
244
+ format_str,
245
+ locale=effective_locale,
246
+ **kwargs,
247
+ )
248
+ if reporter:
249
+ return reporter
250
+
251
+ # 4. Try built-in reporter first (default preference)
252
+ if format_type:
253
+ reporter = self._create_builtin_reporter(format_type, locale=effective_locale)
254
+ if reporter:
255
+ return reporter
256
+
257
+ # 5. Try truthound reporter as fallback (for formats not in builtin)
258
+ if self._use_truthound and self._is_truthound_available():
259
+ reporter = self._create_truthound_reporter(
260
+ format_str,
261
+ locale=effective_locale,
262
+ **kwargs,
263
+ )
264
+ if reporter:
265
+ return reporter
266
+
267
+ raise ValueError(
268
+ f"No reporter available for format: {format_str}. "
269
+ f"Available formats: {self.get_available_formats()}"
270
+ )
271
+
272
+ def _create_truthound_reporter(
273
+ self,
274
+ format_name: str,
275
+ locale: str,
276
+ **kwargs: Any,
277
+ ) -> ReporterProtocol | None:
278
+ """Create a truthound reporter if available."""
279
+ from .adapters import create_truthound_reporter
280
+
281
+ return create_truthound_reporter(format_name, locale=locale, **kwargs)
282
+
283
+ def _create_builtin_reporter(
284
+ self,
285
+ format_type: ReportFormatType,
286
+ locale: str = "en",
287
+ ) -> ReporterProtocol | None:
288
+ """Create a built-in dashboard reporter."""
289
+ # Lazy import to avoid circular imports
290
+ try:
291
+ if format_type == ReportFormatType.JSON:
292
+ from .builtin.json_reporter import BuiltinJSONReporter
293
+
294
+ return BuiltinJSONReporter(locale=locale)
295
+
296
+ elif format_type == ReportFormatType.HTML:
297
+ from .builtin.html_reporter import BuiltinHTMLReporter
298
+
299
+ return BuiltinHTMLReporter(locale=locale)
300
+
301
+ elif format_type == ReportFormatType.CSV:
302
+ from .builtin.csv_reporter import BuiltinCSVReporter
303
+
304
+ return BuiltinCSVReporter()
305
+
306
+ except ImportError as e:
307
+ logger.warning(f"Built-in reporter for {format_type.value} not available: {e}")
308
+
309
+ return None
310
+
311
+ def get_available_formats(self) -> list[str]:
312
+ """Get list of available format names.
313
+
314
+ Returns:
315
+ List of format name strings.
316
+ """
317
+ formats = set()
318
+
319
+ # Add registered formats
320
+ for fmt in self._registered.keys():
321
+ formats.add(fmt.value)
322
+ for fmt in self._factories.keys():
323
+ formats.add(fmt.value)
324
+ for fmt in self._instances.keys():
325
+ formats.add(fmt)
326
+
327
+ # Add truthound formats
328
+ if self._use_truthound and self._is_truthound_available():
329
+ from .adapters import get_truthound_formats
330
+
331
+ formats.update(get_truthound_formats())
332
+
333
+ # Add built-in formats
334
+ builtin_formats = ["json", "html", "csv"]
335
+ formats.update(builtin_formats)
336
+
337
+ return sorted(formats)
338
+
339
+ def is_format_available(self, format: ReportFormatType | str) -> bool:
340
+ """Check if a format is available.
341
+
342
+ Args:
343
+ format: Format to check.
344
+
345
+ Returns:
346
+ True if the format is available.
347
+ """
348
+ if isinstance(format, str):
349
+ format_str = format.lower()
350
+ else:
351
+ format_str = format.value
352
+
353
+ return format_str in self.get_available_formats()
354
+
355
+
356
+ # Global factory instance
357
+ _factory: ReporterFactory | None = None
358
+
359
+
360
+ def get_reporter_factory(
361
+ use_truthound: bool = True,
362
+ default_locale: str = "en",
363
+ ) -> ReporterFactory:
364
+ """Get the global reporter factory.
365
+
366
+ Creates the factory on first call with default settings.
367
+ Subsequent calls return the same instance.
368
+
369
+ Args:
370
+ use_truthound: Whether to enable truthound backend (only on first call).
371
+ default_locale: Default locale (only on first call).
372
+
373
+ Returns:
374
+ Global ReporterFactory instance.
375
+ """
376
+ global _factory
377
+ if _factory is None:
378
+ _factory = ReporterFactory(
379
+ use_truthound=use_truthound,
380
+ default_locale=default_locale,
381
+ )
382
+ return _factory
383
+
384
+
385
+ def reset_factory() -> None:
386
+ """Reset the global factory (for testing)."""
387
+ global _factory
388
+ _factory = None
389
+
390
+
391
+ def register_reporter(
392
+ format_type: ReportFormatType | str,
393
+ ) -> Callable[[type[ReporterT]], type[ReporterT]]:
394
+ """Decorator to register a reporter class.
395
+
396
+ Example:
397
+ @register_reporter("custom")
398
+ class MyCustomReporter(BaseReporter):
399
+ ...
400
+ """
401
+
402
+ def decorator(cls: type[ReporterT]) -> type[ReporterT]:
403
+ get_reporter_factory().register(format_type, cls)
404
+ return cls
405
+
406
+ return decorator
407
+
408
+
409
+ # Convenience functions that delegate to the global factory
410
+
411
+
412
+ def get_reporter(
413
+ format: ReportFormatType | str,
414
+ config: ReporterConfig | None = None,
415
+ locale: str | None = None,
416
+ **kwargs: Any,
417
+ ) -> ReporterProtocol:
418
+ """Get a reporter for the specified format.
419
+
420
+ Convenience function that uses the global factory.
421
+
422
+ Args:
423
+ format: Report format.
424
+ config: Optional configuration.
425
+ locale: Locale override.
426
+ **kwargs: Additional arguments.
427
+
428
+ Returns:
429
+ Reporter instance.
430
+ """
431
+ return get_reporter_factory().get_reporter(
432
+ format,
433
+ config=config,
434
+ locale=locale,
435
+ **kwargs,
436
+ )
437
+
438
+
439
+ def get_available_formats() -> list[str]:
440
+ """Get list of available report formats.
441
+
442
+ Convenience function that uses the global factory.
443
+
444
+ Returns:
445
+ List of format name strings.
446
+ """
447
+ return get_reporter_factory().get_available_formats()
448
+
449
+
450
+ def is_format_available(format: ReportFormatType | str) -> bool:
451
+ """Check if a format is available.
452
+
453
+ Convenience function that uses the global factory.
454
+
455
+ Args:
456
+ format: Format to check.
457
+
458
+ Returns:
459
+ True if available.
460
+ """
461
+ return get_reporter_factory().is_format_available(format)
462
+
463
+
464
+ async def generate_report(
465
+ data: ReportData,
466
+ format: ReportFormatType | str = ReportFormatType.HTML,
467
+ config: ReporterConfig | None = None,
468
+ locale: str | None = None,
469
+ **kwargs: Any,
470
+ ) -> ReportOutput:
471
+ """Generate a report using the appropriate reporter.
472
+
473
+ High-level convenience function for report generation.
474
+
475
+ Args:
476
+ data: Report data.
477
+ format: Output format.
478
+ config: Reporter configuration.
479
+ locale: Locale for i18n.
480
+ **kwargs: Additional arguments passed to reporter.
481
+
482
+ Returns:
483
+ ReportOutput with generated content.
484
+
485
+ Example:
486
+ data = ReportData.from_validation_model(validation)
487
+ output = await generate_report(data, format="html", locale="ko")
488
+ """
489
+ reporter = get_reporter(format, config=config, locale=locale, **kwargs)
490
+ return await reporter.generate(data, config=config)
491
+
492
+
493
+ async def generate_report_from_validation(
494
+ validation: Any,
495
+ format: ReportFormatType | str = ReportFormatType.HTML,
496
+ config: ReporterConfig | None = None,
497
+ locale: str | None = None,
498
+ **kwargs: Any,
499
+ ) -> ReportOutput:
500
+ """Generate a report from a Validation model.
501
+
502
+ Convenience function that handles the conversion from
503
+ Validation model to ReportData.
504
+
505
+ Args:
506
+ validation: Validation model from database.
507
+ format: Output format.
508
+ config: Reporter configuration.
509
+ locale: Locale for i18n.
510
+ **kwargs: Additional arguments.
511
+
512
+ Returns:
513
+ ReportOutput with generated content.
514
+
515
+ Example:
516
+ validation = await service.get_validation(validation_id)
517
+ output = await generate_report_from_validation(
518
+ validation,
519
+ format="html",
520
+ locale="ko",
521
+ )
522
+ """
523
+ from .adapters import ValidationModelAdapter
524
+
525
+ data = ValidationModelAdapter.to_report_data(validation)
526
+ return await generate_report(data, format=format, config=config, locale=locale, **kwargs)