truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.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.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +437 -10
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +11 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""Reporter interfaces for validation result reporting.
|
|
2
|
+
|
|
3
|
+
Reporters generate formatted reports from validation results.
|
|
4
|
+
They support multiple output formats (HTML, CSV, JSON)
|
|
5
|
+
and can be customized for different use cases.
|
|
6
|
+
|
|
7
|
+
This module defines abstract interfaces for reporters that are loosely
|
|
8
|
+
coupled from truthound's reporters module.
|
|
9
|
+
|
|
10
|
+
Reporter features:
|
|
11
|
+
- Multiple output formats
|
|
12
|
+
- Template-based customization
|
|
13
|
+
- Localization support
|
|
14
|
+
- Custom reporter plugins
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from truthound_dashboard.core.interfaces.checkpoint import CheckpointResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReportFormat(str, Enum):
|
|
31
|
+
"""Supported report formats."""
|
|
32
|
+
|
|
33
|
+
HTML = "html"
|
|
34
|
+
CSV = "csv"
|
|
35
|
+
JSON = "json"
|
|
36
|
+
SLACK = "slack" # Slack-formatted blocks
|
|
37
|
+
TEXT = "text"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ReporterConfig:
|
|
42
|
+
"""Configuration for report generation.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
format: Output format.
|
|
46
|
+
template: Template name or path.
|
|
47
|
+
locale: Locale for localization.
|
|
48
|
+
title: Report title.
|
|
49
|
+
description: Report description.
|
|
50
|
+
include_summary: Include summary section.
|
|
51
|
+
include_issues: Include issues detail.
|
|
52
|
+
include_statistics: Include statistics.
|
|
53
|
+
include_charts: Include visualizations.
|
|
54
|
+
max_issues: Maximum issues to include.
|
|
55
|
+
output_path: Path for file output.
|
|
56
|
+
metadata: Additional metadata.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
format: ReportFormat = ReportFormat.HTML
|
|
60
|
+
template: str | None = None
|
|
61
|
+
locale: str = "en"
|
|
62
|
+
title: str = "Validation Report"
|
|
63
|
+
description: str = ""
|
|
64
|
+
include_summary: bool = True
|
|
65
|
+
include_issues: bool = True
|
|
66
|
+
include_statistics: bool = True
|
|
67
|
+
include_charts: bool = True
|
|
68
|
+
max_issues: int = 1000
|
|
69
|
+
output_path: str | None = None
|
|
70
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict[str, Any]:
|
|
73
|
+
"""Convert to dictionary."""
|
|
74
|
+
return {
|
|
75
|
+
"format": self.format.value,
|
|
76
|
+
"template": self.template,
|
|
77
|
+
"locale": self.locale,
|
|
78
|
+
"title": self.title,
|
|
79
|
+
"description": self.description,
|
|
80
|
+
"include_summary": self.include_summary,
|
|
81
|
+
"include_issues": self.include_issues,
|
|
82
|
+
"include_statistics": self.include_statistics,
|
|
83
|
+
"include_charts": self.include_charts,
|
|
84
|
+
"max_issues": self.max_issues,
|
|
85
|
+
"output_path": self.output_path,
|
|
86
|
+
"metadata": self.metadata,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_dict(cls, data: dict[str, Any]) -> "ReporterConfig":
|
|
91
|
+
"""Create from dictionary."""
|
|
92
|
+
format_str = data.get("format", "html")
|
|
93
|
+
if isinstance(format_str, str):
|
|
94
|
+
format_enum = ReportFormat(format_str)
|
|
95
|
+
else:
|
|
96
|
+
format_enum = format_str
|
|
97
|
+
|
|
98
|
+
return cls(
|
|
99
|
+
format=format_enum,
|
|
100
|
+
template=data.get("template"),
|
|
101
|
+
locale=data.get("locale", "en"),
|
|
102
|
+
title=data.get("title", "Validation Report"),
|
|
103
|
+
description=data.get("description", ""),
|
|
104
|
+
include_summary=data.get("include_summary", True),
|
|
105
|
+
include_issues=data.get("include_issues", True),
|
|
106
|
+
include_statistics=data.get("include_statistics", True),
|
|
107
|
+
include_charts=data.get("include_charts", True),
|
|
108
|
+
max_issues=data.get("max_issues", 1000),
|
|
109
|
+
output_path=data.get("output_path"),
|
|
110
|
+
metadata=data.get("metadata", {}),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ReportData:
|
|
116
|
+
"""Data container for report generation.
|
|
117
|
+
|
|
118
|
+
This is the standardized input for reporters, decoupled from
|
|
119
|
+
any specific validation result format.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
run_id: Validation run identifier.
|
|
123
|
+
checkpoint_name: Checkpoint name.
|
|
124
|
+
source_name: Data source name.
|
|
125
|
+
status: Validation status.
|
|
126
|
+
generated_at: Report generation time.
|
|
127
|
+
validation_started_at: When validation started.
|
|
128
|
+
validation_completed_at: When validation completed.
|
|
129
|
+
duration_ms: Validation duration.
|
|
130
|
+
row_count: Number of rows validated.
|
|
131
|
+
column_count: Number of columns.
|
|
132
|
+
issue_count: Total issues found.
|
|
133
|
+
critical_count: Critical issues.
|
|
134
|
+
high_count: High severity issues.
|
|
135
|
+
medium_count: Medium severity issues.
|
|
136
|
+
low_count: Low severity issues.
|
|
137
|
+
issues: List of issue dictionaries.
|
|
138
|
+
summary: Summary statistics.
|
|
139
|
+
metadata: Additional metadata.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
run_id: str
|
|
143
|
+
checkpoint_name: str
|
|
144
|
+
source_name: str
|
|
145
|
+
status: str
|
|
146
|
+
generated_at: datetime = field(default_factory=datetime.now)
|
|
147
|
+
validation_started_at: datetime | None = None
|
|
148
|
+
validation_completed_at: datetime | None = None
|
|
149
|
+
duration_ms: float = 0.0
|
|
150
|
+
row_count: int = 0
|
|
151
|
+
column_count: int = 0
|
|
152
|
+
issue_count: int = 0
|
|
153
|
+
critical_count: int = 0
|
|
154
|
+
high_count: int = 0
|
|
155
|
+
medium_count: int = 0
|
|
156
|
+
low_count: int = 0
|
|
157
|
+
issues: list[dict[str, Any]] = field(default_factory=list)
|
|
158
|
+
summary: dict[str, Any] = field(default_factory=dict)
|
|
159
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
160
|
+
|
|
161
|
+
def to_dict(self) -> dict[str, Any]:
|
|
162
|
+
"""Convert to dictionary."""
|
|
163
|
+
return {
|
|
164
|
+
"run_id": self.run_id,
|
|
165
|
+
"checkpoint_name": self.checkpoint_name,
|
|
166
|
+
"source_name": self.source_name,
|
|
167
|
+
"status": self.status,
|
|
168
|
+
"generated_at": self.generated_at.isoformat(),
|
|
169
|
+
"validation_started_at": (
|
|
170
|
+
self.validation_started_at.isoformat()
|
|
171
|
+
if self.validation_started_at else None
|
|
172
|
+
),
|
|
173
|
+
"validation_completed_at": (
|
|
174
|
+
self.validation_completed_at.isoformat()
|
|
175
|
+
if self.validation_completed_at else None
|
|
176
|
+
),
|
|
177
|
+
"duration_ms": self.duration_ms,
|
|
178
|
+
"row_count": self.row_count,
|
|
179
|
+
"column_count": self.column_count,
|
|
180
|
+
"issue_count": self.issue_count,
|
|
181
|
+
"critical_count": self.critical_count,
|
|
182
|
+
"high_count": self.high_count,
|
|
183
|
+
"medium_count": self.medium_count,
|
|
184
|
+
"low_count": self.low_count,
|
|
185
|
+
"issues": self.issues,
|
|
186
|
+
"summary": self.summary,
|
|
187
|
+
"metadata": self.metadata,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_checkpoint_result(
|
|
192
|
+
cls,
|
|
193
|
+
result: "CheckpointResult",
|
|
194
|
+
) -> "ReportData":
|
|
195
|
+
"""Create from a checkpoint result.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
result: Checkpoint result.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
ReportData instance.
|
|
202
|
+
"""
|
|
203
|
+
# Build summary
|
|
204
|
+
summary = {
|
|
205
|
+
"total_issues": result.issue_count,
|
|
206
|
+
"by_severity": {
|
|
207
|
+
"critical": result.critical_count,
|
|
208
|
+
"high": result.high_count,
|
|
209
|
+
"medium": result.medium_count,
|
|
210
|
+
"low": result.low_count,
|
|
211
|
+
},
|
|
212
|
+
"pass_rate": (
|
|
213
|
+
1 - (result.issue_count / result.row_count)
|
|
214
|
+
if result.row_count > 0 else 1.0
|
|
215
|
+
),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return cls(
|
|
219
|
+
run_id=result.run_id,
|
|
220
|
+
checkpoint_name=result.checkpoint_name,
|
|
221
|
+
source_name=result.source_name,
|
|
222
|
+
status=result.status.value,
|
|
223
|
+
validation_started_at=result.started_at,
|
|
224
|
+
validation_completed_at=result.completed_at,
|
|
225
|
+
duration_ms=result.duration_ms,
|
|
226
|
+
row_count=result.row_count,
|
|
227
|
+
column_count=result.column_count,
|
|
228
|
+
issue_count=result.issue_count,
|
|
229
|
+
critical_count=result.critical_count,
|
|
230
|
+
high_count=result.high_count,
|
|
231
|
+
medium_count=result.medium_count,
|
|
232
|
+
low_count=result.low_count,
|
|
233
|
+
issues=result.issues,
|
|
234
|
+
summary=summary,
|
|
235
|
+
metadata=result.metadata,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class ReportOutput:
|
|
241
|
+
"""Output from report generation.
|
|
242
|
+
|
|
243
|
+
Attributes:
|
|
244
|
+
format: Output format.
|
|
245
|
+
content: Report content (string for text formats, bytes for binary).
|
|
246
|
+
file_path: Path to output file (if saved).
|
|
247
|
+
mime_type: MIME type of the output.
|
|
248
|
+
size_bytes: Size in bytes.
|
|
249
|
+
generated_at: When report was generated.
|
|
250
|
+
metadata: Additional output metadata.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
format: ReportFormat
|
|
254
|
+
content: str | bytes
|
|
255
|
+
file_path: str | None = None
|
|
256
|
+
mime_type: str = "text/html"
|
|
257
|
+
size_bytes: int = 0
|
|
258
|
+
generated_at: datetime = field(default_factory=datetime.now)
|
|
259
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
260
|
+
|
|
261
|
+
def __post_init__(self):
|
|
262
|
+
"""Calculate size if not set."""
|
|
263
|
+
if self.size_bytes == 0:
|
|
264
|
+
if isinstance(self.content, bytes):
|
|
265
|
+
self.size_bytes = len(self.content)
|
|
266
|
+
else:
|
|
267
|
+
self.size_bytes = len(self.content.encode("utf-8"))
|
|
268
|
+
|
|
269
|
+
def save(self, path: str | Path) -> str:
|
|
270
|
+
"""Save report content to file.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
path: Output path.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Absolute path to saved file.
|
|
277
|
+
"""
|
|
278
|
+
path = Path(path)
|
|
279
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
|
|
281
|
+
if isinstance(self.content, bytes):
|
|
282
|
+
path.write_bytes(self.content)
|
|
283
|
+
else:
|
|
284
|
+
path.write_text(self.content, encoding="utf-8")
|
|
285
|
+
|
|
286
|
+
self.file_path = str(path.absolute())
|
|
287
|
+
return self.file_path
|
|
288
|
+
|
|
289
|
+
def to_dict(self) -> dict[str, Any]:
|
|
290
|
+
"""Convert to dictionary (without content)."""
|
|
291
|
+
return {
|
|
292
|
+
"format": self.format.value,
|
|
293
|
+
"file_path": self.file_path,
|
|
294
|
+
"mime_type": self.mime_type,
|
|
295
|
+
"size_bytes": self.size_bytes,
|
|
296
|
+
"generated_at": self.generated_at.isoformat(),
|
|
297
|
+
"metadata": self.metadata,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@runtime_checkable
|
|
302
|
+
class ReporterProtocol(Protocol):
|
|
303
|
+
"""Protocol for reporter implementations.
|
|
304
|
+
|
|
305
|
+
Reporters generate formatted reports from validation data.
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
class HTMLReporter:
|
|
309
|
+
def generate(self, data: ReportData, config: ReporterConfig) -> ReportOutput:
|
|
310
|
+
html = render_template(data, config)
|
|
311
|
+
return ReportOutput(format=ReportFormat.HTML, content=html)
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def name(self) -> str:
|
|
316
|
+
"""Get reporter name."""
|
|
317
|
+
...
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def supported_formats(self) -> list[ReportFormat]:
|
|
321
|
+
"""Get supported output formats."""
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
def generate(
|
|
325
|
+
self,
|
|
326
|
+
data: ReportData,
|
|
327
|
+
config: ReporterConfig | None = None,
|
|
328
|
+
) -> ReportOutput:
|
|
329
|
+
"""Generate a report synchronously.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
data: Report data.
|
|
333
|
+
config: Report configuration.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Report output.
|
|
337
|
+
"""
|
|
338
|
+
...
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@runtime_checkable
|
|
342
|
+
class AsyncReporterProtocol(Protocol):
|
|
343
|
+
"""Protocol for async reporter implementations."""
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def name(self) -> str:
|
|
347
|
+
"""Get reporter name."""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def supported_formats(self) -> list[ReportFormat]:
|
|
352
|
+
"""Get supported output formats."""
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
async def generate_async(
|
|
356
|
+
self,
|
|
357
|
+
data: ReportData,
|
|
358
|
+
config: ReporterConfig | None = None,
|
|
359
|
+
) -> ReportOutput:
|
|
360
|
+
"""Generate a report asynchronously.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
data: Report data.
|
|
364
|
+
config: Report configuration.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Report output.
|
|
368
|
+
"""
|
|
369
|
+
...
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class BaseReporter(ABC):
|
|
373
|
+
"""Abstract base class for reporters.
|
|
374
|
+
|
|
375
|
+
Provides common functionality for all reporters.
|
|
376
|
+
Subclasses must implement the _do_generate method.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
def __init__(
|
|
380
|
+
self,
|
|
381
|
+
name: str | None = None,
|
|
382
|
+
default_config: ReporterConfig | None = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Initialize reporter.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
name: Reporter name.
|
|
388
|
+
default_config: Default configuration.
|
|
389
|
+
"""
|
|
390
|
+
self._name = name or self.__class__.__name__
|
|
391
|
+
self._default_config = default_config or ReporterConfig()
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def name(self) -> str:
|
|
395
|
+
"""Get reporter name."""
|
|
396
|
+
return self._name
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
@abstractmethod
|
|
400
|
+
def supported_formats(self) -> list[ReportFormat]:
|
|
401
|
+
"""Get supported output formats."""
|
|
402
|
+
...
|
|
403
|
+
|
|
404
|
+
def generate(
|
|
405
|
+
self,
|
|
406
|
+
data: ReportData,
|
|
407
|
+
config: ReporterConfig | None = None,
|
|
408
|
+
) -> ReportOutput:
|
|
409
|
+
"""Generate a report.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
data: Report data.
|
|
413
|
+
config: Report configuration.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Report output.
|
|
417
|
+
"""
|
|
418
|
+
config = config or self._default_config
|
|
419
|
+
|
|
420
|
+
# Validate format
|
|
421
|
+
if config.format not in self.supported_formats:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Format {config.format} not supported by {self.name}. "
|
|
424
|
+
f"Supported: {[f.value for f in self.supported_formats]}"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
output = self._do_generate(data, config)
|
|
428
|
+
|
|
429
|
+
# Save to file if path specified
|
|
430
|
+
if config.output_path:
|
|
431
|
+
output.save(config.output_path)
|
|
432
|
+
|
|
433
|
+
return output
|
|
434
|
+
|
|
435
|
+
@abstractmethod
|
|
436
|
+
def _do_generate(
|
|
437
|
+
self,
|
|
438
|
+
data: ReportData,
|
|
439
|
+
config: ReporterConfig,
|
|
440
|
+
) -> ReportOutput:
|
|
441
|
+
"""Perform the actual report generation.
|
|
442
|
+
|
|
443
|
+
Subclasses must implement this method.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
data: Report data.
|
|
447
|
+
config: Report configuration.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Report output.
|
|
451
|
+
"""
|
|
452
|
+
...
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class AsyncBaseReporter(ABC):
|
|
456
|
+
"""Abstract base class for async reporters."""
|
|
457
|
+
|
|
458
|
+
def __init__(
|
|
459
|
+
self,
|
|
460
|
+
name: str | None = None,
|
|
461
|
+
default_config: ReporterConfig | None = None,
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Initialize reporter."""
|
|
464
|
+
self._name = name or self.__class__.__name__
|
|
465
|
+
self._default_config = default_config or ReporterConfig()
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def name(self) -> str:
|
|
469
|
+
"""Get reporter name."""
|
|
470
|
+
return self._name
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
@abstractmethod
|
|
474
|
+
def supported_formats(self) -> list[ReportFormat]:
|
|
475
|
+
"""Get supported output formats."""
|
|
476
|
+
...
|
|
477
|
+
|
|
478
|
+
async def generate_async(
|
|
479
|
+
self,
|
|
480
|
+
data: ReportData,
|
|
481
|
+
config: ReporterConfig | None = None,
|
|
482
|
+
) -> ReportOutput:
|
|
483
|
+
"""Generate a report asynchronously."""
|
|
484
|
+
config = config or self._default_config
|
|
485
|
+
|
|
486
|
+
if config.format not in self.supported_formats:
|
|
487
|
+
raise ValueError(
|
|
488
|
+
f"Format {config.format} not supported by {self.name}"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
output = await self._do_generate_async(data, config)
|
|
492
|
+
|
|
493
|
+
if config.output_path:
|
|
494
|
+
output.save(config.output_path)
|
|
495
|
+
|
|
496
|
+
return output
|
|
497
|
+
|
|
498
|
+
@abstractmethod
|
|
499
|
+
async def _do_generate_async(
|
|
500
|
+
self,
|
|
501
|
+
data: ReportData,
|
|
502
|
+
config: ReporterConfig,
|
|
503
|
+
) -> ReportOutput:
|
|
504
|
+
"""Perform the actual async report generation."""
|
|
505
|
+
...
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# =============================================================================
|
|
509
|
+
# Reporter Registry
|
|
510
|
+
# =============================================================================
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class ReporterRegistry:
|
|
514
|
+
"""Registry for reporter types.
|
|
515
|
+
|
|
516
|
+
Manages reporter registration and access.
|
|
517
|
+
|
|
518
|
+
Example:
|
|
519
|
+
registry = ReporterRegistry()
|
|
520
|
+
registry.register("html", HTMLReporter())
|
|
521
|
+
registry.register("pdf", PDFReporter())
|
|
522
|
+
|
|
523
|
+
reporter = registry.get("html")
|
|
524
|
+
output = reporter.generate(data, config)
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
def __init__(self) -> None:
|
|
528
|
+
"""Initialize registry."""
|
|
529
|
+
self._reporters: dict[str, BaseReporter | AsyncBaseReporter] = {}
|
|
530
|
+
self._factories: dict[str, Callable[..., BaseReporter | AsyncBaseReporter]] = {}
|
|
531
|
+
|
|
532
|
+
def register(
|
|
533
|
+
self,
|
|
534
|
+
name: str,
|
|
535
|
+
reporter: BaseReporter | AsyncBaseReporter,
|
|
536
|
+
) -> None:
|
|
537
|
+
"""Register a reporter instance.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
name: Reporter name.
|
|
541
|
+
reporter: Reporter instance.
|
|
542
|
+
"""
|
|
543
|
+
self._reporters[name] = reporter
|
|
544
|
+
|
|
545
|
+
def register_factory(
|
|
546
|
+
self,
|
|
547
|
+
name: str,
|
|
548
|
+
factory: Callable[..., BaseReporter | AsyncBaseReporter],
|
|
549
|
+
) -> None:
|
|
550
|
+
"""Register a reporter factory.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
name: Reporter name.
|
|
554
|
+
factory: Factory function.
|
|
555
|
+
"""
|
|
556
|
+
self._factories[name] = factory
|
|
557
|
+
|
|
558
|
+
def get(self, name: str) -> BaseReporter | AsyncBaseReporter | None:
|
|
559
|
+
"""Get a reporter by name.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
name: Reporter name.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Reporter or None if not found.
|
|
566
|
+
"""
|
|
567
|
+
return self._reporters.get(name)
|
|
568
|
+
|
|
569
|
+
def create(
|
|
570
|
+
self,
|
|
571
|
+
name: str,
|
|
572
|
+
**kwargs: Any,
|
|
573
|
+
) -> BaseReporter | AsyncBaseReporter:
|
|
574
|
+
"""Create a reporter using a factory.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
name: Reporter name.
|
|
578
|
+
**kwargs: Factory arguments.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Reporter instance.
|
|
582
|
+
|
|
583
|
+
Raises:
|
|
584
|
+
KeyError: If factory not found.
|
|
585
|
+
"""
|
|
586
|
+
if name not in self._factories:
|
|
587
|
+
raise KeyError(f"Reporter factory not found: {name}")
|
|
588
|
+
return self._factories[name](**kwargs)
|
|
589
|
+
|
|
590
|
+
def list_reporters(self) -> list[str]:
|
|
591
|
+
"""List all registered reporter names.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
List of reporter names.
|
|
595
|
+
"""
|
|
596
|
+
return list(set(self._reporters.keys()) | set(self._factories.keys()))
|
|
597
|
+
|
|
598
|
+
def has_reporter(self, name: str) -> bool:
|
|
599
|
+
"""Check if a reporter is registered.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
name: Reporter name.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
True if reporter is registered.
|
|
606
|
+
"""
|
|
607
|
+
return name in self._reporters or name in self._factories
|
|
608
|
+
|
|
609
|
+
def get_supported_formats(self) -> dict[str, list[str]]:
|
|
610
|
+
"""Get supported formats for all reporters.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Dictionary mapping reporter names to supported formats.
|
|
614
|
+
"""
|
|
615
|
+
result = {}
|
|
616
|
+
for name, reporter in self._reporters.items():
|
|
617
|
+
result[name] = [f.value for f in reporter.supported_formats]
|
|
618
|
+
return result
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# Global reporter registry
|
|
622
|
+
_reporter_registry: ReporterRegistry | None = None
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def get_reporter_registry() -> ReporterRegistry:
|
|
626
|
+
"""Get the global reporter registry.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Global ReporterRegistry instance.
|
|
630
|
+
"""
|
|
631
|
+
global _reporter_registry
|
|
632
|
+
if _reporter_registry is None:
|
|
633
|
+
_reporter_registry = ReporterRegistry()
|
|
634
|
+
return _reporter_registry
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def register_reporter(name: str) -> Callable[[type], type]:
|
|
638
|
+
"""Decorator to register a reporter class.
|
|
639
|
+
|
|
640
|
+
Example:
|
|
641
|
+
@register_reporter("my_custom")
|
|
642
|
+
class MyCustomReporter(BaseReporter):
|
|
643
|
+
...
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
def decorator(cls: type) -> type:
|
|
647
|
+
get_reporter_registry().register_factory(name, cls)
|
|
648
|
+
return cls
|
|
649
|
+
|
|
650
|
+
return decorator
|