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.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {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 = "![Status](https://img.shields.io/badge/Status-PASSED-success)"
90
- else:
91
- badge = "![Status](https://img.shields.io/badge/Status-FAILED-critical)"
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"![{severity}](https://img.shields.io/badge/{severity}-{color})"
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};