truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,143 @@
1
+ """Notification deduplication system.
2
+
3
+ This module provides a flexible deduplication system to prevent
4
+ sending duplicate notifications within configurable time windows.
5
+
6
+ Features:
7
+ - 4 window strategies (sliding, tumbling, session, adaptive)
8
+ - 6 deduplication policies (none, basic, severity, issue_based, strict, custom)
9
+ - Pluggable storage backends (in-memory, SQLite, Redis, Redis Streams)
10
+ - Fingerprint-based duplicate detection
11
+ - Factory function for easy store creation
12
+
13
+ Example:
14
+ from truthound_dashboard.core.notifications.deduplication import (
15
+ NotificationDeduplicator,
16
+ InMemoryDeduplicationStore,
17
+ DeduplicationPolicy,
18
+ TimeWindow,
19
+ )
20
+
21
+ # Create deduplicator
22
+ deduplicator = NotificationDeduplicator(
23
+ store=InMemoryDeduplicationStore(),
24
+ default_window=TimeWindow(seconds=300),
25
+ policy=DeduplicationPolicy.SEVERITY,
26
+ )
27
+
28
+ # Generate fingerprint and check
29
+ fingerprint = deduplicator.generate_fingerprint(
30
+ checkpoint_name="daily_check",
31
+ action_type="slack",
32
+ severity="high",
33
+ )
34
+
35
+ if not deduplicator.is_duplicate(fingerprint):
36
+ await send_notification()
37
+ deduplicator.mark_sent(fingerprint)
38
+
39
+ Redis Store Example:
40
+ from truthound_dashboard.core.notifications.deduplication import (
41
+ RedisDeduplicationStore,
42
+ REDIS_AVAILABLE,
43
+ )
44
+
45
+ if REDIS_AVAILABLE:
46
+ store = RedisDeduplicationStore(
47
+ redis_url="redis://localhost:6379/0",
48
+ key_prefix="myapp:dedup:",
49
+ default_ttl=3600,
50
+ )
51
+ # Use store with NotificationDeduplicator
52
+
53
+ Redis Streams Store Example (Production):
54
+ from truthound_dashboard.core.notifications.deduplication import (
55
+ RedisStreamsDeduplicationStore,
56
+ create_deduplication_store,
57
+ DeduplicationStoreType,
58
+ REDIS_AVAILABLE,
59
+ )
60
+
61
+ # Using factory function (recommended)
62
+ store = create_deduplication_store("redis_streams")
63
+
64
+ # Or direct instantiation with full configuration
65
+ if REDIS_AVAILABLE:
66
+ store = RedisStreamsDeduplicationStore(
67
+ redis_url="redis://myredis:6379/1",
68
+ default_ttl=7200,
69
+ max_connections=20,
70
+ enable_fallback=True, # Falls back to InMemory on Redis failure
71
+ )
72
+
73
+ # Health check
74
+ health = await store.health_check_async()
75
+ print(f"Healthy: {health['healthy']}, Mode: {health.get('mode')}")
76
+
77
+ # Get metrics
78
+ metrics = store.get_metrics()
79
+ print(f"Hit rate: {metrics['hit_rate']}%")
80
+
81
+ Factory Function Example:
82
+ from truthound_dashboard.core.notifications.deduplication import (
83
+ create_deduplication_store,
84
+ )
85
+
86
+ # Auto-detect from environment variables
87
+ store = create_deduplication_store()
88
+
89
+ # Explicit type with configuration
90
+ store = create_deduplication_store(
91
+ "redis_streams",
92
+ default_ttl=3600,
93
+ enable_fallback=True,
94
+ )
95
+ """
96
+
97
+ from .policies import DeduplicationPolicy, FingerprintGenerator
98
+ from .service import NotificationDeduplicator, TimeWindow
99
+ from .stores import (
100
+ REDIS_AVAILABLE,
101
+ BaseDeduplicationStore,
102
+ DeduplicationMetrics,
103
+ DeduplicationStoreType,
104
+ InMemoryDeduplicationStore,
105
+ RedisDeduplicationStore,
106
+ RedisStreamsDeduplicationStore,
107
+ SQLiteDeduplicationStore,
108
+ create_deduplication_store,
109
+ )
110
+ from .strategies import (
111
+ AdaptiveWindowStrategy,
112
+ BaseWindowStrategy,
113
+ SessionWindowStrategy,
114
+ SlidingWindowStrategy,
115
+ StrategyRegistry,
116
+ TumblingWindowStrategy,
117
+ )
118
+
119
+ __all__ = [
120
+ # Stores
121
+ "BaseDeduplicationStore",
122
+ "InMemoryDeduplicationStore",
123
+ "SQLiteDeduplicationStore",
124
+ "RedisDeduplicationStore",
125
+ "RedisStreamsDeduplicationStore",
126
+ "DeduplicationMetrics",
127
+ "DeduplicationStoreType",
128
+ "create_deduplication_store",
129
+ "REDIS_AVAILABLE",
130
+ # Strategies
131
+ "BaseWindowStrategy",
132
+ "SlidingWindowStrategy",
133
+ "TumblingWindowStrategy",
134
+ "SessionWindowStrategy",
135
+ "AdaptiveWindowStrategy",
136
+ "StrategyRegistry",
137
+ # Policies
138
+ "DeduplicationPolicy",
139
+ "FingerprintGenerator",
140
+ # Service
141
+ "NotificationDeduplicator",
142
+ "TimeWindow",
143
+ ]
@@ -0,0 +1,274 @@
1
+ """Deduplication policies and fingerprint generation.
2
+
3
+ This module provides policies that determine what fields are
4
+ included in the deduplication fingerprint.
5
+
6
+ Policies:
7
+ - NONE: No deduplication (testing)
8
+ - BASIC: checkpoint_name + action_type
9
+ - SEVERITY: + severity level
10
+ - ISSUE_BASED: + issue hash
11
+ - STRICT: + timestamp bucket
12
+ - CUSTOM: User-defined fields
13
+
14
+ Different policies trade off between:
15
+ - Granularity: How specific the deduplication is
16
+ - Suppression: How many notifications are blocked
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from enum import Enum
25
+ from typing import Any
26
+
27
+
28
+ class DeduplicationPolicy(str, Enum):
29
+ """Deduplication policy types.
30
+
31
+ Defines what fields are included in the fingerprint.
32
+ """
33
+
34
+ # No deduplication - every event is unique
35
+ NONE = "none"
36
+
37
+ # Basic: checkpoint name + action type
38
+ BASIC = "basic"
39
+
40
+ # Severity: basic + severity level
41
+ SEVERITY = "severity"
42
+
43
+ # Issue-based: basic + issue content hash
44
+ ISSUE_BASED = "issue_based"
45
+
46
+ # Strict: basic + timestamp bucket
47
+ STRICT = "strict"
48
+
49
+ # Custom: user-defined fields
50
+ CUSTOM = "custom"
51
+
52
+
53
+ @dataclass
54
+ class FingerprintConfig:
55
+ """Configuration for fingerprint generation.
56
+
57
+ Attributes:
58
+ include_checkpoint: Include checkpoint/source name.
59
+ include_action_type: Include action/channel type.
60
+ include_severity: Include severity level.
61
+ include_issue_hash: Include hash of issue details.
62
+ include_timestamp_bucket: Include timestamp bucket.
63
+ timestamp_bucket_seconds: Size of timestamp buckets.
64
+ custom_fields: Additional fields to include.
65
+ """
66
+
67
+ include_checkpoint: bool = True
68
+ include_action_type: bool = True
69
+ include_severity: bool = False
70
+ include_issue_hash: bool = False
71
+ include_timestamp_bucket: bool = False
72
+ timestamp_bucket_seconds: int = 300
73
+ custom_fields: list[str] = field(default_factory=list)
74
+
75
+ @classmethod
76
+ def from_policy(cls, policy: DeduplicationPolicy) -> "FingerprintConfig":
77
+ """Create config from policy preset.
78
+
79
+ Args:
80
+ policy: Deduplication policy.
81
+
82
+ Returns:
83
+ Configured FingerprintConfig.
84
+ """
85
+ if policy == DeduplicationPolicy.NONE:
86
+ return cls(
87
+ include_checkpoint=False,
88
+ include_action_type=False,
89
+ )
90
+
91
+ elif policy == DeduplicationPolicy.BASIC:
92
+ return cls(
93
+ include_checkpoint=True,
94
+ include_action_type=True,
95
+ )
96
+
97
+ elif policy == DeduplicationPolicy.SEVERITY:
98
+ return cls(
99
+ include_checkpoint=True,
100
+ include_action_type=True,
101
+ include_severity=True,
102
+ )
103
+
104
+ elif policy == DeduplicationPolicy.ISSUE_BASED:
105
+ return cls(
106
+ include_checkpoint=True,
107
+ include_action_type=True,
108
+ include_issue_hash=True,
109
+ )
110
+
111
+ elif policy == DeduplicationPolicy.STRICT:
112
+ return cls(
113
+ include_checkpoint=True,
114
+ include_action_type=True,
115
+ include_severity=True,
116
+ include_timestamp_bucket=True,
117
+ )
118
+
119
+ elif policy == DeduplicationPolicy.CUSTOM:
120
+ # Custom requires explicit configuration
121
+ return cls()
122
+
123
+ return cls()
124
+
125
+
126
+ class FingerprintGenerator:
127
+ """Generates deduplication fingerprints.
128
+
129
+ The fingerprint uniquely identifies a notification for
130
+ deduplication purposes. Two events with the same fingerprint
131
+ within the deduplication window will be considered duplicates.
132
+
133
+ Example:
134
+ generator = FingerprintGenerator(
135
+ policy=DeduplicationPolicy.SEVERITY
136
+ )
137
+
138
+ fingerprint = generator.generate(
139
+ checkpoint_name="daily_check",
140
+ action_type="slack",
141
+ severity="high",
142
+ )
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ policy: DeduplicationPolicy = DeduplicationPolicy.BASIC,
148
+ config: FingerprintConfig | None = None,
149
+ ) -> None:
150
+ """Initialize fingerprint generator.
151
+
152
+ Args:
153
+ policy: Deduplication policy to use.
154
+ config: Optional explicit configuration (overrides policy).
155
+ """
156
+ self.policy = policy
157
+ self.config = config or FingerprintConfig.from_policy(policy)
158
+
159
+ def generate(
160
+ self,
161
+ checkpoint_name: str | None = None,
162
+ action_type: str | None = None,
163
+ severity: str | None = None,
164
+ issues: list[dict[str, Any]] | None = None,
165
+ timestamp: datetime | None = None,
166
+ **custom_fields: Any,
167
+ ) -> str:
168
+ """Generate a fingerprint from event data.
169
+
170
+ Args:
171
+ checkpoint_name: Name of checkpoint/source.
172
+ action_type: Type of notification action.
173
+ severity: Severity level.
174
+ issues: List of issues for hashing.
175
+ timestamp: Event timestamp.
176
+ **custom_fields: Additional fields for custom policy.
177
+
178
+ Returns:
179
+ Generated fingerprint string.
180
+ """
181
+ if self.policy == DeduplicationPolicy.NONE:
182
+ # Unique fingerprint for each event
183
+ return self._random_fingerprint()
184
+
185
+ parts: list[str] = []
186
+
187
+ # Add configured fields
188
+ if self.config.include_checkpoint and checkpoint_name:
189
+ parts.append(f"cp:{checkpoint_name}")
190
+
191
+ if self.config.include_action_type and action_type:
192
+ parts.append(f"act:{action_type}")
193
+
194
+ if self.config.include_severity and severity:
195
+ parts.append(f"sev:{severity}")
196
+
197
+ if self.config.include_issue_hash and issues:
198
+ issue_hash = self._hash_issues(issues)
199
+ parts.append(f"ish:{issue_hash}")
200
+
201
+ if self.config.include_timestamp_bucket:
202
+ bucket = self._get_timestamp_bucket(timestamp)
203
+ parts.append(f"ts:{bucket}")
204
+
205
+ # Add custom fields
206
+ for field_name in self.config.custom_fields:
207
+ if field_name in custom_fields:
208
+ value = custom_fields[field_name]
209
+ parts.append(f"{field_name}:{value}")
210
+
211
+ # Also add any extra custom fields passed
212
+ for field_name, value in custom_fields.items():
213
+ if field_name not in self.config.custom_fields:
214
+ parts.append(f"{field_name}:{value}")
215
+
216
+ if not parts:
217
+ # Fallback to random if no parts
218
+ return self._random_fingerprint()
219
+
220
+ # Join parts and hash
221
+ combined = "|".join(sorted(parts))
222
+ return hashlib.sha256(combined.encode()).hexdigest()[:32]
223
+
224
+ def _hash_issues(self, issues: list[dict[str, Any]]) -> str:
225
+ """Generate hash from issue details."""
226
+ import json
227
+
228
+ # Sort issues for consistent hashing
229
+ try:
230
+ normalized = json.dumps(issues, sort_keys=True)
231
+ except (TypeError, ValueError):
232
+ normalized = str(issues)
233
+
234
+ return hashlib.sha256(normalized.encode()).hexdigest()[:16]
235
+
236
+ def _get_timestamp_bucket(self, timestamp: datetime | None) -> int:
237
+ """Get timestamp bucket number."""
238
+ if timestamp is None:
239
+ timestamp = datetime.utcnow()
240
+
241
+ ts = timestamp.timestamp()
242
+ bucket_size = self.config.timestamp_bucket_seconds
243
+ return int(ts // bucket_size)
244
+
245
+ def _random_fingerprint(self) -> str:
246
+ """Generate a random unique fingerprint."""
247
+ import secrets
248
+
249
+ return secrets.token_hex(16)
250
+
251
+ def with_policy(self, policy: DeduplicationPolicy) -> "FingerprintGenerator":
252
+ """Create new generator with different policy.
253
+
254
+ Args:
255
+ policy: New policy to use.
256
+
257
+ Returns:
258
+ New FingerprintGenerator instance.
259
+ """
260
+ return FingerprintGenerator(policy=policy)
261
+
262
+ def with_config(self, config: FingerprintConfig) -> "FingerprintGenerator":
263
+ """Create new generator with custom config.
264
+
265
+ Args:
266
+ config: Custom configuration.
267
+
268
+ Returns:
269
+ New FingerprintGenerator instance.
270
+ """
271
+ return FingerprintGenerator(
272
+ policy=DeduplicationPolicy.CUSTOM,
273
+ config=config,
274
+ )