truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +645 -23
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +15 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,810 @@
1
+ """Unified store management using truthound's store backends.
2
+
3
+ This module provides a unified interface for validation result storage
4
+ using truthound's enterprise store features:
5
+ - Retention policies (truthound.stores.retention)
6
+ - Caching (truthound.stores.caching)
7
+ - Versioning (truthound.stores.versioning)
8
+ - Storage tiering (truthound.stores.tiering)
9
+ - Observability (truthound.stores.observability)
10
+
11
+ Note: SQLite VACUUM is dashboard-specific and not part of truthound.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timedelta
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import Any, Callable
22
+
23
+ from truthound.stores import get_store
24
+ from truthound.stores.retention import (
25
+ CompositePolicy,
26
+ CountBasedPolicy,
27
+ RetentionConfig,
28
+ RetentionResult,
29
+ RetentionSchedule,
30
+ RetentionStore,
31
+ SizeBasedPolicy,
32
+ StatusBasedPolicy,
33
+ TagBasedPolicy,
34
+ TimeBasedPolicy,
35
+ )
36
+ from truthound.stores.caching import (
37
+ CachedStore,
38
+ CacheConfig,
39
+ CacheMode,
40
+ EvictionPolicy,
41
+ LFUCache,
42
+ LRUCache,
43
+ TTLCache,
44
+ )
45
+ from truthound.stores.versioning import (
46
+ VersionedStore,
47
+ VersionDiff,
48
+ VersionInfo,
49
+ VersioningConfig,
50
+ )
51
+ from truthound.stores.tiering import (
52
+ AccessBasedTierPolicy,
53
+ AgeBasedTierPolicy,
54
+ CompositeTierPolicy,
55
+ MigrationDirection,
56
+ SizeBasedTierPolicy,
57
+ StorageTier,
58
+ TieredStore,
59
+ TieringConfig,
60
+ TieringResult,
61
+ TierType,
62
+ )
63
+ from truthound.stores.observability import ObservableStore
64
+ from truthound.stores.observability import (
65
+ AuditConfig,
66
+ AuditEvent,
67
+ AuditEventType,
68
+ AuditLogger,
69
+ InMemoryAuditBackend,
70
+ InMemoryMetricsBackend,
71
+ JsonAuditBackend,
72
+ MetricsConfig,
73
+ MetricsRegistry,
74
+ ObservabilityConfig,
75
+ StoreMetrics,
76
+ TracingConfig,
77
+ )
78
+
79
+
80
+ # Local definitions for types not exported by truthound
81
+ class PolicyMode(str, Enum):
82
+ """Mode for composite retention policies."""
83
+
84
+ ALL = "all" # All policies must match
85
+ ANY = "any" # Any policy can match
86
+
87
+
88
+ class RetentionAction(str, Enum):
89
+ """Action to take when retention policy matches."""
90
+
91
+ DELETE = "delete"
92
+ ARCHIVE = "archive"
93
+ MOVE = "move"
94
+
95
+
96
+ class VersioningMode(str, Enum):
97
+ """Mode for versioning strategy."""
98
+
99
+ SEMANTIC = "semantic"
100
+ INCREMENTAL = "incremental"
101
+ TIMESTAMP = "timestamp"
102
+ GIT_LIKE = "git_like"
103
+
104
+
105
+ class AuditStatus(str, Enum):
106
+ """Status of audit events."""
107
+
108
+ SUCCESS = "success"
109
+ FAILURE = "failure"
110
+ WARNING = "warning"
111
+
112
+
113
+ class DataRedactor:
114
+ """Simple data redactor for audit logs."""
115
+
116
+ def __init__(self, fields_to_redact: list[str] | None = None):
117
+ self.fields_to_redact = fields_to_redact or []
118
+
119
+ def redact(self, data: dict[str, Any]) -> dict[str, Any]:
120
+ """Redact sensitive fields from data."""
121
+ result = dict(data)
122
+ for field in self.fields_to_redact:
123
+ if field in result:
124
+ result[field] = "[REDACTED]"
125
+ return result
126
+
127
+ from truthound_dashboard.config import get_settings
128
+
129
+ logger = logging.getLogger(__name__)
130
+
131
+
132
+ class CacheType(str, Enum):
133
+ """Cache eviction policy types."""
134
+ LRU = "lru"
135
+ LFU = "lfu"
136
+ TTL = "ttl"
137
+
138
+
139
+ @dataclass
140
+ class DashboardStoreConfig:
141
+ """Configuration for the dashboard store system.
142
+
143
+ Attributes:
144
+ base_path: Base path for file storage.
145
+ enable_caching: Enable caching layer.
146
+ cache_type: Type of cache (LRU, LFU, TTL).
147
+ cache_max_size: Maximum cache entries.
148
+ cache_ttl_seconds: TTL for cache entries.
149
+ enable_versioning: Enable result versioning.
150
+ max_versions: Maximum versions to keep per item.
151
+ enable_tiering: Enable storage tiering.
152
+ enable_observability: Enable audit/metrics/tracing.
153
+ audit_log_path: Path for audit logs.
154
+ """
155
+ base_path: Path = field(default_factory=lambda: Path(".truthound/store"))
156
+
157
+ # Caching
158
+ enable_caching: bool = True
159
+ cache_type: CacheType = CacheType.LFU
160
+ cache_max_size: int = 1000
161
+ cache_ttl_seconds: int = 3600
162
+
163
+ # Versioning
164
+ enable_versioning: bool = True
165
+ max_versions: int = 10
166
+
167
+ # Tiering
168
+ enable_tiering: bool = False
169
+ hot_retention_days: int = 7
170
+ warm_retention_days: int = 30
171
+ cold_retention_days: int = 90
172
+
173
+ # Observability
174
+ enable_observability: bool = True
175
+ enable_audit: bool = True
176
+ enable_metrics: bool = True
177
+ enable_tracing: bool = False
178
+ audit_log_path: Path = field(default_factory=lambda: Path(".truthound/audit"))
179
+
180
+
181
+ @dataclass
182
+ class RetentionPolicySettings:
183
+ """Retention policy settings using truthound policies.
184
+
185
+ Attributes:
186
+ validation_retention_days: Days to keep validations.
187
+ profile_keep_per_source: Profiles to keep per source.
188
+ notification_log_retention_days: Days to keep notification logs.
189
+ max_storage_mb: Maximum storage in MB (optional).
190
+ keep_failed_longer: Keep failed validations longer.
191
+ failed_retention_days: Days to keep failed validations.
192
+ protected_tags: Tags to never delete.
193
+ delete_tags: Tags to delete after retention.
194
+ """
195
+ validation_retention_days: int = 90
196
+ profile_keep_per_source: int = 5
197
+ notification_log_retention_days: int = 30
198
+ max_storage_mb: int | None = None
199
+ keep_failed_longer: bool = True
200
+ failed_retention_days: int = 180
201
+ protected_tags: list[str] = field(default_factory=list)
202
+ delete_tags: list[str] = field(default_factory=list)
203
+
204
+ def to_truthound_config(self) -> RetentionConfig:
205
+ """Convert to truthound RetentionConfig."""
206
+ policies = []
207
+
208
+ # Time-based policy for validations
209
+ policies.append(TimeBasedPolicy(max_age_days=self.validation_retention_days))
210
+
211
+ # Status-based policy for failed validations
212
+ if self.keep_failed_longer:
213
+ policies.append(
214
+ StatusBasedPolicy(
215
+ status="failure",
216
+ max_age_days=self.failed_retention_days,
217
+ retain=True,
218
+ )
219
+ )
220
+
221
+ # Size-based policy if configured
222
+ if self.max_storage_mb:
223
+ policies.append(SizeBasedPolicy(max_size_mb=self.max_storage_mb))
224
+
225
+ # Tag-based policies
226
+ if self.protected_tags:
227
+ policies.append(
228
+ TagBasedPolicy(
229
+ required_tags={tag: "*" for tag in self.protected_tags},
230
+ action=RetentionAction.ARCHIVE,
231
+ )
232
+ )
233
+
234
+ if self.delete_tags:
235
+ policies.append(
236
+ TagBasedPolicy(
237
+ delete_tags={tag: "*" for tag in self.delete_tags},
238
+ action=RetentionAction.DELETE,
239
+ )
240
+ )
241
+
242
+ return RetentionConfig(
243
+ policies=policies,
244
+ mode=PolicyMode.ANY,
245
+ default_action=RetentionAction.DELETE,
246
+ schedule=RetentionSchedule(
247
+ enabled=True,
248
+ interval_hours=24,
249
+ batch_size=1000,
250
+ ),
251
+ )
252
+
253
+
254
+ class DashboardStoreManager:
255
+ """Manager for truthound-based store with all enterprise features.
256
+
257
+ This manager wraps truthound's store system to provide:
258
+ - Caching with LRU/LFU/TTL eviction
259
+ - Versioning with diff and rollback
260
+ - Storage tiering (hot/warm/cold)
261
+ - Observability (audit, metrics, tracing)
262
+ - Retention policies
263
+
264
+ Example:
265
+ manager = DashboardStoreManager()
266
+ manager.initialize()
267
+
268
+ # Save with versioning
269
+ run_id = manager.save(result, message="Initial validation")
270
+
271
+ # Get version history
272
+ history = manager.get_version_history(run_id)
273
+
274
+ # Run retention cleanup
275
+ cleanup_result = manager.run_retention_cleanup()
276
+ """
277
+
278
+ def __init__(self, config: DashboardStoreConfig | None = None) -> None:
279
+ """Initialize store manager.
280
+
281
+ Args:
282
+ config: Store configuration. Uses defaults if not provided.
283
+ """
284
+ self._config = config or DashboardStoreConfig()
285
+ self._store = None
286
+ self._base_store = None
287
+ self._versioned_store = None
288
+ self._cached_store = None
289
+ self._tiered_store = None
290
+ self._observable_store = None
291
+ self._retention_store = None
292
+ self._audit_logger = None
293
+ self._metrics = None
294
+ self._cache = None
295
+ self._initialized = False
296
+
297
+ @property
298
+ def config(self) -> DashboardStoreConfig:
299
+ """Get store configuration."""
300
+ return self._config
301
+
302
+ def initialize(self) -> None:
303
+ """Initialize store with all configured layers."""
304
+ if self._initialized:
305
+ return
306
+
307
+ logger.info("Initializing dashboard store manager...")
308
+
309
+ # Base store (filesystem)
310
+ self._base_store = get_store(
311
+ "filesystem",
312
+ base_path=str(self._config.base_path),
313
+ )
314
+
315
+ current_store = self._base_store
316
+
317
+ # Layer 1: Versioning
318
+ if self._config.enable_versioning:
319
+ versioning_config = VersioningConfig(
320
+ mode=VersioningMode.INCREMENTAL,
321
+ max_versions=self._config.max_versions,
322
+ auto_cleanup=True,
323
+ track_changes=True,
324
+ )
325
+ self._versioned_store = VersionedStore(current_store, versioning_config)
326
+ current_store = self._versioned_store
327
+ logger.info(f"Versioning enabled (max {self._config.max_versions} versions)")
328
+
329
+ # Layer 2: Caching
330
+ if self._config.enable_caching:
331
+ cache_config = CacheConfig(
332
+ max_size=self._config.cache_max_size,
333
+ ttl_seconds=self._config.cache_ttl_seconds,
334
+ enable_statistics=True,
335
+ )
336
+
337
+ if self._config.cache_type == CacheType.LRU:
338
+ self._cache = LRUCache(
339
+ max_size=self._config.cache_max_size,
340
+ ttl_seconds=self._config.cache_ttl_seconds,
341
+ config=cache_config,
342
+ )
343
+ elif self._config.cache_type == CacheType.LFU:
344
+ self._cache = LFUCache(
345
+ max_size=self._config.cache_max_size,
346
+ ttl_seconds=self._config.cache_ttl_seconds,
347
+ config=cache_config,
348
+ )
349
+ else:
350
+ self._cache = TTLCache(
351
+ ttl_seconds=self._config.cache_ttl_seconds,
352
+ max_size=self._config.cache_max_size,
353
+ )
354
+
355
+ self._cached_store = CachedStore(
356
+ current_store,
357
+ self._cache,
358
+ mode=CacheMode.WRITE_THROUGH,
359
+ )
360
+ current_store = self._cached_store
361
+ logger.info(f"Caching enabled ({self._config.cache_type.value})")
362
+
363
+ # Layer 3: Tiering
364
+ if self._config.enable_tiering:
365
+ self._setup_tiering(current_store)
366
+ if self._tiered_store:
367
+ current_store = self._tiered_store
368
+ logger.info("Storage tiering enabled")
369
+
370
+ # Layer 4: Observability
371
+ if self._config.enable_observability:
372
+ self._setup_observability(current_store)
373
+ if self._observable_store:
374
+ current_store = self._observable_store
375
+ logger.info("Observability enabled")
376
+
377
+ self._store = current_store
378
+ self._initialized = True
379
+ logger.info("Dashboard store manager initialized")
380
+
381
+ def _setup_tiering(self, base_store) -> None:
382
+ """Set up storage tiering."""
383
+ # Create tier stores
384
+ hot_store = get_store(
385
+ "filesystem",
386
+ base_path=str(self._config.base_path / "hot"),
387
+ )
388
+ warm_store = get_store(
389
+ "filesystem",
390
+ base_path=str(self._config.base_path / "warm"),
391
+ )
392
+ cold_store = get_store(
393
+ "filesystem",
394
+ base_path=str(self._config.base_path / "cold"),
395
+ )
396
+
397
+ tiers = [
398
+ StorageTier(
399
+ name="hot",
400
+ store=hot_store,
401
+ tier_type=TierType.HOT,
402
+ priority=1,
403
+ ),
404
+ StorageTier(
405
+ name="warm",
406
+ store=warm_store,
407
+ tier_type=TierType.WARM,
408
+ priority=2,
409
+ ),
410
+ StorageTier(
411
+ name="cold",
412
+ store=cold_store,
413
+ tier_type=TierType.COLD,
414
+ priority=3,
415
+ ),
416
+ ]
417
+
418
+ # Age-based tier policies
419
+ policies = [
420
+ AgeBasedTierPolicy(
421
+ from_tier="hot",
422
+ to_tier="warm",
423
+ after_days=self._config.hot_retention_days,
424
+ ),
425
+ AgeBasedTierPolicy(
426
+ from_tier="warm",
427
+ to_tier="cold",
428
+ after_days=self._config.warm_retention_days,
429
+ ),
430
+ ]
431
+
432
+ tiering_config = TieringConfig(
433
+ policies=policies,
434
+ default_tier="hot",
435
+ check_interval_hours=24,
436
+ batch_size=100,
437
+ )
438
+
439
+ self._tiered_store = TieredStore(tiers, tiering_config)
440
+
441
+ def _setup_observability(self, base_store) -> None:
442
+ """Set up observability (audit, metrics, tracing)."""
443
+ # Audit backend
444
+ audit_backend = None
445
+ if self._config.enable_audit:
446
+ self._config.audit_log_path.mkdir(parents=True, exist_ok=True)
447
+ audit_config = AuditConfig(
448
+ enabled=True,
449
+ file_path=str(self._config.audit_log_path / "dashboard_audit.jsonl"),
450
+ redact_sensitive=True,
451
+ sensitive_fields=["password", "api_key", "token"],
452
+ )
453
+ audit_backend = JsonAuditBackend(
454
+ config=audit_config,
455
+ file_path=self._config.audit_log_path / "dashboard_audit.jsonl",
456
+ )
457
+ self._audit_logger = AuditLogger(
458
+ backend=audit_backend,
459
+ store_type="dashboard",
460
+ store_id="validation_store",
461
+ )
462
+
463
+ # Metrics backend
464
+ metrics_backend = None
465
+ if self._config.enable_metrics:
466
+ metrics_backend = InMemoryMetricsBackend()
467
+ self._metrics = StoreMetrics(
468
+ backend=metrics_backend,
469
+ store_type="dashboard",
470
+ store_id="validation_store",
471
+ )
472
+
473
+ obs_config = ObservabilityConfig(
474
+ audit=AuditConfig(enabled=self._config.enable_audit),
475
+ metrics=MetricsConfig(enabled=self._config.enable_metrics),
476
+ tracing=TracingConfig(enabled=self._config.enable_tracing),
477
+ )
478
+
479
+ self._observable_store = ObservableStore(base_store, obs_config)
480
+
481
+ # ----- Retention Operations -----
482
+
483
+ def create_retention_store(
484
+ self,
485
+ settings: RetentionPolicySettings,
486
+ ) -> RetentionStore:
487
+ """Create a retention store with the given settings.
488
+
489
+ Args:
490
+ settings: Retention policy settings.
491
+
492
+ Returns:
493
+ Configured RetentionStore.
494
+ """
495
+ if not self._store:
496
+ self.initialize()
497
+
498
+ config = settings.to_truthound_config()
499
+ return RetentionStore(self._store, config)
500
+
501
+ async def run_retention_cleanup(
502
+ self,
503
+ settings: RetentionPolicySettings,
504
+ dry_run: bool = False,
505
+ ) -> RetentionResult:
506
+ """Run retention cleanup with truthound's retention system.
507
+
508
+ Args:
509
+ settings: Retention policy settings.
510
+ dry_run: If True, don't actually delete items.
511
+
512
+ Returns:
513
+ RetentionResult with cleanup details.
514
+ """
515
+ if not self._store:
516
+ self.initialize()
517
+
518
+ config = settings.to_truthound_config()
519
+ config.dry_run = dry_run
520
+
521
+ retention_store = RetentionStore(self._store, config)
522
+ result = await retention_store.apply_retention()
523
+
524
+ logger.info(
525
+ f"Retention cleanup: scanned={result.items_scanned}, "
526
+ f"deleted={result.items_deleted}, freed={result.bytes_freed} bytes"
527
+ )
528
+
529
+ return result
530
+
531
+ # ----- Cache Operations -----
532
+
533
+ def get_cache_stats(self) -> dict[str, Any]:
534
+ """Get cache statistics from truthound's cache.
535
+
536
+ Returns:
537
+ Cache statistics dictionary.
538
+ """
539
+ if not self._cache:
540
+ return {
541
+ "total_entries": 0,
542
+ "expired_entries": 0,
543
+ "valid_entries": 0,
544
+ "max_size": 0,
545
+ "hits": 0,
546
+ "misses": 0,
547
+ "hit_rate": 0.0,
548
+ }
549
+
550
+ metrics = self._cache.metrics
551
+ return {
552
+ "total_entries": metrics.size,
553
+ "expired_entries": metrics.expirations,
554
+ "valid_entries": metrics.size,
555
+ "max_size": self._config.cache_max_size,
556
+ "hits": metrics.hits,
557
+ "misses": metrics.misses,
558
+ "hit_rate": metrics.hit_rate,
559
+ "evictions": metrics.evictions,
560
+ "average_get_time_ms": metrics.average_get_time_ms,
561
+ "average_set_time_ms": metrics.average_set_time_ms,
562
+ }
563
+
564
+ def clear_cache(self, pattern: str | None = None) -> None:
565
+ """Clear cache entries.
566
+
567
+ Args:
568
+ pattern: Optional pattern to match keys (prefix match).
569
+ """
570
+ if not self._cache:
571
+ return
572
+
573
+ if pattern:
574
+ self._cache.delete_many([
575
+ key for key in self._cache._cache.keys()
576
+ if key.startswith(pattern)
577
+ ])
578
+ else:
579
+ self._cache.clear()
580
+
581
+ # ----- Versioning Operations -----
582
+
583
+ def get_version_history(
584
+ self,
585
+ item_id: str,
586
+ limit: int | None = None,
587
+ ) -> list[VersionInfo]:
588
+ """Get version history for an item.
589
+
590
+ Args:
591
+ item_id: Item identifier.
592
+ limit: Maximum versions to return.
593
+
594
+ Returns:
595
+ List of VersionInfo objects.
596
+ """
597
+ if not self._versioned_store:
598
+ return []
599
+
600
+ return self._versioned_store.get_version_history(item_id, limit=limit)
601
+
602
+ def get_version_diff(
603
+ self,
604
+ item_id: str,
605
+ version_a: int,
606
+ version_b: int | None = None,
607
+ ) -> VersionDiff:
608
+ """Get diff between versions.
609
+
610
+ Args:
611
+ item_id: Item identifier.
612
+ version_a: First version.
613
+ version_b: Second version (latest if None).
614
+
615
+ Returns:
616
+ VersionDiff with changes.
617
+ """
618
+ if not self._versioned_store:
619
+ raise ValueError("Versioning not enabled")
620
+
621
+ return self._versioned_store.diff(item_id, version_a, version_b)
622
+
623
+ def rollback_version(
624
+ self,
625
+ item_id: str,
626
+ version: int,
627
+ message: str | None = None,
628
+ ) -> Any:
629
+ """Rollback to a previous version.
630
+
631
+ Args:
632
+ item_id: Item identifier.
633
+ version: Version to rollback to.
634
+ message: Optional rollback message.
635
+
636
+ Returns:
637
+ Rolled back item.
638
+ """
639
+ if not self._versioned_store:
640
+ raise ValueError("Versioning not enabled")
641
+
642
+ return self._versioned_store.rollback(item_id, version, message=message)
643
+
644
+ # ----- Tiering Operations -----
645
+
646
+ async def run_tiering(self, dry_run: bool = False) -> TieringResult | None:
647
+ """Run storage tiering migration.
648
+
649
+ Args:
650
+ dry_run: If True, don't actually migrate items.
651
+
652
+ Returns:
653
+ TieringResult or None if tiering not enabled.
654
+ """
655
+ if not self._tiered_store:
656
+ return None
657
+
658
+ result = await self._tiered_store.run_migration(dry_run=dry_run)
659
+
660
+ logger.info(
661
+ f"Tiering migration: scanned={result.items_scanned}, "
662
+ f"migrated={result.items_migrated}, bytes={result.bytes_migrated}"
663
+ )
664
+
665
+ return result
666
+
667
+ def get_tier_stats(self) -> dict[str, Any]:
668
+ """Get storage tier statistics.
669
+
670
+ Returns:
671
+ Tier statistics dictionary.
672
+ """
673
+ if not self._tiered_store:
674
+ return {}
675
+
676
+ return {
677
+ tier.name: {
678
+ "type": tier.tier_type.value,
679
+ "priority": tier.priority,
680
+ "item_count": len(list(tier.store.list_ids())),
681
+ }
682
+ for tier in self._tiered_store._tiers
683
+ }
684
+
685
+ # ----- Observability Operations -----
686
+
687
+ def get_audit_events(
688
+ self,
689
+ event_type: AuditEventType | None = None,
690
+ start_time: datetime | None = None,
691
+ end_time: datetime | None = None,
692
+ limit: int = 100,
693
+ ) -> list[AuditEvent]:
694
+ """Query audit events.
695
+
696
+ Args:
697
+ event_type: Filter by event type.
698
+ start_time: Filter events after this time.
699
+ end_time: Filter events before this time.
700
+ limit: Maximum events to return.
701
+
702
+ Returns:
703
+ List of AuditEvent objects.
704
+ """
705
+ if not self._audit_logger:
706
+ return []
707
+
708
+ return self._audit_logger.backend.query(
709
+ event_type=event_type,
710
+ start_time=start_time,
711
+ end_time=end_time,
712
+ limit=limit,
713
+ )
714
+
715
+ def get_store_metrics(self) -> dict[str, Any]:
716
+ """Get store metrics.
717
+
718
+ Returns:
719
+ Store metrics dictionary.
720
+ """
721
+ if not self._metrics:
722
+ return {}
723
+
724
+ try:
725
+ backend = self._metrics.backend
726
+ if hasattr(backend, 'get_metrics'):
727
+ return backend.get_metrics()
728
+ # InMemoryMetricsBackend doesn't have get_metrics, return empty
729
+ return {}
730
+ except Exception:
731
+ return {}
732
+
733
+ # ----- Standard Store Operations -----
734
+
735
+ def save(self, result: Any, **kwargs) -> str:
736
+ """Save a validation result.
737
+
738
+ Args:
739
+ result: Validation result to save.
740
+ **kwargs: Additional arguments (message, created_by, etc.)
741
+
742
+ Returns:
743
+ Run ID.
744
+ """
745
+ if not self._store:
746
+ self.initialize()
747
+
748
+ return self._store.save(result, **kwargs)
749
+
750
+ def get(self, item_id: str, version: int | None = None) -> Any:
751
+ """Get a validation result.
752
+
753
+ Args:
754
+ item_id: Item identifier.
755
+ version: Optional version (if versioning enabled).
756
+
757
+ Returns:
758
+ Validation result.
759
+ """
760
+ if not self._store:
761
+ self.initialize()
762
+
763
+ if version and self._versioned_store:
764
+ return self._versioned_store.get(item_id, version=version)
765
+
766
+ return self._store.get(item_id)
767
+
768
+ def delete(self, item_id: str) -> bool:
769
+ """Delete a validation result.
770
+
771
+ Args:
772
+ item_id: Item identifier.
773
+
774
+ Returns:
775
+ True if deleted.
776
+ """
777
+ if not self._store:
778
+ self.initialize()
779
+
780
+ return self._store.delete(item_id)
781
+
782
+ def close(self) -> None:
783
+ """Close all store connections."""
784
+ if self._store:
785
+ self._store.close()
786
+ self._initialized = False
787
+
788
+
789
+ # Singleton instance
790
+ _store_manager: DashboardStoreManager | None = None
791
+
792
+
793
+ def get_store_manager() -> DashboardStoreManager:
794
+ """Get store manager singleton.
795
+
796
+ Returns:
797
+ DashboardStoreManager instance.
798
+ """
799
+ global _store_manager
800
+ if _store_manager is None:
801
+ _store_manager = DashboardStoreManager()
802
+ return _store_manager
803
+
804
+
805
+ def reset_store_manager() -> None:
806
+ """Reset store manager singleton (for testing)."""
807
+ global _store_manager
808
+ if _store_manager:
809
+ _store_manager.close()
810
+ _store_manager = None