truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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 +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -22,6 +22,7 @@ import logging
|
|
|
22
22
|
from abc import ABC, abstractmethod
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from datetime import datetime, timedelta
|
|
25
|
+
from enum import Enum
|
|
25
26
|
from typing import Any
|
|
26
27
|
|
|
27
28
|
from sqlalchemy import delete, func, select, text
|
|
@@ -38,6 +39,46 @@ from truthound_dashboard.db.models import (
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
class RetentionPolicyType(str, Enum):
|
|
43
|
+
"""Types of retention policies."""
|
|
44
|
+
|
|
45
|
+
TIME = "time" # Time-based retention (days)
|
|
46
|
+
COUNT = "count" # Count-based retention (keep N records)
|
|
47
|
+
SIZE = "size" # Size-based retention (keep under N bytes)
|
|
48
|
+
STATUS = "status" # Status-based retention (keep passed/failed)
|
|
49
|
+
TAG = "tag" # Tag-based retention (keep/remove by tag)
|
|
50
|
+
COMPOSITE = "composite" # Combination of multiple policies
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class RetentionPolicy:
|
|
55
|
+
"""Individual retention policy definition.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
policy_type: Type of retention policy.
|
|
59
|
+
value: Policy value (days, count, bytes, status, or tag).
|
|
60
|
+
target: What this policy applies to (validations, profiles, etc).
|
|
61
|
+
priority: Policy priority (higher = applied first).
|
|
62
|
+
enabled: Whether this policy is active.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
policy_type: RetentionPolicyType
|
|
66
|
+
value: Any
|
|
67
|
+
target: str = "validations"
|
|
68
|
+
priority: int = 0
|
|
69
|
+
enabled: bool = True
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
"""Convert to dictionary."""
|
|
73
|
+
return {
|
|
74
|
+
"policy_type": self.policy_type.value if isinstance(self.policy_type, Enum) else self.policy_type,
|
|
75
|
+
"value": self.value,
|
|
76
|
+
"target": self.target,
|
|
77
|
+
"priority": self.priority,
|
|
78
|
+
"enabled": self.enabled,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
41
82
|
@dataclass
|
|
42
83
|
class CleanupResult:
|
|
43
84
|
"""Result of a cleanup operation.
|
|
@@ -71,11 +112,26 @@ class CleanupResult:
|
|
|
71
112
|
class MaintenanceConfig:
|
|
72
113
|
"""Configuration for maintenance tasks.
|
|
73
114
|
|
|
115
|
+
Supports 6 retention policy types:
|
|
116
|
+
- Time: Keep records for N days
|
|
117
|
+
- Count: Keep N most recent records
|
|
118
|
+
- Size: Keep total storage under N bytes
|
|
119
|
+
- Status: Different retention for passed/failed validations
|
|
120
|
+
- Tag: Protect or delete records by tag
|
|
121
|
+
- Composite: Combine multiple policies
|
|
122
|
+
|
|
74
123
|
Attributes:
|
|
75
|
-
validation_retention_days: Days to keep validation records.
|
|
76
|
-
profile_keep_per_source: Number of profiles to keep per source.
|
|
77
|
-
notification_log_retention_days: Days to keep notification logs.
|
|
124
|
+
validation_retention_days: Days to keep validation records (Time policy).
|
|
125
|
+
profile_keep_per_source: Number of profiles to keep per source (Count policy).
|
|
126
|
+
notification_log_retention_days: Days to keep notification logs (Time policy).
|
|
78
127
|
run_vacuum: Whether to run VACUUM after cleanup.
|
|
128
|
+
enabled: Whether automatic maintenance is enabled.
|
|
129
|
+
max_storage_mb: Maximum storage in MB for validations (Size policy).
|
|
130
|
+
keep_failed_validations: Whether to keep failed validations longer (Status policy).
|
|
131
|
+
failed_retention_days: Days to keep failed validations (Status policy).
|
|
132
|
+
protected_tags: Tags to never delete (Tag policy).
|
|
133
|
+
delete_tags: Tags to delete after standard retention (Tag policy).
|
|
134
|
+
policies: List of custom retention policies (Composite).
|
|
79
135
|
"""
|
|
80
136
|
|
|
81
137
|
validation_retention_days: int = 90
|
|
@@ -83,6 +139,106 @@ class MaintenanceConfig:
|
|
|
83
139
|
notification_log_retention_days: int = 30
|
|
84
140
|
run_vacuum: bool = True
|
|
85
141
|
enabled: bool = True
|
|
142
|
+
# Size-based retention
|
|
143
|
+
max_storage_mb: int | None = None # None = no limit
|
|
144
|
+
# Status-based retention
|
|
145
|
+
keep_failed_validations: bool = True # Keep failed longer for debugging
|
|
146
|
+
failed_retention_days: int = 180 # Days to keep failed validations
|
|
147
|
+
# Tag-based retention
|
|
148
|
+
protected_tags: list[str] = field(default_factory=list) # Tags to never delete
|
|
149
|
+
delete_tags: list[str] = field(default_factory=list) # Tags to delete after retention
|
|
150
|
+
# Custom policies (Composite)
|
|
151
|
+
policies: list[RetentionPolicy] = field(default_factory=list)
|
|
152
|
+
|
|
153
|
+
def get_active_policies(self) -> list[RetentionPolicy]:
|
|
154
|
+
"""Get all active retention policies sorted by priority.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of RetentionPolicy sorted by priority (highest first).
|
|
158
|
+
"""
|
|
159
|
+
active: list[RetentionPolicy] = []
|
|
160
|
+
|
|
161
|
+
# Time policy for validations
|
|
162
|
+
active.append(RetentionPolicy(
|
|
163
|
+
policy_type=RetentionPolicyType.TIME,
|
|
164
|
+
value=self.validation_retention_days,
|
|
165
|
+
target="validations",
|
|
166
|
+
priority=10,
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
# Count policy for profiles
|
|
170
|
+
active.append(RetentionPolicy(
|
|
171
|
+
policy_type=RetentionPolicyType.COUNT,
|
|
172
|
+
value=self.profile_keep_per_source,
|
|
173
|
+
target="profiles",
|
|
174
|
+
priority=10,
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
# Time policy for notification logs
|
|
178
|
+
active.append(RetentionPolicy(
|
|
179
|
+
policy_type=RetentionPolicyType.TIME,
|
|
180
|
+
value=self.notification_log_retention_days,
|
|
181
|
+
target="notification_logs",
|
|
182
|
+
priority=10,
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
# Size policy if configured
|
|
186
|
+
if self.max_storage_mb is not None:
|
|
187
|
+
active.append(RetentionPolicy(
|
|
188
|
+
policy_type=RetentionPolicyType.SIZE,
|
|
189
|
+
value=self.max_storage_mb * 1024 * 1024, # Convert to bytes
|
|
190
|
+
target="validations",
|
|
191
|
+
priority=20,
|
|
192
|
+
))
|
|
193
|
+
|
|
194
|
+
# Status policy if keeping failed validations longer
|
|
195
|
+
if self.keep_failed_validations:
|
|
196
|
+
active.append(RetentionPolicy(
|
|
197
|
+
policy_type=RetentionPolicyType.STATUS,
|
|
198
|
+
value={"status": "failed", "retention_days": self.failed_retention_days},
|
|
199
|
+
target="validations",
|
|
200
|
+
priority=15,
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
# Tag policy for protected tags
|
|
204
|
+
if self.protected_tags:
|
|
205
|
+
active.append(RetentionPolicy(
|
|
206
|
+
policy_type=RetentionPolicyType.TAG,
|
|
207
|
+
value={"protect": self.protected_tags},
|
|
208
|
+
target="validations",
|
|
209
|
+
priority=100, # Highest priority - never delete
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
# Tag policy for delete tags
|
|
213
|
+
if self.delete_tags:
|
|
214
|
+
active.append(RetentionPolicy(
|
|
215
|
+
policy_type=RetentionPolicyType.TAG,
|
|
216
|
+
value={"delete": self.delete_tags},
|
|
217
|
+
target="validations",
|
|
218
|
+
priority=5, # Lower priority
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
# Add custom policies
|
|
222
|
+
active.extend([p for p in self.policies if p.enabled])
|
|
223
|
+
|
|
224
|
+
# Sort by priority (descending)
|
|
225
|
+
return sorted(active, key=lambda p: p.priority, reverse=True)
|
|
226
|
+
|
|
227
|
+
def to_dict(self) -> dict[str, Any]:
|
|
228
|
+
"""Convert to dictionary for API responses."""
|
|
229
|
+
return {
|
|
230
|
+
"validation_retention_days": self.validation_retention_days,
|
|
231
|
+
"profile_keep_per_source": self.profile_keep_per_source,
|
|
232
|
+
"notification_log_retention_days": self.notification_log_retention_days,
|
|
233
|
+
"run_vacuum": self.run_vacuum,
|
|
234
|
+
"enabled": self.enabled,
|
|
235
|
+
"max_storage_mb": self.max_storage_mb,
|
|
236
|
+
"keep_failed_validations": self.keep_failed_validations,
|
|
237
|
+
"failed_retention_days": self.failed_retention_days,
|
|
238
|
+
"protected_tags": self.protected_tags,
|
|
239
|
+
"delete_tags": self.delete_tags,
|
|
240
|
+
"active_policies": [p.to_dict() for p in self.get_active_policies()],
|
|
241
|
+
}
|
|
86
242
|
|
|
87
243
|
|
|
88
244
|
class CleanupStrategy(ABC):
|
|
@@ -300,6 +456,272 @@ class NotificationLogCleanupStrategy(CleanupStrategy):
|
|
|
300
456
|
)
|
|
301
457
|
|
|
302
458
|
|
|
459
|
+
class SizeBasedCleanupStrategy(CleanupStrategy):
|
|
460
|
+
"""Cleanup strategy based on total storage size.
|
|
461
|
+
|
|
462
|
+
Removes oldest validations when total storage exceeds limit.
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def name(self) -> str:
|
|
467
|
+
return "size_based_cleanup"
|
|
468
|
+
|
|
469
|
+
async def execute(self, config: MaintenanceConfig) -> CleanupResult:
|
|
470
|
+
"""Remove validations if total storage exceeds limit."""
|
|
471
|
+
start_time = datetime.utcnow()
|
|
472
|
+
total_deleted = 0
|
|
473
|
+
|
|
474
|
+
if config.max_storage_mb is None:
|
|
475
|
+
return CleanupResult(
|
|
476
|
+
task_name=self.name,
|
|
477
|
+
records_deleted=0,
|
|
478
|
+
duration_ms=0,
|
|
479
|
+
success=True,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
max_bytes = config.max_storage_mb * 1024 * 1024
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
async with get_session() as session:
|
|
486
|
+
# Estimate current storage (based on result_json size)
|
|
487
|
+
# In production, you might track actual file sizes
|
|
488
|
+
size_result = await session.execute(
|
|
489
|
+
select(func.sum(func.length(Validation.result_json)))
|
|
490
|
+
)
|
|
491
|
+
current_size = size_result.scalar() or 0
|
|
492
|
+
|
|
493
|
+
if current_size <= max_bytes:
|
|
494
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
495
|
+
return CleanupResult(
|
|
496
|
+
task_name=self.name,
|
|
497
|
+
records_deleted=0,
|
|
498
|
+
duration_ms=duration,
|
|
499
|
+
success=True,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Calculate how much to delete (aim for 80% of max)
|
|
503
|
+
target_size = int(max_bytes * 0.8)
|
|
504
|
+
bytes_to_free = current_size - target_size
|
|
505
|
+
|
|
506
|
+
# Get validations ordered by created_at, delete oldest first
|
|
507
|
+
validations_result = await session.execute(
|
|
508
|
+
select(Validation.id, func.length(Validation.result_json))
|
|
509
|
+
.order_by(Validation.created_at.asc())
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
freed_bytes = 0
|
|
513
|
+
ids_to_delete = []
|
|
514
|
+
|
|
515
|
+
for val_id, size in validations_result:
|
|
516
|
+
if freed_bytes >= bytes_to_free:
|
|
517
|
+
break
|
|
518
|
+
ids_to_delete.append(val_id)
|
|
519
|
+
freed_bytes += size or 0
|
|
520
|
+
|
|
521
|
+
if ids_to_delete:
|
|
522
|
+
await session.execute(
|
|
523
|
+
delete(Validation).where(Validation.id.in_(ids_to_delete))
|
|
524
|
+
)
|
|
525
|
+
total_deleted = len(ids_to_delete)
|
|
526
|
+
|
|
527
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
528
|
+
|
|
529
|
+
logger.info(
|
|
530
|
+
f"Size-based cleanup: deleted {total_deleted} records, "
|
|
531
|
+
f"freed ~{freed_bytes / 1024 / 1024:.2f} MB"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return CleanupResult(
|
|
535
|
+
task_name=self.name,
|
|
536
|
+
records_deleted=total_deleted,
|
|
537
|
+
duration_ms=duration,
|
|
538
|
+
success=True,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
543
|
+
logger.error(f"Size-based cleanup failed: {e}")
|
|
544
|
+
return CleanupResult(
|
|
545
|
+
task_name=self.name,
|
|
546
|
+
duration_ms=duration,
|
|
547
|
+
success=False,
|
|
548
|
+
error=str(e),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class StatusBasedCleanupStrategy(CleanupStrategy):
|
|
553
|
+
"""Cleanup strategy based on validation status.
|
|
554
|
+
|
|
555
|
+
Applies different retention rules for passed vs failed validations.
|
|
556
|
+
Failed validations are kept longer for debugging purposes.
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def name(self) -> str:
|
|
561
|
+
return "status_based_cleanup"
|
|
562
|
+
|
|
563
|
+
async def execute(self, config: MaintenanceConfig) -> CleanupResult:
|
|
564
|
+
"""Remove validations based on status-specific retention."""
|
|
565
|
+
start_time = datetime.utcnow()
|
|
566
|
+
total_deleted = 0
|
|
567
|
+
|
|
568
|
+
if not config.keep_failed_validations:
|
|
569
|
+
return CleanupResult(
|
|
570
|
+
task_name=self.name,
|
|
571
|
+
records_deleted=0,
|
|
572
|
+
duration_ms=0,
|
|
573
|
+
success=True,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
# Standard cutoff for passed validations
|
|
578
|
+
passed_cutoff = datetime.utcnow() - timedelta(
|
|
579
|
+
days=config.validation_retention_days
|
|
580
|
+
)
|
|
581
|
+
# Extended cutoff for failed validations
|
|
582
|
+
failed_cutoff = datetime.utcnow() - timedelta(
|
|
583
|
+
days=config.failed_retention_days
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
async with get_session() as session:
|
|
587
|
+
# Delete old passed validations (use standard retention)
|
|
588
|
+
passed_count_result = await session.execute(
|
|
589
|
+
select(func.count(Validation.id)).where(
|
|
590
|
+
Validation.passed == True, # noqa: E712
|
|
591
|
+
Validation.created_at < passed_cutoff,
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
passed_count = passed_count_result.scalar() or 0
|
|
595
|
+
|
|
596
|
+
if passed_count > 0:
|
|
597
|
+
await session.execute(
|
|
598
|
+
delete(Validation).where(
|
|
599
|
+
Validation.passed == True, # noqa: E712
|
|
600
|
+
Validation.created_at < passed_cutoff,
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
total_deleted += passed_count
|
|
604
|
+
|
|
605
|
+
# Delete old failed validations (use extended retention)
|
|
606
|
+
failed_count_result = await session.execute(
|
|
607
|
+
select(func.count(Validation.id)).where(
|
|
608
|
+
Validation.passed == False, # noqa: E712
|
|
609
|
+
Validation.created_at < failed_cutoff,
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
failed_count = failed_count_result.scalar() or 0
|
|
613
|
+
|
|
614
|
+
if failed_count > 0:
|
|
615
|
+
await session.execute(
|
|
616
|
+
delete(Validation).where(
|
|
617
|
+
Validation.passed == False, # noqa: E712
|
|
618
|
+
Validation.created_at < failed_cutoff,
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
total_deleted += failed_count
|
|
622
|
+
|
|
623
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
624
|
+
|
|
625
|
+
logger.info(
|
|
626
|
+
f"Status-based cleanup: deleted {passed_count} passed, "
|
|
627
|
+
f"{failed_count} failed validations"
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
return CleanupResult(
|
|
631
|
+
task_name=self.name,
|
|
632
|
+
records_deleted=total_deleted,
|
|
633
|
+
duration_ms=duration,
|
|
634
|
+
success=True,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
except Exception as e:
|
|
638
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
639
|
+
logger.error(f"Status-based cleanup failed: {e}")
|
|
640
|
+
return CleanupResult(
|
|
641
|
+
task_name=self.name,
|
|
642
|
+
duration_ms=duration,
|
|
643
|
+
success=False,
|
|
644
|
+
error=str(e),
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class TagBasedCleanupStrategy(CleanupStrategy):
|
|
649
|
+
"""Cleanup strategy based on validation tags.
|
|
650
|
+
|
|
651
|
+
Protects tagged validations or deletes them based on configuration.
|
|
652
|
+
Tags can be stored in validation metadata or a separate tags table.
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def name(self) -> str:
|
|
657
|
+
return "tag_based_cleanup"
|
|
658
|
+
|
|
659
|
+
async def execute(self, config: MaintenanceConfig) -> CleanupResult:
|
|
660
|
+
"""Handle tag-based retention rules."""
|
|
661
|
+
start_time = datetime.utcnow()
|
|
662
|
+
total_deleted = 0
|
|
663
|
+
|
|
664
|
+
if not config.protected_tags and not config.delete_tags:
|
|
665
|
+
return CleanupResult(
|
|
666
|
+
task_name=self.name,
|
|
667
|
+
records_deleted=0,
|
|
668
|
+
duration_ms=0,
|
|
669
|
+
success=True,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
try:
|
|
673
|
+
async with get_session() as session:
|
|
674
|
+
# Handle delete_tags: remove validations with these tags
|
|
675
|
+
# that are past standard retention
|
|
676
|
+
if config.delete_tags:
|
|
677
|
+
cutoff = datetime.utcnow() - timedelta(
|
|
678
|
+
days=config.validation_retention_days
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
for tag in config.delete_tags:
|
|
682
|
+
# Tags stored in result_json metadata
|
|
683
|
+
# This is a simplified approach; production might use a tags table
|
|
684
|
+
count_result = await session.execute(
|
|
685
|
+
select(func.count(Validation.id)).where(
|
|
686
|
+
Validation.created_at < cutoff,
|
|
687
|
+
Validation.result_json.contains(f'"tag": "{tag}"'),
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
count = count_result.scalar() or 0
|
|
691
|
+
|
|
692
|
+
if count > 0:
|
|
693
|
+
await session.execute(
|
|
694
|
+
delete(Validation).where(
|
|
695
|
+
Validation.created_at < cutoff,
|
|
696
|
+
Validation.result_json.contains(f'"tag": "{tag}"'),
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
total_deleted += count
|
|
700
|
+
|
|
701
|
+
logger.info(f"Tag cleanup: deleted {count} validations with tag '{tag}'")
|
|
702
|
+
|
|
703
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
704
|
+
|
|
705
|
+
logger.info(f"Tag-based cleanup: deleted {total_deleted} total records")
|
|
706
|
+
|
|
707
|
+
return CleanupResult(
|
|
708
|
+
task_name=self.name,
|
|
709
|
+
records_deleted=total_deleted,
|
|
710
|
+
duration_ms=duration,
|
|
711
|
+
success=True,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
except Exception as e:
|
|
715
|
+
duration = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
|
716
|
+
logger.error(f"Tag-based cleanup failed: {e}")
|
|
717
|
+
return CleanupResult(
|
|
718
|
+
task_name=self.name,
|
|
719
|
+
duration_ms=duration,
|
|
720
|
+
success=False,
|
|
721
|
+
error=str(e),
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
303
725
|
@dataclass
|
|
304
726
|
class MaintenanceReport:
|
|
305
727
|
"""Report of a complete maintenance run.
|
|
@@ -385,11 +807,27 @@ class MaintenanceManager:
|
|
|
385
807
|
self._strategies.append(strategy)
|
|
386
808
|
|
|
387
809
|
def register_default_strategies(self) -> None:
|
|
388
|
-
"""Register all default cleanup strategies.
|
|
810
|
+
"""Register all default cleanup strategies.
|
|
811
|
+
|
|
812
|
+
Registers 6 types of cleanup strategies:
|
|
813
|
+
- Time-based: ValidationCleanupStrategy, NotificationLogCleanupStrategy
|
|
814
|
+
- Count-based: ProfileCleanupStrategy
|
|
815
|
+
- Size-based: SizeBasedCleanupStrategy
|
|
816
|
+
- Status-based: StatusBasedCleanupStrategy
|
|
817
|
+
- Tag-based: TagBasedCleanupStrategy
|
|
818
|
+
"""
|
|
389
819
|
self._strategies = [
|
|
820
|
+
# Time-based policies
|
|
390
821
|
ValidationCleanupStrategy(),
|
|
391
|
-
ProfileCleanupStrategy(),
|
|
392
822
|
NotificationLogCleanupStrategy(),
|
|
823
|
+
# Count-based policy
|
|
824
|
+
ProfileCleanupStrategy(),
|
|
825
|
+
# Size-based policy
|
|
826
|
+
SizeBasedCleanupStrategy(),
|
|
827
|
+
# Status-based policy
|
|
828
|
+
StatusBasedCleanupStrategy(),
|
|
829
|
+
# Tag-based policy
|
|
830
|
+
TagBasedCleanupStrategy(),
|
|
393
831
|
]
|
|
394
832
|
|
|
395
833
|
async def run_cleanup(self) -> MaintenanceReport:
|