truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +437 -10
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +11 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,319 +1,131 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Dashboard-specific deduplication service using truthound.
|
|
2
2
|
|
|
3
|
-
This module provides
|
|
4
|
-
|
|
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)
|
|
3
|
+
This module provides adapters that integrate truthound's deduplication
|
|
4
|
+
system with the Dashboard's database configuration.
|
|
25
5
|
"""
|
|
26
6
|
|
|
27
7
|
from __future__ import annotations
|
|
28
8
|
|
|
29
|
-
from dataclasses import dataclass
|
|
30
|
-
from datetime import datetime
|
|
31
9
|
from typing import Any
|
|
32
10
|
|
|
33
|
-
from
|
|
34
|
-
|
|
35
|
-
|
|
11
|
+
from truthound.checkpoint.deduplication import (
|
|
12
|
+
NotificationDeduplicator,
|
|
13
|
+
DeduplicationConfig,
|
|
14
|
+
InMemoryDeduplicationStore,
|
|
15
|
+
TimeWindow,
|
|
16
|
+
DeduplicationPolicy,
|
|
36
17
|
)
|
|
37
|
-
from .policies import DeduplicationPolicy, FingerprintConfig, FingerprintGenerator
|
|
38
|
-
from .stores import BaseDeduplicationStore, InMemoryDeduplicationStore
|
|
39
|
-
from .strategies import BaseWindowStrategy, SlidingWindowStrategy
|
|
40
|
-
|
|
41
18
|
|
|
42
|
-
@dataclass
|
|
43
|
-
class TimeWindow:
|
|
44
|
-
"""Time window configuration with validation.
|
|
45
19
|
|
|
46
|
-
|
|
47
|
-
|
|
20
|
+
class DashboardDeduplicationService:
|
|
21
|
+
"""Dashboard-specific deduplication service.
|
|
48
22
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- Individual components must be non-negative
|
|
23
|
+
Wraps truthound's NotificationDeduplicator and provides integration
|
|
24
|
+
with the Dashboard's database configuration.
|
|
52
25
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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).
|
|
26
|
+
Example:
|
|
27
|
+
service = DashboardDeduplicationService()
|
|
28
|
+
await service.initialize_from_db(session)
|
|
62
29
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
ValueError: If any component is negative.
|
|
30
|
+
if not service.is_duplicate(checkpoint_result, "slack"):
|
|
31
|
+
await send_notification()
|
|
66
32
|
"""
|
|
67
33
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
)
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
"""Initialize the service with default configuration."""
|
|
36
|
+
self._deduplicator: NotificationDeduplicator | None = None
|
|
37
|
+
self._config: DeduplicationConfig | None = None
|
|
100
38
|
|
|
101
39
|
@property
|
|
102
|
-
def
|
|
103
|
-
"""Get
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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,
|
|
40
|
+
def deduplicator(self) -> NotificationDeduplicator:
|
|
41
|
+
"""Get the underlying truthound deduplicator."""
|
|
42
|
+
if self._deduplicator is None:
|
|
43
|
+
# Create with default config
|
|
44
|
+
self._config = DeduplicationConfig(
|
|
45
|
+
enabled=True,
|
|
46
|
+
policy=DeduplicationPolicy.SEVERITY,
|
|
47
|
+
default_window=TimeWindow(minutes=5),
|
|
136
48
|
)
|
|
49
|
+
self._deduplicator = NotificationDeduplicator(
|
|
50
|
+
store=InMemoryDeduplicationStore(),
|
|
51
|
+
config=self._config,
|
|
52
|
+
)
|
|
53
|
+
return self._deduplicator
|
|
137
54
|
|
|
138
|
-
|
|
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__(
|
|
55
|
+
def configure(
|
|
185
56
|
self,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
policy:
|
|
189
|
-
|
|
190
|
-
|
|
57
|
+
*,
|
|
58
|
+
enabled: bool = True,
|
|
59
|
+
policy: str = "severity",
|
|
60
|
+
default_window_seconds: int = 300,
|
|
61
|
+
action_windows: dict[str, int] | None = None,
|
|
62
|
+
severity_windows: dict[str, int] | None = None,
|
|
191
63
|
) -> None:
|
|
192
|
-
"""
|
|
64
|
+
"""Configure the deduplication service.
|
|
193
65
|
|
|
194
66
|
Args:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
67
|
+
enabled: Whether deduplication is enabled.
|
|
68
|
+
policy: Policy name (none, basic, severity, issue_based, strict).
|
|
69
|
+
default_window_seconds: Default window in seconds.
|
|
70
|
+
action_windows: Per-action windows in seconds.
|
|
71
|
+
severity_windows: Per-severity windows in seconds.
|
|
200
72
|
"""
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
73
|
+
# Map policy string to enum
|
|
74
|
+
policy_map = {
|
|
75
|
+
"none": DeduplicationPolicy.NONE,
|
|
76
|
+
"basic": DeduplicationPolicy.BASIC,
|
|
77
|
+
"severity": DeduplicationPolicy.SEVERITY,
|
|
78
|
+
"issue_based": DeduplicationPolicy.ISSUE_BASED,
|
|
79
|
+
"strict": DeduplicationPolicy.STRICT,
|
|
80
|
+
}
|
|
81
|
+
policy_enum = policy_map.get(policy.lower(), DeduplicationPolicy.SEVERITY)
|
|
82
|
+
|
|
83
|
+
# Convert action windows
|
|
84
|
+
action_window_map = {}
|
|
85
|
+
if action_windows:
|
|
86
|
+
for action_type, seconds in action_windows.items():
|
|
87
|
+
action_window_map[action_type] = TimeWindow(seconds=seconds)
|
|
88
|
+
|
|
89
|
+
# Convert severity windows
|
|
90
|
+
severity_window_map = {}
|
|
91
|
+
if severity_windows:
|
|
92
|
+
for severity, seconds in severity_windows.items():
|
|
93
|
+
severity_window_map[severity] = TimeWindow(seconds=seconds)
|
|
94
|
+
|
|
95
|
+
self._config = DeduplicationConfig(
|
|
96
|
+
enabled=enabled,
|
|
97
|
+
policy=policy_enum,
|
|
98
|
+
default_window=TimeWindow(seconds=default_window_seconds),
|
|
99
|
+
action_windows=action_window_map,
|
|
100
|
+
severity_windows=severity_window_map,
|
|
210
101
|
)
|
|
211
102
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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,
|
|
103
|
+
self._deduplicator = NotificationDeduplicator(
|
|
104
|
+
store=InMemoryDeduplicationStore(),
|
|
105
|
+
config=self._config,
|
|
241
106
|
)
|
|
242
107
|
|
|
243
108
|
def is_duplicate(
|
|
244
109
|
self,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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,
|
|
110
|
+
checkpoint_result: Any,
|
|
111
|
+
action_type: str,
|
|
112
|
+
severity: str | None = None,
|
|
299
113
|
) -> bool:
|
|
300
|
-
"""
|
|
114
|
+
"""Check if a notification would be a duplicate.
|
|
301
115
|
|
|
302
116
|
Args:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
metadata: Optional metadata to store.
|
|
117
|
+
checkpoint_result: The checkpoint result object.
|
|
118
|
+
action_type: The action type (slack, email, etc.).
|
|
119
|
+
severity: Optional severity level.
|
|
307
120
|
|
|
308
121
|
Returns:
|
|
309
|
-
True if this
|
|
310
|
-
False if this IS a duplicate (notification should be skipped).
|
|
122
|
+
True if this would be a duplicate notification.
|
|
311
123
|
"""
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
124
|
+
return self.deduplicator.is_duplicate(
|
|
125
|
+
checkpoint_result,
|
|
126
|
+
action_type,
|
|
127
|
+
severity=severity,
|
|
128
|
+
)
|
|
317
129
|
|
|
318
130
|
def get_stats(self) -> dict[str, Any]:
|
|
319
131
|
"""Get deduplication statistics.
|
|
@@ -321,80 +133,51 @@ class NotificationDeduplicator:
|
|
|
321
133
|
Returns:
|
|
322
134
|
Dictionary with statistics.
|
|
323
135
|
"""
|
|
136
|
+
stats = self.deduplicator.get_stats()
|
|
324
137
|
return {
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
138
|
+
"total_evaluated": stats.total_evaluated,
|
|
139
|
+
"suppressed": stats.suppressed,
|
|
140
|
+
"suppression_ratio": stats.suppression_ratio,
|
|
141
|
+
"active_fingerprints": stats.active_fingerprints,
|
|
329
142
|
}
|
|
330
143
|
|
|
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
144
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
):
|
|
355
|
-
"""Decorator for deduplicating async functions.
|
|
356
|
-
|
|
357
|
-
Creates a deduplicator and checks for duplicates before
|
|
358
|
-
executing the decorated function.
|
|
145
|
+
def create_deduplication_config_from_db(
|
|
146
|
+
db_config: dict[str, Any],
|
|
147
|
+
) -> DeduplicationConfig:
|
|
148
|
+
"""Create a DeduplicationConfig from database configuration.
|
|
359
149
|
|
|
360
150
|
Args:
|
|
361
|
-
|
|
362
|
-
window: Deduplication window.
|
|
151
|
+
db_config: Configuration dictionary from database.
|
|
363
152
|
|
|
364
|
-
|
|
365
|
-
|
|
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)
|
|
153
|
+
Returns:
|
|
154
|
+
DeduplicationConfig for truthound's deduplicator.
|
|
375
155
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
156
|
+
policy_map = {
|
|
157
|
+
"none": DeduplicationPolicy.NONE,
|
|
158
|
+
"basic": DeduplicationPolicy.BASIC,
|
|
159
|
+
"severity": DeduplicationPolicy.SEVERITY,
|
|
160
|
+
"issue_based": DeduplicationPolicy.ISSUE_BASED,
|
|
161
|
+
"strict": DeduplicationPolicy.STRICT,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
policy = db_config.get("policy", "severity")
|
|
165
|
+
policy_enum = policy_map.get(policy.lower(), DeduplicationPolicy.SEVERITY)
|
|
166
|
+
|
|
167
|
+
# Build action windows
|
|
168
|
+
action_windows = {}
|
|
169
|
+
for action_type, seconds in db_config.get("action_windows", {}).items():
|
|
170
|
+
action_windows[action_type] = TimeWindow(seconds=seconds)
|
|
171
|
+
|
|
172
|
+
# Build severity windows
|
|
173
|
+
severity_windows = {}
|
|
174
|
+
for severity, seconds in db_config.get("severity_windows", {}).items():
|
|
175
|
+
severity_windows[severity] = TimeWindow(seconds=seconds)
|
|
176
|
+
|
|
177
|
+
return DeduplicationConfig(
|
|
178
|
+
enabled=db_config.get("enabled", True),
|
|
179
|
+
policy=policy_enum,
|
|
180
|
+
default_window=TimeWindow(seconds=db_config.get("window_seconds", 300)),
|
|
181
|
+
action_windows=action_windows,
|
|
182
|
+
severity_windows=severity_windows,
|
|
382
183
|
)
|
|
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
|