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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,319 +1,131 @@
1
- """Main deduplication service.
1
+ """Dashboard-specific deduplication service using truthound.
2
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)
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 ...validation_limits import (
34
- ValidationLimitError,
35
- get_time_window_limits,
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
- Provides a convenient way to specify window durations with built-in
47
- validation to prevent DoS attacks from excessive window sizes.
20
+ class DashboardDeduplicationService:
21
+ """Dashboard-specific deduplication service.
48
22
 
49
- Validation Limits (configurable via environment variables):
50
- - Total seconds: 1 to 604800 (7 days)
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
- 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).
26
+ Example:
27
+ service = DashboardDeduplicationService()
28
+ await service.initialize_from_db(session)
62
29
 
63
- Raises:
64
- ValidationLimitError: If total duration exceeds limits.
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
- 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
- )
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 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,
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
- 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__(
55
+ def configure(
185
56
  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,
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
- """Initialize deduplicator.
64
+ """Configure the deduplication service.
193
65
 
194
66
  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.
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
- 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,
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
- 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,
103
+ self._deduplicator = NotificationDeduplicator(
104
+ store=InMemoryDeduplicationStore(),
105
+ config=self._config,
241
106
  )
242
107
 
243
108
  def is_duplicate(
244
109
  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,
110
+ checkpoint_result: Any,
111
+ action_type: str,
112
+ severity: str | None = None,
299
113
  ) -> bool:
300
- """Atomically check if duplicate and mark as sent if not.
114
+ """Check if a notification would be a duplicate.
301
115
 
302
116
  Args:
303
- fingerprint: The fingerprint to check.
304
- window: Optional window override.
305
- context: Optional context for strategy.
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 is NOT a duplicate (notification should be sent).
310
- False if this IS a duplicate (notification should be skipped).
122
+ True if this would be a duplicate notification.
311
123
  """
312
- if self.is_duplicate(fingerprint, window, context):
313
- return False
314
-
315
- self.mark_sent(fingerprint, metadata)
316
- return True
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
- "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"),
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
- 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.
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
- policy: Deduplication policy.
362
- window: Deduplication window.
151
+ db_config: Configuration dictionary from database.
363
152
 
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)
153
+ Returns:
154
+ DeduplicationConfig for truthound's deduplicator.
375
155
  """
376
- import functools
377
-
378
- # Create shared deduplicator
379
- deduplicator = NotificationDeduplicator(
380
- policy=policy,
381
- default_window=window or TimeWindow(minutes=5),
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