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
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
"""JUnit XML report generator.
|
|
2
|
-
|
|
3
|
-
Generates JUnit-compatible XML reports for CI/CD integration.
|
|
4
|
-
Output can be consumed by Jenkins, GitHub Actions, GitLab CI, etc.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import xml.etree.ElementTree as ET
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
12
|
-
|
|
13
|
-
from .base import Reporter, ReportFormat, ReportMetadata, ReportTheme
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from truthound_dashboard.db.models import Validation
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class JUnitReporter(Reporter):
|
|
20
|
-
"""JUnit XML report generator for CI/CD integration.
|
|
21
|
-
|
|
22
|
-
Produces JUnit-compatible XML that can be consumed by:
|
|
23
|
-
- Jenkins JUnit Plugin
|
|
24
|
-
- GitHub Actions
|
|
25
|
-
- GitLab CI
|
|
26
|
-
- Azure DevOps
|
|
27
|
-
- CircleCI
|
|
28
|
-
- Any tool supporting JUnit XML format
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
def format(self) -> ReportFormat:
|
|
33
|
-
return ReportFormat.JUNIT
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def content_type(self) -> str:
|
|
37
|
-
return "application/xml; charset=utf-8"
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def file_extension(self) -> str:
|
|
41
|
-
return ".xml"
|
|
42
|
-
|
|
43
|
-
async def _render_content(
|
|
44
|
-
self,
|
|
45
|
-
validation: Validation,
|
|
46
|
-
metadata: ReportMetadata,
|
|
47
|
-
include_samples: bool,
|
|
48
|
-
include_statistics: bool,
|
|
49
|
-
) -> str:
|
|
50
|
-
"""Render JUnit XML report content."""
|
|
51
|
-
issues = self._extract_issues(validation)
|
|
52
|
-
|
|
53
|
-
# Create root element (testsuites)
|
|
54
|
-
testsuites = ET.Element("testsuites")
|
|
55
|
-
testsuites.set("name", "Truthound Validation")
|
|
56
|
-
testsuites.set("tests", str(len(issues) + 1)) # +1 for overall test
|
|
57
|
-
testsuites.set("failures", str(validation.total_issues or 0))
|
|
58
|
-
testsuites.set("errors", str(validation.critical_issues or 0))
|
|
59
|
-
testsuites.set("time", str((validation.duration_ms or 0) / 1000))
|
|
60
|
-
|
|
61
|
-
# Create testsuite for this validation
|
|
62
|
-
testsuite = ET.SubElement(testsuites, "testsuite")
|
|
63
|
-
testsuite.set("name", f"Validation: {metadata.source_name or validation.source_id}")
|
|
64
|
-
testsuite.set("tests", str(len(issues) + 1))
|
|
65
|
-
testsuite.set("failures", str(validation.total_issues or 0))
|
|
66
|
-
testsuite.set("errors", str(validation.critical_issues or 0))
|
|
67
|
-
testsuite.set("time", str((validation.duration_ms or 0) / 1000))
|
|
68
|
-
testsuite.set("timestamp", validation.created_at.isoformat() if validation.created_at else datetime.utcnow().isoformat())
|
|
69
|
-
|
|
70
|
-
# Add properties
|
|
71
|
-
properties = ET.SubElement(testsuite, "properties")
|
|
72
|
-
self._add_property(properties, "source_id", validation.source_id)
|
|
73
|
-
self._add_property(properties, "validation_id", validation.id)
|
|
74
|
-
self._add_property(properties, "row_count", str(validation.row_count or 0))
|
|
75
|
-
self._add_property(properties, "column_count", str(validation.column_count or 0))
|
|
76
|
-
self._add_property(properties, "status", validation.status)
|
|
77
|
-
self._add_property(properties, "passed", str(validation.passed).lower())
|
|
78
|
-
|
|
79
|
-
# Add overall validation test case
|
|
80
|
-
overall_test = ET.SubElement(testsuite, "testcase")
|
|
81
|
-
overall_test.set("name", "Overall Validation")
|
|
82
|
-
overall_test.set("classname", f"truthound.{metadata.source_name or validation.source_id}")
|
|
83
|
-
overall_test.set("time", str((validation.duration_ms or 0) / 1000))
|
|
84
|
-
|
|
85
|
-
if not validation.passed:
|
|
86
|
-
failure = ET.SubElement(overall_test, "failure")
|
|
87
|
-
failure.set("message", f"Validation failed with {validation.total_issues} issues")
|
|
88
|
-
failure.set("type", "ValidationFailure")
|
|
89
|
-
failure.text = self._generate_failure_details(validation, issues)
|
|
90
|
-
|
|
91
|
-
# Add individual test cases for each issue type
|
|
92
|
-
issue_groups = self._group_issues_by_type(issues)
|
|
93
|
-
for issue_type, group_issues in issue_groups.items():
|
|
94
|
-
testcase = ET.SubElement(testsuite, "testcase")
|
|
95
|
-
testcase.set("name", f"Check: {issue_type}")
|
|
96
|
-
testcase.set("classname", f"truthound.validators.{issue_type}")
|
|
97
|
-
testcase.set("time", "0")
|
|
98
|
-
|
|
99
|
-
if group_issues:
|
|
100
|
-
# Determine severity for failure type
|
|
101
|
-
max_severity = max(
|
|
102
|
-
(self._severity_order(i.get("severity", "low")) for i in group_issues),
|
|
103
|
-
default=0
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if max_severity >= 3: # critical
|
|
107
|
-
error = ET.SubElement(testcase, "error")
|
|
108
|
-
error.set("message", f"Critical: {len(group_issues)} issues found")
|
|
109
|
-
error.set("type", "CriticalValidationError")
|
|
110
|
-
error.text = self._format_issues_detail(group_issues, include_samples)
|
|
111
|
-
else:
|
|
112
|
-
failure = ET.SubElement(testcase, "failure")
|
|
113
|
-
failure.set("message", f"{len(group_issues)} issues found")
|
|
114
|
-
failure.set("type", "ValidationFailure")
|
|
115
|
-
failure.text = self._format_issues_detail(group_issues, include_samples)
|
|
116
|
-
|
|
117
|
-
# Add system-out with summary
|
|
118
|
-
system_out = ET.SubElement(testsuite, "system-out")
|
|
119
|
-
system_out.text = self._generate_summary(validation, metadata)
|
|
120
|
-
|
|
121
|
-
# Convert to string with proper formatting
|
|
122
|
-
return self._prettify_xml(testsuites)
|
|
123
|
-
|
|
124
|
-
def _add_property(self, parent: ET.Element, name: str, value: str) -> None:
|
|
125
|
-
"""Add a property element."""
|
|
126
|
-
prop = ET.SubElement(parent, "property")
|
|
127
|
-
prop.set("name", name)
|
|
128
|
-
prop.set("value", value)
|
|
129
|
-
|
|
130
|
-
def _severity_order(self, severity: str) -> int:
|
|
131
|
-
"""Get numeric order for severity."""
|
|
132
|
-
order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
|
133
|
-
return order.get(severity.lower(), 0)
|
|
134
|
-
|
|
135
|
-
def _group_issues_by_type(self, issues: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
136
|
-
"""Group issues by their type."""
|
|
137
|
-
groups: dict[str, list[dict[str, Any]]] = {}
|
|
138
|
-
for issue in issues:
|
|
139
|
-
issue_type = issue.get("issue_type", "unknown")
|
|
140
|
-
if issue_type not in groups:
|
|
141
|
-
groups[issue_type] = []
|
|
142
|
-
groups[issue_type].append(issue)
|
|
143
|
-
return groups
|
|
144
|
-
|
|
145
|
-
def _generate_failure_details(
|
|
146
|
-
self,
|
|
147
|
-
validation: Validation,
|
|
148
|
-
issues: list[dict[str, Any]],
|
|
149
|
-
) -> str:
|
|
150
|
-
"""Generate detailed failure message."""
|
|
151
|
-
lines = [
|
|
152
|
-
f"Validation Status: {validation.status}",
|
|
153
|
-
f"Total Issues: {validation.total_issues}",
|
|
154
|
-
f"Critical: {validation.critical_issues}",
|
|
155
|
-
f"High: {validation.high_issues}",
|
|
156
|
-
f"Medium: {validation.medium_issues}",
|
|
157
|
-
f"Low: {validation.low_issues}",
|
|
158
|
-
"",
|
|
159
|
-
"Issue Summary:",
|
|
160
|
-
]
|
|
161
|
-
|
|
162
|
-
for issue in issues[:10]: # Limit to first 10
|
|
163
|
-
lines.append(
|
|
164
|
-
f" - [{issue.get('severity', 'unknown').upper()}] "
|
|
165
|
-
f"{issue.get('column', 'N/A')}: {issue.get('issue_type', 'unknown')} "
|
|
166
|
-
f"({issue.get('count', 0)} occurrences)"
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
if len(issues) > 10:
|
|
170
|
-
lines.append(f" ... and {len(issues) - 10} more issues")
|
|
171
|
-
|
|
172
|
-
return "\n".join(lines)
|
|
173
|
-
|
|
174
|
-
def _format_issues_detail(
|
|
175
|
-
self,
|
|
176
|
-
issues: list[dict[str, Any]],
|
|
177
|
-
include_samples: bool,
|
|
178
|
-
) -> str:
|
|
179
|
-
"""Format issues for display in XML."""
|
|
180
|
-
lines = []
|
|
181
|
-
for issue in issues:
|
|
182
|
-
line = (
|
|
183
|
-
f"Column: {issue.get('column', 'N/A')}, "
|
|
184
|
-
f"Count: {issue.get('count', 0)}"
|
|
185
|
-
)
|
|
186
|
-
if issue.get("details"):
|
|
187
|
-
line += f", Details: {issue.get('details')}"
|
|
188
|
-
|
|
189
|
-
if include_samples and issue.get("sample_values"):
|
|
190
|
-
samples = [str(v)[:30] for v in issue["sample_values"][:3]]
|
|
191
|
-
line += f", Samples: [{', '.join(samples)}]"
|
|
192
|
-
|
|
193
|
-
lines.append(line)
|
|
194
|
-
|
|
195
|
-
return "\n".join(lines)
|
|
196
|
-
|
|
197
|
-
def _generate_summary(
|
|
198
|
-
self,
|
|
199
|
-
validation: Validation,
|
|
200
|
-
metadata: ReportMetadata,
|
|
201
|
-
) -> str:
|
|
202
|
-
"""Generate summary for system-out."""
|
|
203
|
-
return f"""
|
|
204
|
-
Truthound Validation Report
|
|
205
|
-
===========================
|
|
206
|
-
Source: {metadata.source_name or validation.source_id}
|
|
207
|
-
Validation ID: {validation.id}
|
|
208
|
-
Generated: {metadata.generated_at.isoformat()}
|
|
209
|
-
|
|
210
|
-
Data Statistics:
|
|
211
|
-
Rows: {validation.row_count or 'N/A'}
|
|
212
|
-
Columns: {validation.column_count or 'N/A'}
|
|
213
|
-
|
|
214
|
-
Validation Results:
|
|
215
|
-
Status: {validation.status}
|
|
216
|
-
Passed: {validation.passed}
|
|
217
|
-
Duration: {(validation.duration_ms or 0) / 1000:.2f}s
|
|
218
|
-
|
|
219
|
-
Issue Summary:
|
|
220
|
-
Total: {validation.total_issues or 0}
|
|
221
|
-
Critical: {validation.critical_issues or 0}
|
|
222
|
-
High: {validation.high_issues or 0}
|
|
223
|
-
Medium: {validation.medium_issues or 0}
|
|
224
|
-
Low: {validation.low_issues or 0}
|
|
225
|
-
"""
|
|
226
|
-
|
|
227
|
-
def _prettify_xml(self, elem: ET.Element) -> str:
|
|
228
|
-
"""Return pretty-printed XML string."""
|
|
229
|
-
from xml.dom import minidom
|
|
230
|
-
|
|
231
|
-
rough_string = ET.tostring(elem, encoding="unicode", method="xml")
|
|
232
|
-
reparsed = minidom.parseString(rough_string)
|
|
233
|
-
return reparsed.toprettyxml(indent=" ", encoding=None)
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""Markdown report generator.
|
|
2
|
-
|
|
3
|
-
Generates Markdown reports suitable for documentation, GitHub,
|
|
4
|
-
and other platforms that render Markdown.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from typing import TYPE_CHECKING, Any
|
|
10
|
-
|
|
11
|
-
from .base import Reporter, ReportFormat, ReportMetadata, ReportTheme
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from truthound_dashboard.db.models import Validation
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class MarkdownReporter(Reporter):
|
|
18
|
-
"""Markdown report generator.
|
|
19
|
-
|
|
20
|
-
Produces GitHub-flavored Markdown reports with tables and badges.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(self, flavor: str = "github") -> None:
|
|
24
|
-
"""Initialize Markdown reporter.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
flavor: Markdown flavor (github, standard).
|
|
28
|
-
"""
|
|
29
|
-
self._flavor = flavor
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
def format(self) -> ReportFormat:
|
|
33
|
-
return ReportFormat.MARKDOWN
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def content_type(self) -> str:
|
|
37
|
-
return "text/markdown; charset=utf-8"
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def file_extension(self) -> str:
|
|
41
|
-
return ".md"
|
|
42
|
-
|
|
43
|
-
async def _render_content(
|
|
44
|
-
self,
|
|
45
|
-
validation: Validation,
|
|
46
|
-
metadata: ReportMetadata,
|
|
47
|
-
include_samples: bool,
|
|
48
|
-
include_statistics: bool,
|
|
49
|
-
) -> str:
|
|
50
|
-
"""Render Markdown report content."""
|
|
51
|
-
sections = []
|
|
52
|
-
|
|
53
|
-
# Header
|
|
54
|
-
sections.append(self._render_header(validation, metadata))
|
|
55
|
-
|
|
56
|
-
# Status badge
|
|
57
|
-
sections.append(self._render_status_badge(validation))
|
|
58
|
-
|
|
59
|
-
# Summary
|
|
60
|
-
sections.append(self._render_summary(validation))
|
|
61
|
-
|
|
62
|
-
# Statistics
|
|
63
|
-
if include_statistics:
|
|
64
|
-
sections.append(self._render_statistics(validation))
|
|
65
|
-
|
|
66
|
-
# Issues table
|
|
67
|
-
issues = self._extract_issues(validation)
|
|
68
|
-
sections.append(self._render_issues_table(issues, include_samples))
|
|
69
|
-
|
|
70
|
-
# Footer
|
|
71
|
-
sections.append(self._render_footer(metadata))
|
|
72
|
-
|
|
73
|
-
return "\n\n".join(filter(None, sections))
|
|
74
|
-
|
|
75
|
-
def _render_header(self, validation: Validation, metadata: ReportMetadata) -> str:
|
|
76
|
-
"""Render report header."""
|
|
77
|
-
source_name = metadata.source_name or validation.source_id
|
|
78
|
-
generated = metadata.generated_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
79
|
-
|
|
80
|
-
return f"""# {metadata.title}
|
|
81
|
-
|
|
82
|
-
**Source:** {source_name}
|
|
83
|
-
**Generated:** {generated}
|
|
84
|
-
**Validation ID:** `{validation.id}`"""
|
|
85
|
-
|
|
86
|
-
def _render_status_badge(self, validation: Validation) -> str:
|
|
87
|
-
"""Render status badge."""
|
|
88
|
-
if validation.passed:
|
|
89
|
-
badge = ""
|
|
90
|
-
else:
|
|
91
|
-
badge = ""
|
|
92
|
-
|
|
93
|
-
return badge
|
|
94
|
-
|
|
95
|
-
def _render_summary(self, validation: Validation) -> str:
|
|
96
|
-
"""Render validation summary."""
|
|
97
|
-
return f"""## Summary
|
|
98
|
-
|
|
99
|
-
| Metric | Count |
|
|
100
|
-
|--------|-------|
|
|
101
|
-
| Total Issues | {validation.total_issues or 0} |
|
|
102
|
-
| Critical | {validation.critical_issues or 0} |
|
|
103
|
-
| High | {validation.high_issues or 0} |
|
|
104
|
-
| Medium | {validation.medium_issues or 0} |
|
|
105
|
-
| Low | {validation.low_issues or 0} |"""
|
|
106
|
-
|
|
107
|
-
def _render_statistics(self, validation: Validation) -> str:
|
|
108
|
-
"""Render data statistics."""
|
|
109
|
-
duration = validation.duration_ms
|
|
110
|
-
duration_str = f"{duration / 1000:.2f}s" if duration else "N/A"
|
|
111
|
-
|
|
112
|
-
started = (
|
|
113
|
-
validation.started_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
114
|
-
if validation.started_at
|
|
115
|
-
else "N/A"
|
|
116
|
-
)
|
|
117
|
-
completed = (
|
|
118
|
-
validation.completed_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
119
|
-
if validation.completed_at
|
|
120
|
-
else "N/A"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
row_count = f"{validation.row_count:,}" if validation.row_count else "N/A"
|
|
124
|
-
|
|
125
|
-
return f"""## Statistics
|
|
126
|
-
|
|
127
|
-
| Metric | Value |
|
|
128
|
-
|--------|-------|
|
|
129
|
-
| Row Count | {row_count} |
|
|
130
|
-
| Column Count | {validation.column_count or 'N/A'} |
|
|
131
|
-
| Duration | {duration_str} |
|
|
132
|
-
| Status | {validation.status} |
|
|
133
|
-
| Started At | {started} |
|
|
134
|
-
| Completed At | {completed} |"""
|
|
135
|
-
|
|
136
|
-
def _render_issues_table(
|
|
137
|
-
self, issues: list[dict[str, Any]], include_samples: bool
|
|
138
|
-
) -> str:
|
|
139
|
-
"""Render issues as Markdown table."""
|
|
140
|
-
if not issues:
|
|
141
|
-
return """## Issues
|
|
142
|
-
|
|
143
|
-
No issues found. All validations passed."""
|
|
144
|
-
|
|
145
|
-
# Build table header
|
|
146
|
-
headers = ["Column", "Issue Type", "Severity", "Count", "Details"]
|
|
147
|
-
if include_samples:
|
|
148
|
-
headers.append("Samples")
|
|
149
|
-
|
|
150
|
-
header_row = "| " + " | ".join(headers) + " |"
|
|
151
|
-
separator = "| " + " | ".join(["---"] * len(headers)) + " |"
|
|
152
|
-
|
|
153
|
-
# Build rows
|
|
154
|
-
rows = []
|
|
155
|
-
for issue in issues:
|
|
156
|
-
severity = issue.get("severity", "medium")
|
|
157
|
-
severity_badge = self._get_severity_badge(severity)
|
|
158
|
-
|
|
159
|
-
row = [
|
|
160
|
-
f"`{issue.get('column', 'N/A')}`",
|
|
161
|
-
issue.get("issue_type", "Unknown"),
|
|
162
|
-
severity_badge,
|
|
163
|
-
str(issue.get("count", 0)),
|
|
164
|
-
(issue.get("details", "") or "")[:50],
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
if include_samples:
|
|
168
|
-
samples = issue.get("sample_values", [])
|
|
169
|
-
samples_str = ", ".join(str(v)[:20] for v in samples[:3])
|
|
170
|
-
if samples_str:
|
|
171
|
-
samples_str = f"`{samples_str}`"
|
|
172
|
-
row.append(samples_str or "-")
|
|
173
|
-
|
|
174
|
-
rows.append("| " + " | ".join(row) + " |")
|
|
175
|
-
|
|
176
|
-
return f"""## Issues ({len(issues)})
|
|
177
|
-
|
|
178
|
-
{header_row}
|
|
179
|
-
{separator}
|
|
180
|
-
{chr(10).join(rows)}"""
|
|
181
|
-
|
|
182
|
-
def _get_severity_badge(self, severity: str) -> str:
|
|
183
|
-
"""Get Markdown badge for severity level.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
severity: Severity level.
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
Markdown badge string.
|
|
190
|
-
"""
|
|
191
|
-
colors = {
|
|
192
|
-
"critical": "critical",
|
|
193
|
-
"high": "orange",
|
|
194
|
-
"medium": "yellow",
|
|
195
|
-
"low": "blue",
|
|
196
|
-
}
|
|
197
|
-
color = colors.get(severity.lower(), "lightgrey")
|
|
198
|
-
|
|
199
|
-
if self._flavor == "github":
|
|
200
|
-
return f""
|
|
201
|
-
return f"**{severity.upper()}**"
|
|
202
|
-
|
|
203
|
-
def _render_footer(self, metadata: ReportMetadata) -> str:
|
|
204
|
-
"""Render report footer."""
|
|
205
|
-
return """---
|
|
206
|
-
|
|
207
|
-
*Generated by [Truthound Dashboard](https://github.com/truthound/truthound-dashboard)*"""
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
"""PDF report generator.
|
|
2
|
-
|
|
3
|
-
Generates professional PDF reports using HTML-to-PDF conversion.
|
|
4
|
-
Supports multiple themes and includes all validation details.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
-
|
|
12
|
-
from .base import Reporter, ReportFormat, ReportMetadata, ReportTheme
|
|
13
|
-
from .html_reporter import HTMLReporter
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from truthound_dashboard.db.models import Validation
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
# Check if weasyprint is available
|
|
21
|
-
_WEASYPRINT_AVAILABLE = False
|
|
22
|
-
try:
|
|
23
|
-
import weasyprint # noqa: F401
|
|
24
|
-
_WEASYPRINT_AVAILABLE = True
|
|
25
|
-
except ImportError:
|
|
26
|
-
logger.debug("weasyprint not available, PDF generation will use HTML fallback")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class PDFReporter(Reporter):
|
|
30
|
-
"""PDF report generator using HTML-to-PDF conversion.
|
|
31
|
-
|
|
32
|
-
Uses weasyprint for high-quality PDF generation.
|
|
33
|
-
Falls back to HTML if weasyprint is not installed.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(self) -> None:
|
|
37
|
-
"""Initialize PDF reporter with HTML reporter for content generation."""
|
|
38
|
-
self._html_reporter = HTMLReporter()
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def format(self) -> ReportFormat:
|
|
42
|
-
return ReportFormat.PDF
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def content_type(self) -> str:
|
|
46
|
-
return "application/pdf"
|
|
47
|
-
|
|
48
|
-
@property
|
|
49
|
-
def file_extension(self) -> str:
|
|
50
|
-
return ".pdf"
|
|
51
|
-
|
|
52
|
-
@classmethod
|
|
53
|
-
def is_available(cls) -> bool:
|
|
54
|
-
"""Check if PDF generation is available."""
|
|
55
|
-
return _WEASYPRINT_AVAILABLE
|
|
56
|
-
|
|
57
|
-
async def _render_content(
|
|
58
|
-
self,
|
|
59
|
-
validation: Validation,
|
|
60
|
-
metadata: ReportMetadata,
|
|
61
|
-
include_samples: bool,
|
|
62
|
-
include_statistics: bool,
|
|
63
|
-
) -> bytes:
|
|
64
|
-
"""Render PDF report content.
|
|
65
|
-
|
|
66
|
-
Uses HTML reporter to generate content, then converts to PDF.
|
|
67
|
-
"""
|
|
68
|
-
# Generate HTML content using the HTML reporter
|
|
69
|
-
html_content = await self._html_reporter._render_content(
|
|
70
|
-
validation=validation,
|
|
71
|
-
metadata=metadata,
|
|
72
|
-
include_samples=include_samples,
|
|
73
|
-
include_statistics=include_statistics,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Add PDF-specific styles for better printing
|
|
77
|
-
pdf_styles = self._get_pdf_styles(metadata.theme)
|
|
78
|
-
html_content = html_content.replace(
|
|
79
|
-
"</style>",
|
|
80
|
-
f"{pdf_styles}\n </style>"
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# Convert HTML to PDF
|
|
84
|
-
if _WEASYPRINT_AVAILABLE:
|
|
85
|
-
return self._convert_to_pdf(html_content)
|
|
86
|
-
else:
|
|
87
|
-
# Return HTML as bytes if weasyprint is not available
|
|
88
|
-
logger.warning(
|
|
89
|
-
"weasyprint not installed. Install with: pip install weasyprint"
|
|
90
|
-
)
|
|
91
|
-
return html_content.encode("utf-8")
|
|
92
|
-
|
|
93
|
-
def _get_pdf_styles(self, theme: ReportTheme) -> str:
|
|
94
|
-
"""Get additional CSS styles for PDF output."""
|
|
95
|
-
return """
|
|
96
|
-
/* PDF-specific styles */
|
|
97
|
-
@page {
|
|
98
|
-
size: A4;
|
|
99
|
-
margin: 1.5cm;
|
|
100
|
-
|
|
101
|
-
@top-center {
|
|
102
|
-
content: "Truthound Validation Report";
|
|
103
|
-
font-size: 10pt;
|
|
104
|
-
color: #64748b;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@bottom-right {
|
|
108
|
-
content: counter(page) " / " counter(pages);
|
|
109
|
-
font-size: 10pt;
|
|
110
|
-
color: #64748b;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@page :first {
|
|
115
|
-
@top-center { content: none; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
body {
|
|
119
|
-
font-size: 11pt;
|
|
120
|
-
line-height: 1.5;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.container {
|
|
124
|
-
max-width: 100%;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.header {
|
|
128
|
-
page-break-after: avoid;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.card {
|
|
132
|
-
page-break-inside: avoid;
|
|
133
|
-
margin-bottom: 0.75cm;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
table {
|
|
137
|
-
font-size: 10pt;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
th, td {
|
|
141
|
-
padding: 0.4rem 0.6rem;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.summary-grid {
|
|
145
|
-
grid-template-columns: repeat(5, 1fr);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.summary-item .value {
|
|
149
|
-
font-size: 1.5rem;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.footer {
|
|
153
|
-
page-break-before: avoid;
|
|
154
|
-
font-size: 9pt;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* Remove hover effects for PDF */
|
|
158
|
-
tr:hover {
|
|
159
|
-
background: transparent !important;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/* Ensure badges print with colors */
|
|
163
|
-
.severity-badge,
|
|
164
|
-
.status-badge {
|
|
165
|
-
-webkit-print-color-adjust: exact;
|
|
166
|
-
print-color-adjust: exact;
|
|
167
|
-
}
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
def _convert_to_pdf(self, html_content: str) -> bytes:
|
|
171
|
-
"""Convert HTML content to PDF bytes.
|
|
172
|
-
|
|
173
|
-
Args:
|
|
174
|
-
html_content: HTML string to convert.
|
|
175
|
-
|
|
176
|
-
Returns:
|
|
177
|
-
PDF content as bytes.
|
|
178
|
-
"""
|
|
179
|
-
try:
|
|
180
|
-
import weasyprint
|
|
181
|
-
from weasyprint import CSS
|
|
182
|
-
|
|
183
|
-
# Additional CSS for better PDF rendering
|
|
184
|
-
extra_css = CSS(string="""
|
|
185
|
-
@page { margin: 1.5cm; }
|
|
186
|
-
body { -webkit-print-color-adjust: exact; }
|
|
187
|
-
""")
|
|
188
|
-
|
|
189
|
-
# Create PDF
|
|
190
|
-
html = weasyprint.HTML(string=html_content, base_url=".")
|
|
191
|
-
pdf_bytes = html.write_pdf(stylesheets=[extra_css])
|
|
192
|
-
|
|
193
|
-
return pdf_bytes
|
|
194
|
-
|
|
195
|
-
except Exception as e:
|
|
196
|
-
logger.error(f"Failed to convert HTML to PDF: {e}")
|
|
197
|
-
raise RuntimeError(f"PDF generation failed: {e}") from e
|
|
198
|
-
|
|
199
|
-
def _extract_issues(self, validation: Validation) -> list[dict[str, Any]]:
|
|
200
|
-
"""Extract issues from validation result."""
|
|
201
|
-
return self._html_reporter._extract_issues(validation)
|
|
202
|
-
|
|
203
|
-
def _get_severity_color(self, severity: str, theme: ReportTheme) -> str:
|
|
204
|
-
"""Get color for severity level."""
|
|
205
|
-
return self._html_reporter._get_severity_color(severity, theme)
|
|
206
|
-
|
|
207
|
-
def _get_status_indicator(self, passed: bool | None) -> str:
|
|
208
|
-
"""Get status indicator text."""
|
|
209
|
-
return self._html_reporter._get_status_indicator(passed)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{bi as L,bJ as ln,aH as A,bg as P,bK as gn,bL as dn,aG as W,bM as hn,bN as z,bO as pn,bE as An,bP as m,bj as N,bo as H,br as T,bQ as _n,bm as on,bR as wn,bH as On,aI as V,bF as bn,bS as I}from"./index-DkU82VsU.js";var Pn="[object Symbol]";function x(n){return typeof n=="symbol"||L(n)&&ln(n)==Pn}function vn(n,r){for(var e=-1,i=n==null?0:n.length,f=Array(i);++e<i;)f[e]=r(n[e],e,n);return f}var K=P?P.prototype:void 0,U=K?K.toString:void 0;function j(n){if(typeof n=="string")return n;if(A(n))return vn(n,j)+"";if(x(n))return U?U.call(n):"";var r=n+"";return r=="0"&&1/n==-1/0?"-0":r}function yn(){}function En(n,r){for(var e=-1,i=n==null?0:n.length;++e<i&&r(n[e],e,n)!==!1;);return n}function cn(n,r,e,i){for(var f=n.length,t=e+-1;++t<f;)if(r(n[t],t,n))return t;return-1}function Tn(n){return n!==n}function Rn(n,r,e){for(var i=e-1,f=n.length;++i<f;)if(n[i]===r)return i;return-1}function In(n,r,e){return r===r?Rn(n,r,e):cn(n,Tn,e)}function Sn(n,r){var e=n==null?0:n.length;return!!e&&In(n,r,0)>-1}function M(n){return W(n)?gn(n):dn(n)}var Ln=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,xn=/^\w*$/;function $(n,r){if(A(n))return!1;var e=typeof n;return e=="number"||e=="symbol"||e=="boolean"||n==null||x(n)?!0:xn.test(n)||!Ln.test(n)||r!=null&&n in Object(r)}var Mn=500;function $n(n){var r=hn(n,function(i){return e.size===Mn&&e.clear(),i}),e=r.cache;return r}var Cn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Fn=/\\(\\)?/g,Gn=$n(function(n){var r=[];return n.charCodeAt(0)===46&&r.push(""),n.replace(Cn,function(e,i,f,t){r.push(f?t.replace(Fn,"$1"):i||e)}),r});function Dn(n){return n==null?"":j(n)}function k(n,r){return A(n)?n:$(n,r)?[n]:Gn(Dn(n))}function R(n){if(typeof n=="string"||x(n))return n;var r=n+"";return r=="0"&&1/n==-1/0?"-0":r}function nn(n,r){r=k(r,n);for(var e=0,i=r.length;n!=null&&e<i;)n=n[R(r[e++])];return e&&e==i?n:void 0}function mn(n,r,e){var i=n==null?void 0:nn(n,r);return i===void 0?e:i}function rn(n,r){for(var e=-1,i=r.length,f=n.length;++e<i;)n[f+e]=r[e];return n}var B=P?P.isConcatSpreadable:void 0;function Nn(n){return A(n)||z(n)||!!(B&&n&&n[B])}function Br(n,r,e,i,f){var t=-1,s=n.length;for(e||(e=Nn),f||(f=[]);++t<s;){var u=n[t];e(u)?rn(f,u):i||(f[f.length]=u)}return f}function Hn(n,r,e,i){var f=-1,t=n==null?0:n.length;for(i&&t&&(e=n[++f]);++f<t;)e=r(e,n[f],f,n);return e}function en(n,r){for(var e=-1,i=n==null?0:n.length,f=0,t=[];++e<i;){var s=n[e];r(s,e,n)&&(t[f++]=s)}return t}function Kn(){return[]}var Un=Object.prototype,Bn=Un.propertyIsEnumerable,q=Object.getOwnPropertySymbols,qn=q?function(n){return n==null?[]:(n=Object(n),en(q(n),function(r){return Bn.call(n,r)}))}:Kn;function Zn(n,r,e){var i=r(n);return A(n)?i:rn(i,e(n))}function Z(n){return Zn(n,M,qn)}var Yn="__lodash_hash_undefined__";function Jn(n){return this.__data__.set(n,Yn),this}function Qn(n){return this.__data__.has(n)}function v(n){var r=-1,e=n==null?0:n.length;for(this.__data__=new pn;++r<e;)this.add(n[r])}v.prototype.add=v.prototype.push=Jn;v.prototype.has=Qn;function Xn(n,r){for(var e=-1,i=n==null?0:n.length;++e<i;)if(r(n[e],e,n))return!0;return!1}function tn(n,r){return n.has(r)}var Wn=1,zn=2;function fn(n,r,e,i,f,t){var s=e&Wn,u=n.length,a=r.length;if(u!=a&&!(s&&a>u))return!1;var h=t.get(n),g=t.get(r);if(h&&g)return h==r&&g==n;var l=-1,d=!0,o=e&zn?new v:void 0;for(t.set(n,r),t.set(r,n);++l<u;){var p=n[l],_=r[l];if(i)var w=s?i(_,p,l,r,n,t):i(p,_,l,n,r,t);if(w!==void 0){if(w)continue;d=!1;break}if(o){if(!Xn(r,function(O,b){if(!tn(o,b)&&(p===O||f(p,O,e,i,t)))return o.push(b)})){d=!1;break}}else if(!(p===_||f(p,_,e,i,t))){d=!1;break}}return t.delete(n),t.delete(r),d}function Vn(n){var r=-1,e=Array(n.size);return n.forEach(function(i,f){e[++r]=[f,i]}),e}function C(n){var r=-1,e=Array(n.size);return n.forEach(function(i){e[++r]=i}),e}var jn=1,kn=2,nr="[object Boolean]",rr="[object Date]",er="[object Error]",ir="[object Map]",tr="[object Number]",fr="[object RegExp]",sr="[object Set]",ur="[object String]",ar="[object Symbol]",lr="[object ArrayBuffer]",gr="[object DataView]",Y=P?P.prototype:void 0,S=Y?Y.valueOf:void 0;function dr(n,r,e,i,f,t,s){switch(e){case gr:if(n.byteLength!=r.byteLength||n.byteOffset!=r.byteOffset)return!1;n=n.buffer,r=r.buffer;case lr:return!(n.byteLength!=r.byteLength||!t(new m(n),new m(r)));case nr:case rr:case tr:return An(+n,+r);case er:return n.name==r.name&&n.message==r.message;case fr:case ur:return n==r+"";case ir:var u=Vn;case sr:var a=i&jn;if(u||(u=C),n.size!=r.size&&!a)return!1;var h=s.get(n);if(h)return h==r;i|=kn,s.set(n,r);var g=fn(u(n),u(r),i,f,t,s);return s.delete(n),g;case ar:if(S)return S.call(n)==S.call(r)}return!1}var hr=1,pr=Object.prototype,Ar=pr.hasOwnProperty;function _r(n,r,e,i,f,t){var s=e&hr,u=Z(n),a=u.length,h=Z(r),g=h.length;if(a!=g&&!s)return!1;for(var l=a;l--;){var d=u[l];if(!(s?d in r:Ar.call(r,d)))return!1}var o=t.get(n),p=t.get(r);if(o&&p)return o==r&&p==n;var _=!0;t.set(n,r),t.set(r,n);for(var w=s;++l<a;){d=u[l];var O=n[d],b=r[d];if(i)var D=s?i(b,O,d,r,n,t):i(O,b,d,n,r,t);if(!(D===void 0?O===b||f(O,b,e,i,t):D)){_=!1;break}w||(w=d=="constructor")}if(_&&!w){var y=n.constructor,E=r.constructor;y!=E&&"constructor"in n&&"constructor"in r&&!(typeof y=="function"&&y instanceof y&&typeof E=="function"&&E instanceof E)&&(_=!1)}return t.delete(n),t.delete(r),_}var or=1,J="[object Arguments]",Q="[object Array]",c="[object Object]",wr=Object.prototype,X=wr.hasOwnProperty;function Or(n,r,e,i,f,t){var s=A(n),u=A(r),a=s?Q:N(n),h=u?Q:N(r);a=a==J?c:a,h=h==J?c:h;var g=a==c,l=h==c,d=a==h;if(d&&H(n)){if(!H(r))return!1;s=!0,g=!1}if(d&&!g)return t||(t=new T),s||_n(n)?fn(n,r,e,i,f,t):dr(n,r,a,e,i,f,t);if(!(e&or)){var o=g&&X.call(n,"__wrapped__"),p=l&&X.call(r,"__wrapped__");if(o||p){var _=o?n.value():n,w=p?r.value():r;return t||(t=new T),f(_,w,e,i,t)}}return d?(t||(t=new T),_r(n,r,e,i,f,t)):!1}function F(n,r,e,i,f){return n===r?!0:n==null||r==null||!L(n)&&!L(r)?n!==n&&r!==r:Or(n,r,e,i,F,f)}var br=1,Pr=2;function vr(n,r,e,i){var f=e.length,t=f;if(n==null)return!t;for(n=Object(n);f--;){var s=e[f];if(s[2]?s[1]!==n[s[0]]:!(s[0]in n))return!1}for(;++f<t;){s=e[f];var u=s[0],a=n[u],h=s[1];if(s[2]){if(a===void 0&&!(u in n))return!1}else{var g=new T,l;if(!(l===void 0?F(h,a,br|Pr,i,g):l))return!1}}return!0}function sn(n){return n===n&&!on(n)}function yr(n){for(var r=M(n),e=r.length;e--;){var i=r[e],f=n[i];r[e]=[i,f,sn(f)]}return r}function un(n,r){return function(e){return e==null?!1:e[n]===r&&(r!==void 0||n in Object(e))}}function Er(n){var r=yr(n);return r.length==1&&r[0][2]?un(r[0][0],r[0][1]):function(e){return e===n||vr(e,n,r)}}function cr(n,r){return n!=null&&r in Object(n)}function Tr(n,r,e){r=k(r,n);for(var i=-1,f=r.length,t=!1;++i<f;){var s=R(r[i]);if(!(t=n!=null&&e(n,s)))break;n=n[s]}return t||++i!=f?t:(f=n==null?0:n.length,!!f&&wn(f)&&On(s,f)&&(A(n)||z(n)))}function Rr(n,r){return n!=null&&Tr(n,r,cr)}var Ir=1,Sr=2;function Lr(n,r){return $(n)&&sn(r)?un(R(n),r):function(e){var i=mn(e,n);return i===void 0&&i===r?Rr(e,n):F(r,i,Ir|Sr)}}function xr(n){return function(r){return r==null?void 0:r[n]}}function Mr(n){return function(r){return nn(r,n)}}function $r(n){return $(n)?xr(R(n)):Mr(n)}function an(n){return typeof n=="function"?n:n==null?V:typeof n=="object"?A(n)?Lr(n[0],n[1]):Er(n):$r(n)}function Cr(n,r){return n&&bn(n,r,M)}function Fr(n,r){return function(e,i){if(e==null)return e;if(!W(e))return n(e,i);for(var f=e.length,t=-1,s=Object(e);++t<f&&i(s[t],t,s)!==!1;);return e}}var G=Fr(Cr);function Gr(n){return typeof n=="function"?n:V}function qr(n,r){var e=A(n)?En:G;return e(n,Gr(r))}function Dr(n,r){var e=[];return G(n,function(i,f,t){r(i,f,t)&&e.push(i)}),e}function Zr(n,r){var e=A(n)?en:Dr;return e(n,an(r))}function mr(n,r,e,i,f){return f(n,function(t,s,u){e=i?(i=!1,t):r(e,t,s,u)}),e}function Yr(n,r,e){var i=A(n)?Hn:mr,f=arguments.length<3;return i(n,an(r),e,f,G)}var Nr=1/0,Hr=I&&1/C(new I([,-0]))[1]==Nr?function(n){return new I(n)}:yn,Kr=200;function Jr(n,r,e){var i=-1,f=Sn,t=n.length,s=!0,u=[],a=u;if(t>=Kr){var h=r?null:Hr(n);if(h)return C(h);s=!1,f=tn,a=new v}else a=r?[]:u;n:for(;++i<t;){var g=n[i],l=r?r(g):g;if(g=g!==0?g:0,s&&l===l){for(var d=a.length;d--;)if(a[d]===l)continue n;r&&a.push(l),u.push(g)}else f(a,l,e)||(a!==u&&a.push(l),u.push(g))}return u}export{G as a,Br as b,vn as c,an as d,rn as e,Zn as f,qn as g,Z as h,x as i,En as j,M as k,Jr as l,Zr as m,qr as n,cn as o,Gr as p,Cr as q,Yr as r,Kn as s,Tr as t,k as u,R as v,nn as w,Rr as x,Dn as y};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a3 as ln,a4 as an,a5 as g,a6 as tn,a7 as H,a8 as q,a9 as _,aa as un,ab as B,ac as rn,ad as L,ae as o,af as sn,ag as on,ah as fn}from"./index-DkU82VsU.js";function cn(l){return l.innerRadius}function gn(l){return l.outerRadius}function yn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,C,a){var O=I-l,i=D-h,n=C-v,d=a-A,u=d*O-n*i;if(!(u*u<g))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function W(l,h,I,D,v,A,C){var a=l-I,O=h-D,i=(C?A:-A)/L(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,F=(u+f)/2,t=(s+c)/2,m=f-u,y=c-s,R=m*m+y*y,T=v-A,P=u*c-f*s,S=(y<0?-1:1)*L(fn(0,T*T*R-P*P)),j=(P*y-m*S)/R,z=(-P*m-y*S)/R,w=(P*y+m*S)/R,p=(-P*m+y*S)/R,x=j-F,e=z-t,r=w-F,G=p-t;return x*x+e*e>r*r+G*G&&(j=w,z=p),{cx:j,cy:z,x01:-n,y01:-d,x11:j*(v/T-1),y11:z*(v/T-1)}}function hn(){var l=cn,h=gn,I=B(0),D=null,v=yn,A=dn,C=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-an,c=A.apply(this,arguments)-an,F=un(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>g))a.moveTo(0,0);else if(F>tn-g)a.moveTo(s*H(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>g&&(a.moveTo(u*H(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,y=c,R=f,T=c,P=F,S=F,j=C.apply(this,arguments)/2,z=j>g&&(D?+D.apply(this,arguments):L(u*u+s*s)),w=_(un(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if(z>g){var G=sn(z/u*q(j)),M=sn(z/s*q(j));(P-=G*2)>g?(G*=t?1:-1,R+=G,T-=G):(P=0,R=T=(f+c)/2),(S-=M*2)>g?(M*=t?1:-1,m+=M,y-=M):(S=0,m=y=(f+c)/2)}var J=s*H(m),K=s*q(m),N=u*H(T),Q=u*q(T);if(w>g){var U=s*H(y),V=s*q(y),X=u*H(R),Y=u*q(R),E;if(F<rn)if(E=pn(J,K,X,Y,U,V,N,Q)){var Z=J-E[0],$=K-E[1],b=U-E[0],k=V-E[1],nn=1/q(on((Z*b+$*k)/(L(Z*Z+$*$)*L(b*b+k*k)))/2),en=L(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>g?x>g?(e=W(X,Y,J,K,s,x,t),r=W(U,V,N,Q,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(J,K),a.arc(0,0,s,m,y,!t)):a.moveTo(J,K),!(u>g)||!(P>g)?a.lineTo(N,Q):p>g?(e=W(N,Q,U,V,u,-p,t),r=W(J,K,X,Y,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-rn/2;return[H(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:B(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:B(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:B(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:B(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:B(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:B(+n),i):A},i.padAngle=function(n){return arguments.length?(C=typeof n=="function"?n:B(+n),i):C},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|