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,745 @@
|
|
|
1
|
+
"""Reporter interface definitions.
|
|
2
|
+
|
|
3
|
+
This module defines the abstract interfaces for the reporter system,
|
|
4
|
+
enabling loose coupling with truthound and other reporting backends.
|
|
5
|
+
|
|
6
|
+
The interface design follows these principles:
|
|
7
|
+
1. Protocol-based typing for flexible duck typing
|
|
8
|
+
2. Backend-agnostic data structures
|
|
9
|
+
3. Adapter pattern for external reporter integration
|
|
10
|
+
4. Factory pattern for reporter instantiation
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
from truthound_dashboard.core.reporters.interfaces import (
|
|
14
|
+
ReporterProtocol,
|
|
15
|
+
ReporterConfig,
|
|
16
|
+
ReportData,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class MyReporter:
|
|
20
|
+
# Duck typing - no inheritance required
|
|
21
|
+
def render(self, data: ReportData, config: ReporterConfig) -> str:
|
|
22
|
+
...
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from enum import Enum
|
|
31
|
+
from typing import (
|
|
32
|
+
TYPE_CHECKING,
|
|
33
|
+
Any,
|
|
34
|
+
Generic,
|
|
35
|
+
Protocol,
|
|
36
|
+
TypeVar,
|
|
37
|
+
runtime_checkable,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReportFormatType(str, Enum):
|
|
45
|
+
"""Supported report output formats.
|
|
46
|
+
|
|
47
|
+
This enum is backend-agnostic and maps to specific implementations.
|
|
48
|
+
Includes both standard formats and CI-specific formats.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Standard formats
|
|
52
|
+
HTML = "html"
|
|
53
|
+
CSV = "csv"
|
|
54
|
+
JSON = "json"
|
|
55
|
+
YAML = "yaml"
|
|
56
|
+
NDJSON = "ndjson"
|
|
57
|
+
CONSOLE = "console"
|
|
58
|
+
TABLE = "table"
|
|
59
|
+
|
|
60
|
+
# CI platform formats (auto-detected or specific)
|
|
61
|
+
CI = "ci" # Auto-detect CI platform
|
|
62
|
+
GITHUB = "github" # GitHub Actions
|
|
63
|
+
GITLAB = "gitlab" # GitLab CI
|
|
64
|
+
JENKINS = "jenkins" # Jenkins
|
|
65
|
+
AZURE = "azure" # Azure DevOps
|
|
66
|
+
CIRCLECI = "circleci" # CircleCI
|
|
67
|
+
BITBUCKET = "bitbucket" # Bitbucket Pipelines
|
|
68
|
+
TRAVIS = "travis" # Travis CI
|
|
69
|
+
TEAMCITY = "teamcity" # TeamCity
|
|
70
|
+
BUILDKITE = "buildkite" # Buildkite
|
|
71
|
+
DRONE = "drone" # Drone CI
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_string(cls, value: str) -> ReportFormatType:
|
|
75
|
+
"""Parse format from string (case-insensitive)."""
|
|
76
|
+
value_lower = value.lower()
|
|
77
|
+
for fmt in cls:
|
|
78
|
+
if fmt.value == value_lower:
|
|
79
|
+
return fmt
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Unknown report format: {value}. "
|
|
82
|
+
f"Supported formats: {[f.value for f in cls]}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def is_ci_format(cls, format: "ReportFormatType") -> bool:
|
|
87
|
+
"""Check if this is a CI-specific format."""
|
|
88
|
+
return format in {
|
|
89
|
+
cls.CI,
|
|
90
|
+
cls.GITHUB,
|
|
91
|
+
cls.GITLAB,
|
|
92
|
+
cls.JENKINS,
|
|
93
|
+
cls.AZURE,
|
|
94
|
+
cls.CIRCLECI,
|
|
95
|
+
cls.BITBUCKET,
|
|
96
|
+
cls.TRAVIS,
|
|
97
|
+
cls.TEAMCITY,
|
|
98
|
+
cls.BUILDKITE,
|
|
99
|
+
cls.DRONE,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ReportThemeType(str, Enum):
|
|
104
|
+
"""Report visual themes."""
|
|
105
|
+
|
|
106
|
+
LIGHT = "light"
|
|
107
|
+
DARK = "dark"
|
|
108
|
+
PROFESSIONAL = "professional"
|
|
109
|
+
MINIMAL = "minimal"
|
|
110
|
+
HIGH_CONTRAST = "high_contrast"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class ReporterConfig:
|
|
115
|
+
"""Configuration for report generation.
|
|
116
|
+
|
|
117
|
+
This is a backend-agnostic configuration that can be
|
|
118
|
+
translated to specific backend configurations.
|
|
119
|
+
|
|
120
|
+
Attributes:
|
|
121
|
+
title: Report title.
|
|
122
|
+
theme: Visual theme.
|
|
123
|
+
locale: Language locale code (e.g., 'en', 'ko', 'ja').
|
|
124
|
+
include_samples: Include sample values in output.
|
|
125
|
+
include_statistics: Include statistics section.
|
|
126
|
+
include_metadata: Include report metadata.
|
|
127
|
+
max_sample_values: Maximum sample values to include.
|
|
128
|
+
timestamp_format: Date/time format string.
|
|
129
|
+
custom_options: Backend-specific options.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
title: str = "Validation Report"
|
|
133
|
+
theme: ReportThemeType = ReportThemeType.PROFESSIONAL
|
|
134
|
+
locale: str = "en"
|
|
135
|
+
include_samples: bool = True
|
|
136
|
+
include_statistics: bool = True
|
|
137
|
+
include_metadata: bool = True
|
|
138
|
+
max_sample_values: int = 5
|
|
139
|
+
timestamp_format: str = "%Y-%m-%d %H:%M:%S"
|
|
140
|
+
custom_options: dict[str, Any] = field(default_factory=dict)
|
|
141
|
+
|
|
142
|
+
def with_option(self, key: str, value: Any) -> ReporterConfig:
|
|
143
|
+
"""Return a new config with an additional option."""
|
|
144
|
+
new_options = {**self.custom_options, key: value}
|
|
145
|
+
return ReporterConfig(
|
|
146
|
+
title=self.title,
|
|
147
|
+
theme=self.theme,
|
|
148
|
+
locale=self.locale,
|
|
149
|
+
include_samples=self.include_samples,
|
|
150
|
+
include_statistics=self.include_statistics,
|
|
151
|
+
include_metadata=self.include_metadata,
|
|
152
|
+
max_sample_values=self.max_sample_values,
|
|
153
|
+
timestamp_format=self.timestamp_format,
|
|
154
|
+
custom_options=new_options,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ValidationIssueData:
|
|
160
|
+
"""Backend-agnostic validation issue representation.
|
|
161
|
+
|
|
162
|
+
This data class standardizes issue data from various sources
|
|
163
|
+
(truthound, custom validators, external systems).
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
column: str | None
|
|
167
|
+
issue_type: str
|
|
168
|
+
severity: str # low, medium, high, critical
|
|
169
|
+
message: str
|
|
170
|
+
count: int = 1
|
|
171
|
+
expected: Any = None
|
|
172
|
+
actual: Any = None
|
|
173
|
+
sample_values: list[Any] | None = None
|
|
174
|
+
validator_name: str | None = None
|
|
175
|
+
details: dict[str, Any] | None = None
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict[str, Any]:
|
|
178
|
+
"""Convert to dictionary."""
|
|
179
|
+
result = {
|
|
180
|
+
"column": self.column,
|
|
181
|
+
"issue_type": self.issue_type,
|
|
182
|
+
"severity": self.severity,
|
|
183
|
+
"message": self.message,
|
|
184
|
+
"count": self.count,
|
|
185
|
+
}
|
|
186
|
+
if self.expected is not None:
|
|
187
|
+
result["expected"] = self.expected
|
|
188
|
+
if self.actual is not None:
|
|
189
|
+
result["actual"] = self.actual
|
|
190
|
+
if self.sample_values:
|
|
191
|
+
result["sample_values"] = self.sample_values
|
|
192
|
+
if self.validator_name:
|
|
193
|
+
result["validator_name"] = self.validator_name
|
|
194
|
+
if self.details:
|
|
195
|
+
result["details"] = self.details
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass
|
|
200
|
+
class ValidationSummary:
|
|
201
|
+
"""Summary statistics for validation results."""
|
|
202
|
+
|
|
203
|
+
total_issues: int = 0
|
|
204
|
+
critical_issues: int = 0
|
|
205
|
+
high_issues: int = 0
|
|
206
|
+
medium_issues: int = 0
|
|
207
|
+
low_issues: int = 0
|
|
208
|
+
passed: bool = True
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def has_critical(self) -> bool:
|
|
212
|
+
return self.critical_issues > 0
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def has_high(self) -> bool:
|
|
216
|
+
return self.high_issues > 0
|
|
217
|
+
|
|
218
|
+
def to_dict(self) -> dict[str, Any]:
|
|
219
|
+
"""Convert to dictionary."""
|
|
220
|
+
return {
|
|
221
|
+
"total_issues": self.total_issues,
|
|
222
|
+
"critical_issues": self.critical_issues,
|
|
223
|
+
"high_issues": self.high_issues,
|
|
224
|
+
"medium_issues": self.medium_issues,
|
|
225
|
+
"low_issues": self.low_issues,
|
|
226
|
+
"passed": self.passed,
|
|
227
|
+
"has_critical": self.has_critical,
|
|
228
|
+
"has_high": self.has_high,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class DataStatistics:
|
|
234
|
+
"""Data statistics for reports."""
|
|
235
|
+
|
|
236
|
+
row_count: int | None = None
|
|
237
|
+
column_count: int | None = None
|
|
238
|
+
duration_ms: int | None = None
|
|
239
|
+
started_at: datetime | None = None
|
|
240
|
+
completed_at: datetime | None = None
|
|
241
|
+
|
|
242
|
+
def to_dict(self) -> dict[str, Any]:
|
|
243
|
+
"""Convert to dictionary."""
|
|
244
|
+
return {
|
|
245
|
+
"row_count": self.row_count,
|
|
246
|
+
"column_count": self.column_count,
|
|
247
|
+
"duration_ms": self.duration_ms,
|
|
248
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
249
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class ReportData:
|
|
255
|
+
"""Backend-agnostic data container for report generation.
|
|
256
|
+
|
|
257
|
+
This data class serves as the standardized input for all reporters,
|
|
258
|
+
regardless of the original data source (truthound, database, etc.).
|
|
259
|
+
|
|
260
|
+
Attributes:
|
|
261
|
+
validation_id: Unique identifier for the validation run.
|
|
262
|
+
source_id: Data source identifier.
|
|
263
|
+
source_name: Human-readable source name.
|
|
264
|
+
issues: List of validation issues.
|
|
265
|
+
summary: Validation summary statistics.
|
|
266
|
+
statistics: Data statistics.
|
|
267
|
+
status: Validation status string.
|
|
268
|
+
error_message: Error message if validation failed.
|
|
269
|
+
metadata: Additional metadata.
|
|
270
|
+
raw_data: Original raw data (for backends that need it).
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
validation_id: str
|
|
274
|
+
source_id: str
|
|
275
|
+
source_name: str | None = None
|
|
276
|
+
issues: list[ValidationIssueData] = field(default_factory=list)
|
|
277
|
+
summary: ValidationSummary = field(default_factory=ValidationSummary)
|
|
278
|
+
statistics: DataStatistics = field(default_factory=DataStatistics)
|
|
279
|
+
status: str = "completed"
|
|
280
|
+
error_message: str | None = None
|
|
281
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
282
|
+
raw_data: Any = None # Original truthound Report/ValidationResult if available
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def from_validation_model(cls, validation: Any) -> ReportData:
|
|
286
|
+
"""Create ReportData from a Validation database model.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
validation: Validation model from database.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
ReportData instance.
|
|
293
|
+
"""
|
|
294
|
+
# Extract issues from result_json
|
|
295
|
+
issues = []
|
|
296
|
+
if validation.result_json and "issues" in validation.result_json:
|
|
297
|
+
for issue_dict in validation.result_json["issues"]:
|
|
298
|
+
issues.append(
|
|
299
|
+
ValidationIssueData(
|
|
300
|
+
column=issue_dict.get("column"),
|
|
301
|
+
issue_type=issue_dict.get("issue_type", "unknown"),
|
|
302
|
+
severity=issue_dict.get("severity", "medium"),
|
|
303
|
+
message=issue_dict.get("message", ""),
|
|
304
|
+
count=issue_dict.get("count", 1),
|
|
305
|
+
expected=issue_dict.get("expected"),
|
|
306
|
+
actual=issue_dict.get("actual"),
|
|
307
|
+
sample_values=issue_dict.get("sample_values"),
|
|
308
|
+
validator_name=issue_dict.get("validator_name"),
|
|
309
|
+
details=issue_dict.get("details"),
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Build summary
|
|
314
|
+
summary = ValidationSummary(
|
|
315
|
+
total_issues=validation.total_issues or 0,
|
|
316
|
+
critical_issues=validation.critical_issues or 0,
|
|
317
|
+
high_issues=validation.high_issues or 0,
|
|
318
|
+
medium_issues=validation.medium_issues or 0,
|
|
319
|
+
low_issues=validation.low_issues or 0,
|
|
320
|
+
passed=validation.passed if validation.passed is not None else True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Build statistics
|
|
324
|
+
statistics = DataStatistics(
|
|
325
|
+
row_count=validation.row_count,
|
|
326
|
+
column_count=validation.column_count,
|
|
327
|
+
duration_ms=validation.duration_ms,
|
|
328
|
+
started_at=validation.started_at,
|
|
329
|
+
completed_at=validation.completed_at,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Get source name
|
|
333
|
+
source_name = None
|
|
334
|
+
if hasattr(validation, "source") and validation.source:
|
|
335
|
+
source_name = validation.source.name
|
|
336
|
+
|
|
337
|
+
return cls(
|
|
338
|
+
validation_id=str(validation.id),
|
|
339
|
+
source_id=str(validation.source_id),
|
|
340
|
+
source_name=source_name,
|
|
341
|
+
issues=issues,
|
|
342
|
+
summary=summary,
|
|
343
|
+
statistics=statistics,
|
|
344
|
+
status=validation.status or "completed",
|
|
345
|
+
error_message=validation.error_message,
|
|
346
|
+
metadata={
|
|
347
|
+
"created_at": validation.created_at.isoformat()
|
|
348
|
+
if validation.created_at
|
|
349
|
+
else None,
|
|
350
|
+
},
|
|
351
|
+
raw_data=validation,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def from_check_result(
|
|
356
|
+
cls,
|
|
357
|
+
check_result: Any,
|
|
358
|
+
source_id: str | None = None,
|
|
359
|
+
) -> ReportData:
|
|
360
|
+
"""Create ReportData from a TruthoundAdapter CheckResult.
|
|
361
|
+
|
|
362
|
+
This enables direct report generation from validation results
|
|
363
|
+
without storing them in the database first.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
check_result: CheckResult from TruthoundAdapter.
|
|
367
|
+
source_id: Optional source identifier.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
ReportData instance.
|
|
371
|
+
"""
|
|
372
|
+
# Convert issues
|
|
373
|
+
issues = []
|
|
374
|
+
for issue_dict in check_result.issues:
|
|
375
|
+
issues.append(
|
|
376
|
+
ValidationIssueData(
|
|
377
|
+
column=issue_dict.get("column"),
|
|
378
|
+
issue_type=issue_dict.get("issue_type", "unknown"),
|
|
379
|
+
severity=issue_dict.get("severity", "medium"),
|
|
380
|
+
message=issue_dict.get("message", ""),
|
|
381
|
+
count=issue_dict.get("count", 1),
|
|
382
|
+
expected=issue_dict.get("expected"),
|
|
383
|
+
actual=issue_dict.get("actual"),
|
|
384
|
+
sample_values=issue_dict.get("sample_values"),
|
|
385
|
+
validator_name=issue_dict.get("validator_name"),
|
|
386
|
+
details=issue_dict.get("details"),
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Build summary
|
|
391
|
+
summary = ValidationSummary(
|
|
392
|
+
total_issues=check_result.total_issues,
|
|
393
|
+
critical_issues=check_result.critical_issues,
|
|
394
|
+
high_issues=check_result.high_issues,
|
|
395
|
+
medium_issues=check_result.medium_issues,
|
|
396
|
+
low_issues=check_result.low_issues,
|
|
397
|
+
passed=check_result.passed,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Build statistics
|
|
401
|
+
run_time = check_result.run_time
|
|
402
|
+
statistics = DataStatistics(
|
|
403
|
+
row_count=check_result.row_count,
|
|
404
|
+
column_count=check_result.column_count,
|
|
405
|
+
started_at=run_time if run_time else None,
|
|
406
|
+
completed_at=datetime.now(),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Use run_id if available, otherwise generate one
|
|
410
|
+
validation_id = check_result.run_id or f"check-{id(check_result)}"
|
|
411
|
+
|
|
412
|
+
return cls(
|
|
413
|
+
validation_id=validation_id,
|
|
414
|
+
source_id=source_id or check_result.source,
|
|
415
|
+
source_name=check_result.source,
|
|
416
|
+
issues=issues,
|
|
417
|
+
summary=summary,
|
|
418
|
+
statistics=statistics,
|
|
419
|
+
status="passed" if check_result.passed else "failed",
|
|
420
|
+
raw_data=check_result._raw_result if hasattr(check_result, "_raw_result") else None,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def to_dict(self) -> dict[str, Any]:
|
|
424
|
+
"""Convert to dictionary for serialization."""
|
|
425
|
+
return {
|
|
426
|
+
"validation_id": self.validation_id,
|
|
427
|
+
"source_id": self.source_id,
|
|
428
|
+
"source_name": self.source_name,
|
|
429
|
+
"issues": [issue.to_dict() for issue in self.issues],
|
|
430
|
+
"summary": self.summary.to_dict(),
|
|
431
|
+
"statistics": self.statistics.to_dict(),
|
|
432
|
+
"status": self.status,
|
|
433
|
+
"error_message": self.error_message,
|
|
434
|
+
"metadata": self.metadata,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass
|
|
439
|
+
class ReportOutput:
|
|
440
|
+
"""Output from report generation.
|
|
441
|
+
|
|
442
|
+
Attributes:
|
|
443
|
+
content: Generated report content (string or bytes).
|
|
444
|
+
content_type: MIME type of the content.
|
|
445
|
+
filename: Suggested filename for download.
|
|
446
|
+
format: Report format that was used.
|
|
447
|
+
size_bytes: Size of content in bytes.
|
|
448
|
+
generation_time_ms: Time taken to generate in milliseconds.
|
|
449
|
+
metadata: Additional metadata about the report.
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
content: str | bytes
|
|
453
|
+
content_type: str
|
|
454
|
+
filename: str
|
|
455
|
+
format: ReportFormatType
|
|
456
|
+
size_bytes: int = 0
|
|
457
|
+
generation_time_ms: int = 0
|
|
458
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
459
|
+
|
|
460
|
+
def __post_init__(self) -> None:
|
|
461
|
+
"""Calculate size if not set."""
|
|
462
|
+
if self.size_bytes == 0:
|
|
463
|
+
if isinstance(self.content, str):
|
|
464
|
+
self.size_bytes = len(self.content.encode("utf-8"))
|
|
465
|
+
else:
|
|
466
|
+
self.size_bytes = len(self.content)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# Type variable for config
|
|
470
|
+
ConfigT = TypeVar("ConfigT", bound=ReporterConfig)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@runtime_checkable
|
|
474
|
+
class ReporterProtocol(Protocol):
|
|
475
|
+
"""Protocol for reporter implementations.
|
|
476
|
+
|
|
477
|
+
This protocol enables duck typing for reporters from any source.
|
|
478
|
+
Implementations don't need to inherit from any base class.
|
|
479
|
+
|
|
480
|
+
Example:
|
|
481
|
+
class MyReporter:
|
|
482
|
+
@property
|
|
483
|
+
def format(self) -> ReportFormatType:
|
|
484
|
+
return ReportFormatType.HTML
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def content_type(self) -> str:
|
|
488
|
+
return "text/html"
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def file_extension(self) -> str:
|
|
492
|
+
return ".html"
|
|
493
|
+
|
|
494
|
+
async def generate(
|
|
495
|
+
self,
|
|
496
|
+
data: ReportData,
|
|
497
|
+
config: ReporterConfig | None = None,
|
|
498
|
+
) -> ReportOutput:
|
|
499
|
+
...
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def format(self) -> ReportFormatType:
|
|
504
|
+
"""Get the report format this reporter produces."""
|
|
505
|
+
...
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def content_type(self) -> str:
|
|
509
|
+
"""Get the MIME content type for this format."""
|
|
510
|
+
...
|
|
511
|
+
|
|
512
|
+
@property
|
|
513
|
+
def file_extension(self) -> str:
|
|
514
|
+
"""Get the file extension for this format."""
|
|
515
|
+
...
|
|
516
|
+
|
|
517
|
+
async def generate(
|
|
518
|
+
self,
|
|
519
|
+
data: ReportData,
|
|
520
|
+
config: ReporterConfig | None = None,
|
|
521
|
+
) -> ReportOutput:
|
|
522
|
+
"""Generate a report.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
data: Standardized report data.
|
|
526
|
+
config: Optional configuration.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
ReportOutput with generated content.
|
|
530
|
+
"""
|
|
531
|
+
...
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class BaseReporter(ABC, Generic[ConfigT]):
|
|
535
|
+
"""Abstract base class for reporter implementations.
|
|
536
|
+
|
|
537
|
+
This provides a common base for dashboard-specific reporters
|
|
538
|
+
while maintaining compatibility with the ReporterProtocol.
|
|
539
|
+
|
|
540
|
+
Subclasses should implement:
|
|
541
|
+
- format (property)
|
|
542
|
+
- content_type (property)
|
|
543
|
+
- file_extension (property)
|
|
544
|
+
- _render_content (method)
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
class MyHTMLReporter(BaseReporter[ReporterConfig]):
|
|
548
|
+
@property
|
|
549
|
+
def format(self) -> ReportFormatType:
|
|
550
|
+
return ReportFormatType.HTML
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def content_type(self) -> str:
|
|
554
|
+
return "text/html"
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def file_extension(self) -> str:
|
|
558
|
+
return ".html"
|
|
559
|
+
|
|
560
|
+
async def _render_content(
|
|
561
|
+
self,
|
|
562
|
+
data: ReportData,
|
|
563
|
+
config: ConfigT,
|
|
564
|
+
) -> str:
|
|
565
|
+
return f"<html>...</html>"
|
|
566
|
+
"""
|
|
567
|
+
|
|
568
|
+
def __init__(self, default_config: ConfigT | None = None) -> None:
|
|
569
|
+
"""Initialize reporter.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
default_config: Default configuration for this reporter.
|
|
573
|
+
"""
|
|
574
|
+
self._default_config = default_config or self._create_default_config()
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
@abstractmethod
|
|
578
|
+
def format(self) -> ReportFormatType:
|
|
579
|
+
"""Get the report format this reporter produces."""
|
|
580
|
+
...
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
@abstractmethod
|
|
584
|
+
def content_type(self) -> str:
|
|
585
|
+
"""Get the MIME content type for this format."""
|
|
586
|
+
...
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
@abstractmethod
|
|
590
|
+
def file_extension(self) -> str:
|
|
591
|
+
"""Get the file extension for this format."""
|
|
592
|
+
...
|
|
593
|
+
|
|
594
|
+
def _create_default_config(self) -> ConfigT:
|
|
595
|
+
"""Create default configuration.
|
|
596
|
+
|
|
597
|
+
Override in subclasses to provide format-specific defaults.
|
|
598
|
+
"""
|
|
599
|
+
return ReporterConfig() # type: ignore
|
|
600
|
+
|
|
601
|
+
async def generate(
|
|
602
|
+
self,
|
|
603
|
+
data: ReportData,
|
|
604
|
+
config: ReporterConfig | None = None,
|
|
605
|
+
) -> ReportOutput:
|
|
606
|
+
"""Generate a report.
|
|
607
|
+
|
|
608
|
+
This is the main entry point (Template Method pattern).
|
|
609
|
+
Subclasses implement _render_content() for format-specific logic.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
data: Standardized report data.
|
|
613
|
+
config: Optional configuration (uses default if not provided).
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
ReportOutput with generated content.
|
|
617
|
+
"""
|
|
618
|
+
import time
|
|
619
|
+
|
|
620
|
+
start_time = time.time()
|
|
621
|
+
|
|
622
|
+
# Merge with default config
|
|
623
|
+
effective_config = config or self._default_config
|
|
624
|
+
|
|
625
|
+
# Generate filename
|
|
626
|
+
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
|
627
|
+
filename = f"validation_report_{timestamp}{self.file_extension}"
|
|
628
|
+
|
|
629
|
+
# Render content
|
|
630
|
+
content = await self._render_content(data, effective_config)
|
|
631
|
+
|
|
632
|
+
generation_time_ms = int((time.time() - start_time) * 1000)
|
|
633
|
+
|
|
634
|
+
return ReportOutput(
|
|
635
|
+
content=content,
|
|
636
|
+
content_type=self.content_type,
|
|
637
|
+
filename=filename,
|
|
638
|
+
format=self.format,
|
|
639
|
+
generation_time_ms=generation_time_ms,
|
|
640
|
+
metadata={
|
|
641
|
+
"title": effective_config.title,
|
|
642
|
+
"theme": effective_config.theme.value,
|
|
643
|
+
"locale": effective_config.locale,
|
|
644
|
+
"validation_id": data.validation_id,
|
|
645
|
+
"source_id": data.source_id,
|
|
646
|
+
"source_name": data.source_name,
|
|
647
|
+
},
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
@abstractmethod
|
|
651
|
+
async def _render_content(
|
|
652
|
+
self,
|
|
653
|
+
data: ReportData,
|
|
654
|
+
config: ConfigT,
|
|
655
|
+
) -> str | bytes:
|
|
656
|
+
"""Render the report content.
|
|
657
|
+
|
|
658
|
+
Subclasses implement this to produce format-specific output.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
data: Report data.
|
|
662
|
+
config: Reporter configuration.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
Rendered report content.
|
|
666
|
+
"""
|
|
667
|
+
...
|
|
668
|
+
|
|
669
|
+
def _get_severity_color(self, severity: str, theme: ReportThemeType) -> str:
|
|
670
|
+
"""Get color for severity level based on theme.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
severity: Severity level (critical, high, medium, low).
|
|
674
|
+
theme: Current theme.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
CSS color value.
|
|
678
|
+
"""
|
|
679
|
+
if theme == ReportThemeType.DARK:
|
|
680
|
+
colors = {
|
|
681
|
+
"critical": "#ef4444",
|
|
682
|
+
"high": "#f97316",
|
|
683
|
+
"medium": "#eab308",
|
|
684
|
+
"low": "#3b82f6",
|
|
685
|
+
}
|
|
686
|
+
else:
|
|
687
|
+
colors = {
|
|
688
|
+
"critical": "#dc2626",
|
|
689
|
+
"high": "#ea580c",
|
|
690
|
+
"medium": "#ca8a04",
|
|
691
|
+
"low": "#2563eb",
|
|
692
|
+
}
|
|
693
|
+
return colors.get(severity.lower(), "#6b7280")
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@runtime_checkable
|
|
697
|
+
class ReporterAdapterProtocol(Protocol):
|
|
698
|
+
"""Protocol for reporter adapters.
|
|
699
|
+
|
|
700
|
+
Adapters wrap external reporter implementations (e.g., truthound)
|
|
701
|
+
and translate them to the ReporterProtocol interface.
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
def adapt(self, external_reporter: Any) -> ReporterProtocol:
|
|
705
|
+
"""Adapt an external reporter to the ReporterProtocol.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
external_reporter: External reporter instance.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Reporter that implements ReporterProtocol.
|
|
712
|
+
"""
|
|
713
|
+
...
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@runtime_checkable
|
|
717
|
+
class ReporterFactoryProtocol(Protocol):
|
|
718
|
+
"""Protocol for reporter factories.
|
|
719
|
+
|
|
720
|
+
Factories create reporter instances based on format and configuration.
|
|
721
|
+
"""
|
|
722
|
+
|
|
723
|
+
def get_reporter(
|
|
724
|
+
self,
|
|
725
|
+
format: ReportFormatType | str,
|
|
726
|
+
config: ReporterConfig | None = None,
|
|
727
|
+
) -> ReporterProtocol:
|
|
728
|
+
"""Get a reporter for the specified format.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
format: Report format.
|
|
732
|
+
config: Optional configuration.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Reporter instance.
|
|
736
|
+
"""
|
|
737
|
+
...
|
|
738
|
+
|
|
739
|
+
def get_available_formats(self) -> list[str]:
|
|
740
|
+
"""Get list of available format names."""
|
|
741
|
+
...
|
|
742
|
+
|
|
743
|
+
def is_format_available(self, format: ReportFormatType | str) -> bool:
|
|
744
|
+
"""Check if a format is available."""
|
|
745
|
+
...
|