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,701 @@
1
+ """Quality Reporter API endpoints.
2
+
3
+ This module provides REST API endpoints for quality assessment and reporting
4
+ of validation rules, integrating with truthound's QualityReporter module.
5
+
6
+ Endpoints:
7
+ - GET /quality/formats - Get available report formats and options
8
+ - POST /quality/sources/{source_id}/score - Score validation rules
9
+ - POST /quality/sources/{source_id}/report - Generate quality report
10
+ - GET /quality/sources/{source_id}/report/download - Download report
11
+ - GET /quality/sources/{source_id}/summary - Get quality summary
12
+ - POST /quality/filter - Filter quality scores
13
+ - POST /quality/compare - Compare quality scores
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Annotated, Any
19
+
20
+ from fastapi import APIRouter, HTTPException, Path, Query
21
+ from fastapi.responses import Response
22
+
23
+ from truthound_dashboard.core.quality_reporter import (
24
+ QualityFilter,
25
+ QualityReportConfig,
26
+ QualityReportFormat,
27
+ QualityReportStatus,
28
+ QualityReporterService,
29
+ QualityThresholds,
30
+ )
31
+ from truthound_dashboard.schemas.quality_reporter import (
32
+ QualityCompareRequest,
33
+ QualityCompareResponse,
34
+ QualityFilterRequest,
35
+ QualityFormatsResponse,
36
+ QualityLevel,
37
+ QualityLevelDistribution,
38
+ QualityMetricsSchema,
39
+ QualityReportConfigSchema,
40
+ QualityReportFormat as QualityReportFormatSchema,
41
+ QualityReportGenerateRequest,
42
+ QualityReportResponse,
43
+ QualityReportStatus as QualityReportStatusSchema,
44
+ QualityScoreRequest,
45
+ QualityScoreResponse,
46
+ QualityScoreSchema,
47
+ QualityStatisticsSchema,
48
+ QualitySummaryResponse,
49
+ QualityThresholdsSchema,
50
+ )
51
+
52
+ from .deps import SessionDep
53
+
54
+ router = APIRouter(prefix="/quality")
55
+
56
+
57
+ # =============================================================================
58
+ # Dependency
59
+ # =============================================================================
60
+
61
+
62
+ async def get_quality_reporter_service(session: SessionDep) -> QualityReporterService:
63
+ """Get quality reporter service dependency."""
64
+ return QualityReporterService(session)
65
+
66
+
67
+ QualityReporterServiceDep = Annotated[
68
+ QualityReporterService,
69
+ __import__("fastapi").Depends(get_quality_reporter_service)
70
+ ]
71
+
72
+
73
+ # =============================================================================
74
+ # Helper Functions
75
+ # =============================================================================
76
+
77
+
78
+ def _convert_score_to_response(score: dict[str, Any]) -> QualityScoreSchema:
79
+ """Convert internal score to response schema."""
80
+ metrics = score.get("metrics", {})
81
+ return QualityScoreSchema(
82
+ rule_name=score["rule_name"],
83
+ rule_type=score.get("rule_type"),
84
+ column=score.get("column"),
85
+ metrics=QualityMetricsSchema(
86
+ f1_score=metrics.get("f1_score", 0.0),
87
+ precision=metrics.get("precision", 0.0),
88
+ recall=metrics.get("recall", 0.0),
89
+ accuracy=metrics.get("accuracy", 0.0),
90
+ confidence=metrics.get("confidence", 0.0),
91
+ quality_level=QualityLevel(metrics.get("quality_level", "unacceptable")),
92
+ ),
93
+ confusion_matrix=score.get("confusion_matrix"),
94
+ test_sample_size=score.get("test_sample_size", 0),
95
+ evaluation_time_ms=score.get("evaluation_time_ms", 0.0),
96
+ recommendation=score.get("recommendation"),
97
+ should_use=score.get("should_use", True),
98
+ issues=score.get("issues", []),
99
+ )
100
+
101
+
102
+ def _convert_statistics_to_response(
103
+ stats: dict[str, Any] | None,
104
+ ) -> QualityStatisticsSchema | None:
105
+ """Convert internal statistics to response schema."""
106
+ if not stats:
107
+ return None
108
+ return QualityStatisticsSchema(
109
+ total_count=stats.get("total_count", 0),
110
+ excellent_count=stats.get("excellent_count", 0),
111
+ good_count=stats.get("good_count", 0),
112
+ acceptable_count=stats.get("acceptable_count", 0),
113
+ poor_count=stats.get("poor_count", 0),
114
+ unacceptable_count=stats.get("unacceptable_count", 0),
115
+ should_use_count=stats.get("should_use_count", 0),
116
+ avg_f1=stats.get("avg_f1", 0.0),
117
+ min_f1=stats.get("min_f1", 0.0),
118
+ max_f1=stats.get("max_f1", 0.0),
119
+ avg_precision=stats.get("avg_precision", 0.0),
120
+ avg_recall=stats.get("avg_recall", 0.0),
121
+ avg_confidence=stats.get("avg_confidence", 0.0),
122
+ )
123
+
124
+
125
+ def _convert_distribution_to_response(
126
+ dist: list[dict[str, Any]] | None,
127
+ ) -> list[QualityLevelDistribution] | None:
128
+ """Convert internal distribution to response schema."""
129
+ if not dist:
130
+ return None
131
+ return [
132
+ QualityLevelDistribution(
133
+ level=QualityLevel(d["level"]),
134
+ count=d["count"],
135
+ percentage=d["percentage"],
136
+ )
137
+ for d in dist
138
+ ]
139
+
140
+
141
+ def _request_to_filter(request: QualityFilterRequest) -> QualityFilter:
142
+ """Convert request schema to internal filter."""
143
+ from truthound_dashboard.core.quality_reporter import (
144
+ QualityLevel as InternalLevel,
145
+ )
146
+
147
+ min_level = None
148
+ max_level = None
149
+ if request.min_level:
150
+ min_level = InternalLevel(request.min_level.value)
151
+ if request.max_level:
152
+ max_level = InternalLevel(request.max_level.value)
153
+
154
+ return QualityFilter(
155
+ min_level=min_level,
156
+ max_level=max_level,
157
+ min_f1=request.min_f1,
158
+ max_f1=request.max_f1,
159
+ min_confidence=request.min_confidence,
160
+ should_use_only=request.should_use_only,
161
+ include_columns=request.include_columns,
162
+ exclude_columns=request.exclude_columns,
163
+ rule_types=request.rule_types,
164
+ )
165
+
166
+
167
+ def _request_to_config(request: QualityReportConfigSchema | None) -> QualityReportConfig:
168
+ """Convert request schema to internal config."""
169
+ if not request:
170
+ return QualityReportConfig()
171
+
172
+ return QualityReportConfig(
173
+ title=request.title,
174
+ description=request.description,
175
+ include_metrics=request.include_metrics,
176
+ include_confusion_matrix=request.include_confusion_matrix,
177
+ include_recommendations=request.include_recommendations,
178
+ include_statistics=request.include_statistics,
179
+ include_summary=request.include_summary,
180
+ include_charts=request.include_charts,
181
+ metric_precision=request.metric_precision,
182
+ percentage_format=request.percentage_format,
183
+ sort_order=request.sort_order.value if request.sort_order else "f1_desc",
184
+ max_scores=request.max_scores,
185
+ theme=request.theme,
186
+ )
187
+
188
+
189
+ def _request_to_thresholds(
190
+ request: QualityThresholdsSchema | None,
191
+ ) -> QualityThresholds:
192
+ """Convert request schema to internal thresholds."""
193
+ if not request:
194
+ return QualityThresholds()
195
+
196
+ return QualityThresholds(
197
+ excellent=request.excellent,
198
+ good=request.good,
199
+ acceptable=request.acceptable,
200
+ poor=request.poor,
201
+ )
202
+
203
+
204
+ # =============================================================================
205
+ # Endpoints
206
+ # =============================================================================
207
+
208
+
209
+ @router.get(
210
+ "/formats",
211
+ response_model=QualityFormatsResponse,
212
+ summary="Get available quality report formats",
213
+ description="List all available report formats, sort orders, themes, and options",
214
+ )
215
+ async def get_formats(
216
+ service: QualityReporterServiceDep,
217
+ ) -> QualityFormatsResponse:
218
+ """Get available quality report formats and options.
219
+
220
+ Returns:
221
+ Available formats, sort orders, themes, and default quality thresholds.
222
+ """
223
+ data = service.get_available_formats()
224
+ return QualityFormatsResponse(
225
+ formats=data["formats"],
226
+ sort_orders=data["sort_orders"],
227
+ themes=data["themes"],
228
+ default_thresholds=QualityThresholdsSchema(**data["default_thresholds"]),
229
+ )
230
+
231
+
232
+ @router.post(
233
+ "/sources/{source_id}/score",
234
+ response_model=QualityScoreResponse,
235
+ summary="Score validation rules for a source",
236
+ description="Evaluate quality of validation rules using F1 score, precision, recall, and accuracy",
237
+ )
238
+ async def score_source(
239
+ service: QualityReporterServiceDep,
240
+ source_id: Annotated[str, Path(description="Source ID to score")],
241
+ request: QualityScoreRequest | None = None,
242
+ ) -> QualityScoreResponse:
243
+ """Score validation rules for a source.
244
+
245
+ This endpoint evaluates the quality of validation rules by:
246
+ - Computing F1 score, precision, recall, and accuracy
247
+ - Assigning quality levels (excellent, good, acceptable, poor, unacceptable)
248
+ - Generating recommendations for rule usage
249
+
250
+ Args:
251
+ service: Quality reporter service.
252
+ source_id: Source ID to score.
253
+ request: Optional scoring configuration.
254
+
255
+ Returns:
256
+ Quality score result with metrics and statistics.
257
+ """
258
+ request = request or QualityScoreRequest()
259
+ thresholds = _request_to_thresholds(request.thresholds)
260
+
261
+ result = await service.score_source(
262
+ source_id,
263
+ validation_id=request.validation_id,
264
+ rule_names=request.rule_names,
265
+ sample_size=request.sample_size,
266
+ thresholds=thresholds,
267
+ )
268
+
269
+ # Convert internal result to response
270
+ scores = [_convert_score_to_response(s.to_dict()) for s in result.scores]
271
+ statistics = _convert_statistics_to_response(
272
+ result.statistics.to_dict() if result.statistics else None
273
+ )
274
+ distribution = _convert_distribution_to_response(
275
+ [d.to_dict() for d in result.level_distribution] if result.level_distribution else None
276
+ )
277
+
278
+ return QualityScoreResponse(
279
+ id=result.id,
280
+ source_id=result.source_id,
281
+ source_name=result.source_name,
282
+ validation_id=result.validation_id,
283
+ status=QualityReportStatusSchema(result.status.value),
284
+ scores=scores,
285
+ statistics=statistics,
286
+ level_distribution=distribution,
287
+ sample_size=result.sample_size,
288
+ evaluation_time_ms=result.evaluation_time_ms,
289
+ error_message=result.error_message,
290
+ created_at=result.created_at,
291
+ updated_at=result.updated_at,
292
+ )
293
+
294
+
295
+ @router.post(
296
+ "/sources/{source_id}/report",
297
+ response_model=QualityReportResponse,
298
+ summary="Generate quality report",
299
+ description="Generate a comprehensive quality report in various formats (HTML, JSON, Markdown, etc.)",
300
+ )
301
+ async def generate_report(
302
+ service: QualityReporterServiceDep,
303
+ source_id: Annotated[str, Path(description="Source ID for the report")],
304
+ request: QualityReportGenerateRequest | None = None,
305
+ ) -> QualityReportResponse:
306
+ """Generate a quality report for a source.
307
+
308
+ Generates a comprehensive quality report that includes:
309
+ - Quality scores for all validation rules
310
+ - Aggregate statistics and level distribution
311
+ - Visual charts (HTML format)
312
+ - Recommendations
313
+
314
+ Args:
315
+ service: Quality reporter service.
316
+ source_id: Source ID for the report.
317
+ request: Report generation configuration.
318
+
319
+ Returns:
320
+ Generated report metadata.
321
+ """
322
+ request = request or QualityReportGenerateRequest()
323
+
324
+ # Convert format enum
325
+ from truthound_dashboard.core.quality_reporter import (
326
+ QualityReportFormat as InternalFormat,
327
+ )
328
+
329
+ format_value = request.format.value if request.format else "html"
330
+ internal_format = InternalFormat(format_value)
331
+
332
+ # Convert config and filter
333
+ config = _request_to_config(request.config)
334
+ filter_config = _request_to_filter(request.filter) if request.filter else None
335
+
336
+ result = await service.generate_report(
337
+ source_id=source_id,
338
+ validation_id=request.validation_id,
339
+ format=internal_format,
340
+ config=config,
341
+ filter_config=filter_config,
342
+ score_rules=request.score_rules,
343
+ sample_size=request.sample_size,
344
+ )
345
+
346
+ statistics = _convert_statistics_to_response(
347
+ result.statistics.to_dict() if result.statistics else None
348
+ )
349
+
350
+ return QualityReportResponse(
351
+ id=result.id,
352
+ source_id=result.source_id,
353
+ source_name=result.source_name,
354
+ validation_id=result.validation_id,
355
+ format=QualityReportFormatSchema(result.format.value),
356
+ status=QualityReportStatusSchema(result.status.value),
357
+ filename=result.filename,
358
+ file_path=result.file_path,
359
+ file_size_bytes=result.file_size_bytes,
360
+ content_type=result.content_type,
361
+ generation_time_ms=result.generation_time_ms,
362
+ scores_count=result.scores_count,
363
+ statistics=statistics,
364
+ error_message=result.error_message,
365
+ download_count=result.download_count,
366
+ expires_at=result.expires_at,
367
+ created_at=result.created_at,
368
+ updated_at=result.updated_at,
369
+ )
370
+
371
+
372
+ @router.get(
373
+ "/sources/{source_id}/report/download",
374
+ summary="Download quality report",
375
+ description="Download the generated quality report content",
376
+ )
377
+ async def download_report(
378
+ service: QualityReporterServiceDep,
379
+ source_id: Annotated[str, Path(description="Source ID")],
380
+ format: Annotated[
381
+ QualityReportFormatSchema,
382
+ Query(description="Report format"),
383
+ ] = QualityReportFormatSchema.HTML,
384
+ title: Annotated[str | None, Query(description="Report title")] = None,
385
+ include_charts: Annotated[
386
+ bool, Query(description="Include charts (HTML only)")
387
+ ] = True,
388
+ theme: Annotated[
389
+ str, Query(description="Report theme")
390
+ ] = "professional",
391
+ max_scores: Annotated[
392
+ int | None, Query(description="Maximum scores to include", ge=1)
393
+ ] = None,
394
+ ) -> Response:
395
+ """Download quality report as file.
396
+
397
+ Args:
398
+ service: Quality reporter service.
399
+ source_id: Source ID.
400
+ format: Report format.
401
+ title: Optional report title.
402
+ include_charts: Include charts in HTML reports.
403
+ theme: Report theme.
404
+ max_scores: Maximum scores to include.
405
+
406
+ Returns:
407
+ Report file content.
408
+ """
409
+ from truthound_dashboard.core.quality_reporter import (
410
+ QualityReportFormat as InternalFormat,
411
+ )
412
+
413
+ config = QualityReportConfig(
414
+ title=title,
415
+ include_charts=include_charts,
416
+ theme=theme,
417
+ max_scores=max_scores,
418
+ )
419
+
420
+ internal_format = InternalFormat(format.value)
421
+ result = await service.generate_report(
422
+ source_id=source_id,
423
+ format=internal_format,
424
+ config=config,
425
+ )
426
+
427
+ if result.status != QualityReportStatus.COMPLETED or not result.content:
428
+ raise HTTPException(
429
+ status_code=500,
430
+ detail=result.error_message or "Failed to generate report",
431
+ )
432
+
433
+ # Determine media type
434
+ media_types = {
435
+ QualityReportFormatSchema.CONSOLE: "text/plain",
436
+ QualityReportFormatSchema.JSON: "application/json",
437
+ QualityReportFormatSchema.HTML: "text/html",
438
+ QualityReportFormatSchema.MARKDOWN: "text/markdown",
439
+ QualityReportFormatSchema.JUNIT: "application/xml",
440
+ }
441
+ media_type = media_types.get(format, "text/plain")
442
+
443
+ return Response(
444
+ content=result.content,
445
+ media_type=media_type,
446
+ headers={
447
+ "Content-Disposition": f'attachment; filename="{result.filename}"',
448
+ },
449
+ )
450
+
451
+
452
+ @router.get(
453
+ "/sources/{source_id}/report/preview",
454
+ summary="Preview quality report",
455
+ description="Preview the quality report content inline",
456
+ )
457
+ async def preview_report(
458
+ service: QualityReporterServiceDep,
459
+ source_id: Annotated[str, Path(description="Source ID")],
460
+ format: Annotated[
461
+ QualityReportFormatSchema,
462
+ Query(description="Report format"),
463
+ ] = QualityReportFormatSchema.HTML,
464
+ theme: Annotated[str, Query(description="Report theme")] = "professional",
465
+ max_scores: Annotated[
466
+ int | None, Query(description="Maximum scores", ge=1)
467
+ ] = 20,
468
+ ) -> Response:
469
+ """Preview quality report inline.
470
+
471
+ Args:
472
+ service: Quality reporter service.
473
+ source_id: Source ID.
474
+ format: Report format.
475
+ theme: Report theme.
476
+ max_scores: Maximum scores to include.
477
+
478
+ Returns:
479
+ Report content for inline display.
480
+ """
481
+ from truthound_dashboard.core.quality_reporter import (
482
+ QualityReportFormat as InternalFormat,
483
+ )
484
+
485
+ config = QualityReportConfig(
486
+ theme=theme,
487
+ max_scores=max_scores,
488
+ include_charts=True,
489
+ )
490
+
491
+ internal_format = InternalFormat(format.value)
492
+ result = await service.generate_report(
493
+ source_id=source_id,
494
+ format=internal_format,
495
+ config=config,
496
+ )
497
+
498
+ if result.status != QualityReportStatus.COMPLETED or not result.content:
499
+ raise HTTPException(
500
+ status_code=500,
501
+ detail=result.error_message or "Failed to generate report",
502
+ )
503
+
504
+ media_types = {
505
+ QualityReportFormatSchema.CONSOLE: "text/plain",
506
+ QualityReportFormatSchema.JSON: "application/json",
507
+ QualityReportFormatSchema.HTML: "text/html",
508
+ QualityReportFormatSchema.MARKDOWN: "text/markdown",
509
+ QualityReportFormatSchema.JUNIT: "application/xml",
510
+ }
511
+ media_type = media_types.get(format, "text/plain")
512
+
513
+ return Response(content=result.content, media_type=media_type)
514
+
515
+
516
+ @router.get(
517
+ "/sources/{source_id}/summary",
518
+ response_model=QualitySummaryResponse,
519
+ summary="Get quality summary",
520
+ description="Get a summary of quality scores for a source",
521
+ )
522
+ async def get_summary(
523
+ service: QualityReporterServiceDep,
524
+ source_id: Annotated[str, Path(description="Source ID")],
525
+ validation_id: Annotated[
526
+ str | None, Query(description="Validation ID")
527
+ ] = None,
528
+ sample_size: Annotated[
529
+ int, Query(description="Sample size", ge=100, le=1000000)
530
+ ] = 10000,
531
+ ) -> QualitySummaryResponse:
532
+ """Get quality summary for a source.
533
+
534
+ Returns aggregate statistics including:
535
+ - Total rules scored
536
+ - Quality level distribution
537
+ - Recommendations summary
538
+ - Metric averages (F1, precision, recall, confidence)
539
+
540
+ Args:
541
+ service: Quality reporter service.
542
+ source_id: Source ID.
543
+ validation_id: Optional validation ID.
544
+ sample_size: Sample size for scoring.
545
+
546
+ Returns:
547
+ Quality summary.
548
+ """
549
+ summary = await service.get_summary(
550
+ source_id,
551
+ validation_id=validation_id,
552
+ sample_size=sample_size,
553
+ )
554
+
555
+ if "error" in summary:
556
+ raise HTTPException(status_code=500, detail=summary["error"])
557
+
558
+ statistics = _convert_statistics_to_response(summary["statistics"])
559
+ if not statistics:
560
+ statistics = QualityStatisticsSchema()
561
+
562
+ distribution = [
563
+ QualityLevelDistribution(
564
+ level=QualityLevel(d["level"]),
565
+ count=d["count"],
566
+ percentage=d["percentage"],
567
+ )
568
+ for d in summary.get("level_distribution", [])
569
+ ]
570
+
571
+ return QualitySummaryResponse(
572
+ total_rules=summary["total_rules"],
573
+ statistics=statistics,
574
+ level_distribution=distribution,
575
+ recommendations=summary["recommendations"],
576
+ metric_averages=summary["metric_averages"],
577
+ )
578
+
579
+
580
+ @router.post(
581
+ "/compare",
582
+ response_model=QualityCompareResponse,
583
+ summary="Compare quality scores",
584
+ description="Compare and rank quality scores across sources",
585
+ )
586
+ async def compare_scores(
587
+ service: QualityReporterServiceDep,
588
+ request: QualityCompareRequest,
589
+ ) -> QualityCompareResponse:
590
+ """Compare quality scores across sources.
591
+
592
+ Compares and ranks quality scores by specified metrics.
593
+ Can optionally group results by column, level, or rule type.
594
+
595
+ Args:
596
+ service: Quality reporter service.
597
+ request: Comparison configuration.
598
+
599
+ Returns:
600
+ Comparison result with ranked scores.
601
+ """
602
+ # Get scores from sources
603
+ all_scores = []
604
+
605
+ if request.source_ids:
606
+ for source_id in request.source_ids:
607
+ result = await service.score_source(source_id)
608
+ if result.status == QualityReportStatus.COMPLETED:
609
+ all_scores.extend(result.scores)
610
+
611
+ if not all_scores:
612
+ raise HTTPException(
613
+ status_code=400,
614
+ detail="No scores available for comparison",
615
+ )
616
+
617
+ # Compare scores
618
+ comparison = await service.compare_scores(
619
+ all_scores,
620
+ sort_by=request.sort_by,
621
+ descending=request.descending,
622
+ group_by=request.group_by,
623
+ max_results=request.max_results,
624
+ )
625
+
626
+ # Convert to response
627
+ scores = [
628
+ _convert_score_to_response(s) for s in comparison.get("scores", [])
629
+ ]
630
+
631
+ best_rule = None
632
+ if comparison.get("best_rule"):
633
+ best_rule = _convert_score_to_response(comparison["best_rule"])
634
+
635
+ worst_rule = None
636
+ if comparison.get("worst_rule"):
637
+ worst_rule = _convert_score_to_response(comparison["worst_rule"])
638
+
639
+ statistics = _convert_statistics_to_response(comparison.get("statistics"))
640
+
641
+ # Convert groups
642
+ groups = None
643
+ if comparison.get("groups"):
644
+ groups = {
645
+ key: [_convert_score_to_response(s) for s in group_scores]
646
+ for key, group_scores in comparison["groups"].items()
647
+ }
648
+
649
+ return QualityCompareResponse(
650
+ scores=scores,
651
+ ranked_by=comparison["ranked_by"],
652
+ best_rule=best_rule,
653
+ worst_rule=worst_rule,
654
+ groups=groups,
655
+ statistics=statistics,
656
+ )
657
+
658
+
659
+ @router.post(
660
+ "/filter",
661
+ response_model=list[QualityScoreSchema],
662
+ summary="Filter quality scores",
663
+ description="Filter quality scores by various criteria",
664
+ )
665
+ async def filter_scores(
666
+ service: QualityReporterServiceDep,
667
+ source_id: Annotated[str, Query(description="Source ID to filter scores from")],
668
+ request: QualityFilterRequest,
669
+ ) -> list[QualityScoreSchema]:
670
+ """Filter quality scores by criteria.
671
+
672
+ Available filters:
673
+ - Quality level (min/max)
674
+ - F1 score range
675
+ - Confidence threshold
676
+ - Specific columns
677
+ - Rule types
678
+ - Should-use recommendation
679
+
680
+ Args:
681
+ service: Quality reporter service.
682
+ source_id: Source ID to get scores from.
683
+ request: Filter configuration.
684
+
685
+ Returns:
686
+ Filtered quality scores.
687
+ """
688
+ # Get scores first
689
+ result = await service.score_source(source_id)
690
+
691
+ if result.status != QualityReportStatus.COMPLETED:
692
+ raise HTTPException(
693
+ status_code=500,
694
+ detail=result.error_message or "Failed to score source",
695
+ )
696
+
697
+ # Apply filter
698
+ filter_config = _request_to_filter(request)
699
+ filtered = await service.filter_scores(result.scores, filter_config)
700
+
701
+ return [_convert_score_to_response(s.to_dict()) for s in filtered]