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
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Main deduplication service.
|
|
2
|
+
|
|
3
|
+
This module provides the NotificationDeduplicator service that
|
|
4
|
+
combines storage, strategies, and policies for complete
|
|
5
|
+
deduplication functionality.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
# Create deduplicator
|
|
9
|
+
deduplicator = NotificationDeduplicator(
|
|
10
|
+
store=InMemoryDeduplicationStore(),
|
|
11
|
+
default_window=TimeWindow(minutes=5),
|
|
12
|
+
policy=DeduplicationPolicy.SEVERITY,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Check and record
|
|
16
|
+
fingerprint = deduplicator.generate_fingerprint(
|
|
17
|
+
checkpoint_name="daily_check",
|
|
18
|
+
action_type="slack",
|
|
19
|
+
severity="high",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if not deduplicator.is_duplicate(fingerprint):
|
|
23
|
+
await send_notification()
|
|
24
|
+
deduplicator.mark_sent(fingerprint)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from ...validation_limits import (
|
|
34
|
+
ValidationLimitError,
|
|
35
|
+
get_time_window_limits,
|
|
36
|
+
)
|
|
37
|
+
from .policies import DeduplicationPolicy, FingerprintConfig, FingerprintGenerator
|
|
38
|
+
from .stores import BaseDeduplicationStore, InMemoryDeduplicationStore
|
|
39
|
+
from .strategies import BaseWindowStrategy, SlidingWindowStrategy
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class TimeWindow:
|
|
44
|
+
"""Time window configuration with validation.
|
|
45
|
+
|
|
46
|
+
Provides a convenient way to specify window durations with built-in
|
|
47
|
+
validation to prevent DoS attacks from excessive window sizes.
|
|
48
|
+
|
|
49
|
+
Validation Limits (configurable via environment variables):
|
|
50
|
+
- Total seconds: 1 to 604800 (7 days)
|
|
51
|
+
- Individual components must be non-negative
|
|
52
|
+
|
|
53
|
+
Environment Variables:
|
|
54
|
+
- TRUTHOUND_TIMEWINDOW_MIN: Minimum total duration (default: 1)
|
|
55
|
+
- TRUTHOUND_TIMEWINDOW_MAX: Maximum total duration (default: 604800)
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
seconds: Additional seconds (default: 0).
|
|
59
|
+
minutes: Additional minutes (default: 0).
|
|
60
|
+
hours: Additional hours (default: 0).
|
|
61
|
+
days: Additional days (default: 0).
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValidationLimitError: If total duration exceeds limits.
|
|
65
|
+
ValueError: If any component is negative.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
seconds: int = 0
|
|
69
|
+
minutes: int = 0
|
|
70
|
+
hours: int = 0
|
|
71
|
+
days: int = 0
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
"""Validate window configuration after initialization.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If any component is negative.
|
|
78
|
+
ValidationLimitError: If total duration exceeds limits.
|
|
79
|
+
"""
|
|
80
|
+
# Validate non-negative values
|
|
81
|
+
if self.seconds < 0:
|
|
82
|
+
raise ValueError(f"seconds must be non-negative, got {self.seconds}")
|
|
83
|
+
if self.minutes < 0:
|
|
84
|
+
raise ValueError(f"minutes must be non-negative, got {self.minutes}")
|
|
85
|
+
if self.hours < 0:
|
|
86
|
+
raise ValueError(f"hours must be non-negative, got {self.hours}")
|
|
87
|
+
if self.days < 0:
|
|
88
|
+
raise ValueError(f"days must be non-negative, got {self.days}")
|
|
89
|
+
|
|
90
|
+
# Validate total duration against limits
|
|
91
|
+
total = self.total_seconds
|
|
92
|
+
limits = get_time_window_limits()
|
|
93
|
+
valid, error = limits.validate_total_seconds(total)
|
|
94
|
+
if not valid:
|
|
95
|
+
raise ValidationLimitError(
|
|
96
|
+
error or f"Invalid total duration: {total}",
|
|
97
|
+
parameter="total_seconds",
|
|
98
|
+
value=total,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def total_seconds(self) -> int:
|
|
103
|
+
"""Get total duration in seconds."""
|
|
104
|
+
return (
|
|
105
|
+
self.seconds
|
|
106
|
+
+ self.minutes * 60
|
|
107
|
+
+ self.hours * 3600
|
|
108
|
+
+ self.days * 86400
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_seconds(cls, seconds: int) -> "TimeWindow":
|
|
113
|
+
"""Create from seconds with validation.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
seconds: Total duration in seconds.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
TimeWindow instance.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValidationLimitError: If seconds exceeds limits.
|
|
123
|
+
ValueError: If seconds is negative.
|
|
124
|
+
"""
|
|
125
|
+
if seconds < 0:
|
|
126
|
+
raise ValueError(f"seconds must be non-negative, got {seconds}")
|
|
127
|
+
|
|
128
|
+
# Validate against limits before creating
|
|
129
|
+
limits = get_time_window_limits()
|
|
130
|
+
valid, error = limits.validate_total_seconds(seconds)
|
|
131
|
+
if not valid:
|
|
132
|
+
raise ValidationLimitError(
|
|
133
|
+
error or f"Invalid duration: {seconds}",
|
|
134
|
+
parameter="seconds",
|
|
135
|
+
value=seconds,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return cls(seconds=seconds)
|
|
139
|
+
|
|
140
|
+
def __repr__(self) -> str:
|
|
141
|
+
parts = []
|
|
142
|
+
if self.days:
|
|
143
|
+
parts.append(f"{self.days}d")
|
|
144
|
+
if self.hours:
|
|
145
|
+
parts.append(f"{self.hours}h")
|
|
146
|
+
if self.minutes:
|
|
147
|
+
parts.append(f"{self.minutes}m")
|
|
148
|
+
if self.seconds:
|
|
149
|
+
parts.append(f"{self.seconds}s")
|
|
150
|
+
return f"TimeWindow({' '.join(parts) or '0s'})"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class NotificationDeduplicator:
|
|
154
|
+
"""Main deduplication service.
|
|
155
|
+
|
|
156
|
+
Provides complete deduplication functionality by combining:
|
|
157
|
+
- Storage backend for tracking sent notifications
|
|
158
|
+
- Window strategy for calculating deduplication windows
|
|
159
|
+
- Fingerprint policy for generating unique identifiers
|
|
160
|
+
|
|
161
|
+
Thread-safe for concurrent use.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
deduplicator = NotificationDeduplicator(
|
|
165
|
+
store=SQLiteDeduplicationStore("dedup.db"),
|
|
166
|
+
default_window=TimeWindow(minutes=5),
|
|
167
|
+
policy=DeduplicationPolicy.SEVERITY,
|
|
168
|
+
strategy=AdaptiveWindowStrategy(),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Generate fingerprint
|
|
172
|
+
fp = deduplicator.generate_fingerprint(
|
|
173
|
+
checkpoint_name="check1",
|
|
174
|
+
action_type="slack",
|
|
175
|
+
severity="high",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Check if duplicate
|
|
179
|
+
if not deduplicator.is_duplicate(fp):
|
|
180
|
+
send_notification()
|
|
181
|
+
deduplicator.mark_sent(fp)
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
store: BaseDeduplicationStore | None = None,
|
|
187
|
+
default_window: TimeWindow | None = None,
|
|
188
|
+
policy: DeduplicationPolicy = DeduplicationPolicy.BASIC,
|
|
189
|
+
strategy: BaseWindowStrategy | None = None,
|
|
190
|
+
fingerprint_config: FingerprintConfig | None = None,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Initialize deduplicator.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
store: Storage backend (default: InMemoryDeduplicationStore).
|
|
196
|
+
default_window: Default deduplication window.
|
|
197
|
+
policy: Fingerprint policy.
|
|
198
|
+
strategy: Window strategy (default: SlidingWindowStrategy).
|
|
199
|
+
fingerprint_config: Custom fingerprint config.
|
|
200
|
+
"""
|
|
201
|
+
self.store = store or InMemoryDeduplicationStore()
|
|
202
|
+
self.default_window = default_window or TimeWindow(minutes=5)
|
|
203
|
+
self.policy = policy
|
|
204
|
+
self.strategy = strategy or SlidingWindowStrategy(
|
|
205
|
+
window_seconds=self.default_window.total_seconds
|
|
206
|
+
)
|
|
207
|
+
self.fingerprint_generator = FingerprintGenerator(
|
|
208
|
+
policy=policy,
|
|
209
|
+
config=fingerprint_config,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def generate_fingerprint(
|
|
213
|
+
self,
|
|
214
|
+
checkpoint_name: str | None = None,
|
|
215
|
+
action_type: str | None = None,
|
|
216
|
+
severity: str | None = None,
|
|
217
|
+
issues: list[dict[str, Any]] | None = None,
|
|
218
|
+
timestamp: datetime | None = None,
|
|
219
|
+
**custom_fields: Any,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""Generate a deduplication fingerprint.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
checkpoint_name: Name of checkpoint/source.
|
|
225
|
+
action_type: Type of notification channel.
|
|
226
|
+
severity: Severity level.
|
|
227
|
+
issues: List of issues.
|
|
228
|
+
timestamp: Event timestamp.
|
|
229
|
+
**custom_fields: Additional fields.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Generated fingerprint string.
|
|
233
|
+
"""
|
|
234
|
+
return self.fingerprint_generator.generate(
|
|
235
|
+
checkpoint_name=checkpoint_name,
|
|
236
|
+
action_type=action_type,
|
|
237
|
+
severity=severity,
|
|
238
|
+
issues=issues,
|
|
239
|
+
timestamp=timestamp,
|
|
240
|
+
**custom_fields,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def is_duplicate(
|
|
244
|
+
self,
|
|
245
|
+
fingerprint: str,
|
|
246
|
+
window: TimeWindow | None = None,
|
|
247
|
+
context: dict[str, Any] | None = None,
|
|
248
|
+
) -> bool:
|
|
249
|
+
"""Check if a fingerprint is a duplicate.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
fingerprint: The fingerprint to check.
|
|
253
|
+
window: Optional window override.
|
|
254
|
+
context: Optional context for strategy.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if this is a duplicate notification.
|
|
258
|
+
"""
|
|
259
|
+
# Get window duration from strategy
|
|
260
|
+
window_seconds = self.strategy.get_window_seconds(fingerprint, context)
|
|
261
|
+
|
|
262
|
+
# Override with explicit window if provided
|
|
263
|
+
if window is not None:
|
|
264
|
+
window_seconds = window.total_seconds
|
|
265
|
+
|
|
266
|
+
# Use default if strategy returns 0
|
|
267
|
+
if window_seconds <= 0:
|
|
268
|
+
window_seconds = self.default_window.total_seconds
|
|
269
|
+
|
|
270
|
+
# Get window-aligned key
|
|
271
|
+
window_key = self.strategy.get_window_key(fingerprint)
|
|
272
|
+
|
|
273
|
+
# Check store
|
|
274
|
+
return self.store.exists(window_key, window_seconds)
|
|
275
|
+
|
|
276
|
+
def mark_sent(
|
|
277
|
+
self,
|
|
278
|
+
fingerprint: str,
|
|
279
|
+
metadata: dict[str, Any] | None = None,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Mark a fingerprint as sent.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
fingerprint: The fingerprint to record.
|
|
285
|
+
metadata: Optional metadata to store.
|
|
286
|
+
"""
|
|
287
|
+
# Get window-aligned key
|
|
288
|
+
window_key = self.strategy.get_window_key(fingerprint)
|
|
289
|
+
|
|
290
|
+
# Record in store
|
|
291
|
+
self.store.record(window_key, metadata)
|
|
292
|
+
|
|
293
|
+
def check_and_mark(
|
|
294
|
+
self,
|
|
295
|
+
fingerprint: str,
|
|
296
|
+
window: TimeWindow | None = None,
|
|
297
|
+
context: dict[str, Any] | None = None,
|
|
298
|
+
metadata: dict[str, Any] | None = None,
|
|
299
|
+
) -> bool:
|
|
300
|
+
"""Atomically check if duplicate and mark as sent if not.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
fingerprint: The fingerprint to check.
|
|
304
|
+
window: Optional window override.
|
|
305
|
+
context: Optional context for strategy.
|
|
306
|
+
metadata: Optional metadata to store.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if this is NOT a duplicate (notification should be sent).
|
|
310
|
+
False if this IS a duplicate (notification should be skipped).
|
|
311
|
+
"""
|
|
312
|
+
if self.is_duplicate(fingerprint, window, context):
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
self.mark_sent(fingerprint, metadata)
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def get_stats(self) -> dict[str, Any]:
|
|
319
|
+
"""Get deduplication statistics.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Dictionary with statistics.
|
|
323
|
+
"""
|
|
324
|
+
return {
|
|
325
|
+
"total_entries": self.store.count(),
|
|
326
|
+
"policy": self.policy.value,
|
|
327
|
+
"default_window_seconds": self.default_window.total_seconds,
|
|
328
|
+
"strategy_type": getattr(self.strategy, "strategy_type", "unknown"),
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
def cleanup(self, max_age: TimeWindow | None = None) -> int:
|
|
332
|
+
"""Remove expired entries.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
max_age: Maximum age of entries to keep.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Number of entries removed.
|
|
339
|
+
"""
|
|
340
|
+
if max_age is None:
|
|
341
|
+
# Default to 24 hours
|
|
342
|
+
max_age = TimeWindow(hours=24)
|
|
343
|
+
|
|
344
|
+
return self.store.cleanup(max_age.total_seconds)
|
|
345
|
+
|
|
346
|
+
def clear(self) -> None:
|
|
347
|
+
"""Clear all deduplication state."""
|
|
348
|
+
self.store.clear()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def deduplicated(
|
|
352
|
+
policy: DeduplicationPolicy = DeduplicationPolicy.BASIC,
|
|
353
|
+
window: TimeWindow | None = None,
|
|
354
|
+
):
|
|
355
|
+
"""Decorator for deduplicating async functions.
|
|
356
|
+
|
|
357
|
+
Creates a deduplicator and checks for duplicates before
|
|
358
|
+
executing the decorated function.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
policy: Deduplication policy.
|
|
362
|
+
window: Deduplication window.
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
@deduplicated(
|
|
366
|
+
policy=DeduplicationPolicy.SEVERITY,
|
|
367
|
+
window=TimeWindow(minutes=10),
|
|
368
|
+
)
|
|
369
|
+
async def send_slack_notification(
|
|
370
|
+
checkpoint_name: str,
|
|
371
|
+
severity: str,
|
|
372
|
+
message: str,
|
|
373
|
+
):
|
|
374
|
+
await slack.post(message)
|
|
375
|
+
"""
|
|
376
|
+
import functools
|
|
377
|
+
|
|
378
|
+
# Create shared deduplicator
|
|
379
|
+
deduplicator = NotificationDeduplicator(
|
|
380
|
+
policy=policy,
|
|
381
|
+
default_window=window or TimeWindow(minutes=5),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def decorator(func):
|
|
385
|
+
@functools.wraps(func)
|
|
386
|
+
async def wrapper(*args, **kwargs):
|
|
387
|
+
# Generate fingerprint from kwargs
|
|
388
|
+
fingerprint = deduplicator.generate_fingerprint(**kwargs)
|
|
389
|
+
|
|
390
|
+
# Check and mark
|
|
391
|
+
if not deduplicator.check_and_mark(fingerprint):
|
|
392
|
+
# Duplicate - skip execution
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
# Execute function
|
|
396
|
+
return await func(*args, **kwargs)
|
|
397
|
+
|
|
398
|
+
return wrapper
|
|
399
|
+
|
|
400
|
+
return decorator
|