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.
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.0.dist-info}/METADATA +142 -22
  162. truthound_dashboard-1.4.0.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.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,509 @@
1
+ """Validation limits and configuration for time-based parameters.
2
+
3
+ This module provides centralized validation constants and utilities
4
+ for preventing DoS attacks and ensuring reasonable configuration values
5
+ across deduplication, throttling, and escalation features.
6
+
7
+ All limits are configurable via environment variables to support
8
+ different deployment scenarios (development, staging, production).
9
+
10
+ Environment Variables:
11
+ TRUTHOUND_DEDUP_WINDOW_MIN: Minimum deduplication window (seconds)
12
+ TRUTHOUND_DEDUP_WINDOW_MAX: Maximum deduplication window (seconds)
13
+ TRUTHOUND_THROTTLE_LIMIT_MIN: Minimum throttle rate limit
14
+ TRUTHOUND_THROTTLE_LIMIT_MAX: Maximum throttle rate limit
15
+ TRUTHOUND_THROTTLE_WINDOW_MIN: Minimum throttle window (seconds)
16
+ TRUTHOUND_THROTTLE_WINDOW_MAX: Maximum throttle window (seconds)
17
+ TRUTHOUND_ESCALATION_DELAY_MIN: Minimum escalation delay (minutes)
18
+ TRUTHOUND_ESCALATION_DELAY_MAX: Maximum escalation delay (minutes)
19
+ TRUTHOUND_ESCALATION_CHECK_INTERVAL_MIN: Min scheduler check interval (seconds)
20
+ TRUTHOUND_ESCALATION_CHECK_INTERVAL_MAX: Max scheduler check interval (seconds)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from dataclasses import dataclass
27
+ from functools import lru_cache
28
+ from typing import Any
29
+
30
+
31
+ # =============================================================================
32
+ # Default Validation Limits
33
+ # =============================================================================
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class DeduplicationLimits:
38
+ """Validation limits for deduplication configuration.
39
+
40
+ Attributes:
41
+ window_min_seconds: Minimum window size (prevents too-small windows).
42
+ window_max_seconds: Maximum window size (prevents memory exhaustion).
43
+ window_default_seconds: Default window size if not specified.
44
+ """
45
+
46
+ window_min_seconds: int = 1
47
+ window_max_seconds: int = 86400 # 24 hours
48
+ window_default_seconds: int = 300 # 5 minutes
49
+
50
+ def validate_window_seconds(self, value: int) -> tuple[bool, str | None]:
51
+ """Validate window_seconds value.
52
+
53
+ Args:
54
+ value: Window duration in seconds.
55
+
56
+ Returns:
57
+ Tuple of (is_valid, error_message).
58
+ """
59
+ if value < self.window_min_seconds:
60
+ return (
61
+ False,
62
+ f"window_seconds must be at least {self.window_min_seconds} second(s), "
63
+ f"got {value}",
64
+ )
65
+ if value > self.window_max_seconds:
66
+ return (
67
+ False,
68
+ f"window_seconds must not exceed {self.window_max_seconds} seconds "
69
+ f"({self.window_max_seconds // 3600} hours), got {value}",
70
+ )
71
+ return (True, None)
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ThrottlingLimits:
76
+ """Validation limits for throttling configuration.
77
+
78
+ Attributes:
79
+ limit_min: Minimum rate limit value (prevents zero/negative limits).
80
+ limit_max: Maximum rate limit value (prevents unreasonable limits).
81
+ window_min_seconds: Minimum window size for custom throttlers.
82
+ window_max_seconds: Maximum window size for custom throttlers.
83
+ burst_allowance_min: Minimum burst allowance factor.
84
+ burst_allowance_max: Maximum burst allowance factor.
85
+ per_minute_max: Maximum notifications per minute.
86
+ per_hour_max: Maximum notifications per hour.
87
+ per_day_max: Maximum notifications per day.
88
+ """
89
+
90
+ limit_min: int = 1
91
+ limit_max: int = 100000 # Allow high limits for batch systems
92
+ window_min_seconds: int = 1
93
+ window_max_seconds: int = 86400 # 24 hours
94
+ burst_allowance_min: float = 1.0
95
+ burst_allowance_max: float = 10.0
96
+ per_minute_max: int = 10000
97
+ per_hour_max: int = 100000
98
+ per_day_max: int = 1000000
99
+
100
+ def validate_rate_limit(
101
+ self,
102
+ value: int,
103
+ limit_name: str = "rate limit",
104
+ ) -> tuple[bool, str | None]:
105
+ """Validate a rate limit value.
106
+
107
+ Args:
108
+ value: Rate limit value.
109
+ limit_name: Name of the limit for error messages.
110
+
111
+ Returns:
112
+ Tuple of (is_valid, error_message).
113
+ """
114
+ if value < self.limit_min:
115
+ return (
116
+ False,
117
+ f"{limit_name} must be at least {self.limit_min}, got {value}",
118
+ )
119
+ if value > self.limit_max:
120
+ return (
121
+ False,
122
+ f"{limit_name} must not exceed {self.limit_max}, got {value}",
123
+ )
124
+ return (True, None)
125
+
126
+ def validate_window_seconds(self, value: int) -> tuple[bool, str | None]:
127
+ """Validate window_seconds for throttling.
128
+
129
+ Args:
130
+ value: Window duration in seconds.
131
+
132
+ Returns:
133
+ Tuple of (is_valid, error_message).
134
+ """
135
+ if value < self.window_min_seconds:
136
+ return (
137
+ False,
138
+ f"window_seconds must be at least {self.window_min_seconds} second(s), "
139
+ f"got {value}",
140
+ )
141
+ if value > self.window_max_seconds:
142
+ return (
143
+ False,
144
+ f"window_seconds must not exceed {self.window_max_seconds} seconds, "
145
+ f"got {value}",
146
+ )
147
+ return (True, None)
148
+
149
+ def validate_burst_allowance(self, value: float) -> tuple[bool, str | None]:
150
+ """Validate burst allowance factor.
151
+
152
+ Args:
153
+ value: Burst allowance factor.
154
+
155
+ Returns:
156
+ Tuple of (is_valid, error_message).
157
+ """
158
+ if value < self.burst_allowance_min:
159
+ return (
160
+ False,
161
+ f"burst_allowance must be at least {self.burst_allowance_min}, "
162
+ f"got {value}",
163
+ )
164
+ if value > self.burst_allowance_max:
165
+ return (
166
+ False,
167
+ f"burst_allowance must not exceed {self.burst_allowance_max}, "
168
+ f"got {value}",
169
+ )
170
+ return (True, None)
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class EscalationLimits:
175
+ """Validation limits for escalation configuration.
176
+
177
+ Attributes:
178
+ delay_min_minutes: Minimum delay between escalation levels.
179
+ delay_max_minutes: Maximum delay between escalation levels.
180
+ max_levels: Maximum number of escalation levels.
181
+ max_escalations_min: Minimum value for max_escalations.
182
+ max_escalations_max: Maximum value for max_escalations.
183
+ check_interval_min_seconds: Minimum scheduler check interval.
184
+ check_interval_max_seconds: Maximum scheduler check interval.
185
+ """
186
+
187
+ delay_min_minutes: int = 0
188
+ delay_max_minutes: int = 10080 # 7 days
189
+ max_levels: int = 20
190
+ max_escalations_min: int = 1
191
+ max_escalations_max: int = 100
192
+ check_interval_min_seconds: int = 10
193
+ check_interval_max_seconds: int = 3600 # 1 hour
194
+
195
+ def validate_delay_minutes(self, value: int) -> tuple[bool, str | None]:
196
+ """Validate escalation delay in minutes.
197
+
198
+ Args:
199
+ value: Delay in minutes.
200
+
201
+ Returns:
202
+ Tuple of (is_valid, error_message).
203
+ """
204
+ if value < self.delay_min_minutes:
205
+ return (
206
+ False,
207
+ f"delay_minutes must be at least {self.delay_min_minutes}, got {value}",
208
+ )
209
+ if value > self.delay_max_minutes:
210
+ return (
211
+ False,
212
+ f"delay_minutes must not exceed {self.delay_max_minutes} minutes "
213
+ f"({self.delay_max_minutes // 1440} days), got {value}",
214
+ )
215
+ return (True, None)
216
+
217
+ def validate_max_escalations(self, value: int) -> tuple[bool, str | None]:
218
+ """Validate max_escalations value.
219
+
220
+ Args:
221
+ value: Maximum escalation attempts.
222
+
223
+ Returns:
224
+ Tuple of (is_valid, error_message).
225
+ """
226
+ if value < self.max_escalations_min:
227
+ return (
228
+ False,
229
+ f"max_escalations must be at least {self.max_escalations_min}, "
230
+ f"got {value}",
231
+ )
232
+ if value > self.max_escalations_max:
233
+ return (
234
+ False,
235
+ f"max_escalations must not exceed {self.max_escalations_max}, "
236
+ f"got {value}",
237
+ )
238
+ return (True, None)
239
+
240
+ def validate_check_interval(self, value: int) -> tuple[bool, str | None]:
241
+ """Validate scheduler check interval in seconds.
242
+
243
+ Args:
244
+ value: Check interval in seconds.
245
+
246
+ Returns:
247
+ Tuple of (is_valid, error_message).
248
+ """
249
+ if value < self.check_interval_min_seconds:
250
+ return (
251
+ False,
252
+ f"check_interval_seconds must be at least "
253
+ f"{self.check_interval_min_seconds} seconds, got {value}",
254
+ )
255
+ if value > self.check_interval_max_seconds:
256
+ return (
257
+ False,
258
+ f"check_interval_seconds must not exceed "
259
+ f"{self.check_interval_max_seconds} seconds, got {value}",
260
+ )
261
+ return (True, None)
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class TimeWindowLimits:
266
+ """Validation limits for TimeWindow class.
267
+
268
+ These limits apply to the service-level TimeWindow dataclass
269
+ used in the deduplication service.
270
+
271
+ Attributes:
272
+ total_seconds_min: Minimum total duration.
273
+ total_seconds_max: Maximum total duration.
274
+ days_max: Maximum days component.
275
+ hours_max: Maximum hours component.
276
+ minutes_max: Maximum minutes component.
277
+ seconds_max: Maximum seconds component.
278
+ """
279
+
280
+ total_seconds_min: int = 1
281
+ total_seconds_max: int = 604800 # 7 days
282
+ days_max: int = 7
283
+ hours_max: int = 168 # 7 * 24
284
+ minutes_max: int = 10080 # 7 * 24 * 60
285
+ seconds_max: int = 604800 # 7 * 24 * 60 * 60
286
+
287
+ def validate_total_seconds(self, value: int) -> tuple[bool, str | None]:
288
+ """Validate total duration in seconds.
289
+
290
+ Args:
291
+ value: Total duration in seconds.
292
+
293
+ Returns:
294
+ Tuple of (is_valid, error_message).
295
+ """
296
+ if value < self.total_seconds_min:
297
+ return (
298
+ False,
299
+ f"Total duration must be at least {self.total_seconds_min} second(s), "
300
+ f"got {value}",
301
+ )
302
+ if value > self.total_seconds_max:
303
+ return (
304
+ False,
305
+ f"Total duration must not exceed {self.total_seconds_max} seconds "
306
+ f"({self.total_seconds_max // 86400} days), got {value}",
307
+ )
308
+ return (True, None)
309
+
310
+
311
+ # =============================================================================
312
+ # Configuration Loader
313
+ # =============================================================================
314
+
315
+
316
+ @lru_cache(maxsize=1)
317
+ def get_deduplication_limits() -> DeduplicationLimits:
318
+ """Get deduplication validation limits from environment.
319
+
320
+ Returns:
321
+ DeduplicationLimits instance with values from environment or defaults.
322
+ """
323
+ return DeduplicationLimits(
324
+ window_min_seconds=int(os.getenv("TRUTHOUND_DEDUP_WINDOW_MIN", "1")),
325
+ window_max_seconds=int(os.getenv("TRUTHOUND_DEDUP_WINDOW_MAX", "86400")),
326
+ window_default_seconds=int(os.getenv("TRUTHOUND_DEDUP_WINDOW_DEFAULT", "300")),
327
+ )
328
+
329
+
330
+ @lru_cache(maxsize=1)
331
+ def get_throttling_limits() -> ThrottlingLimits:
332
+ """Get throttling validation limits from environment.
333
+
334
+ Returns:
335
+ ThrottlingLimits instance with values from environment or defaults.
336
+ """
337
+ return ThrottlingLimits(
338
+ limit_min=int(os.getenv("TRUTHOUND_THROTTLE_LIMIT_MIN", "1")),
339
+ limit_max=int(os.getenv("TRUTHOUND_THROTTLE_LIMIT_MAX", "100000")),
340
+ window_min_seconds=int(os.getenv("TRUTHOUND_THROTTLE_WINDOW_MIN", "1")),
341
+ window_max_seconds=int(os.getenv("TRUTHOUND_THROTTLE_WINDOW_MAX", "86400")),
342
+ burst_allowance_min=float(
343
+ os.getenv("TRUTHOUND_THROTTLE_BURST_MIN", "1.0")
344
+ ),
345
+ burst_allowance_max=float(
346
+ os.getenv("TRUTHOUND_THROTTLE_BURST_MAX", "10.0")
347
+ ),
348
+ per_minute_max=int(os.getenv("TRUTHOUND_THROTTLE_PER_MINUTE_MAX", "10000")),
349
+ per_hour_max=int(os.getenv("TRUTHOUND_THROTTLE_PER_HOUR_MAX", "100000")),
350
+ per_day_max=int(os.getenv("TRUTHOUND_THROTTLE_PER_DAY_MAX", "1000000")),
351
+ )
352
+
353
+
354
+ @lru_cache(maxsize=1)
355
+ def get_escalation_limits() -> EscalationLimits:
356
+ """Get escalation validation limits from environment.
357
+
358
+ Returns:
359
+ EscalationLimits instance with values from environment or defaults.
360
+ """
361
+ return EscalationLimits(
362
+ delay_min_minutes=int(os.getenv("TRUTHOUND_ESCALATION_DELAY_MIN", "0")),
363
+ delay_max_minutes=int(os.getenv("TRUTHOUND_ESCALATION_DELAY_MAX", "10080")),
364
+ max_levels=int(os.getenv("TRUTHOUND_ESCALATION_MAX_LEVELS", "20")),
365
+ max_escalations_min=int(
366
+ os.getenv("TRUTHOUND_ESCALATION_MAX_ESCALATIONS_MIN", "1")
367
+ ),
368
+ max_escalations_max=int(
369
+ os.getenv("TRUTHOUND_ESCALATION_MAX_ESCALATIONS_MAX", "100")
370
+ ),
371
+ check_interval_min_seconds=int(
372
+ os.getenv("TRUTHOUND_ESCALATION_CHECK_INTERVAL_MIN", "10")
373
+ ),
374
+ check_interval_max_seconds=int(
375
+ os.getenv("TRUTHOUND_ESCALATION_CHECK_INTERVAL_MAX", "3600")
376
+ ),
377
+ )
378
+
379
+
380
+ @lru_cache(maxsize=1)
381
+ def get_time_window_limits() -> TimeWindowLimits:
382
+ """Get TimeWindow validation limits from environment.
383
+
384
+ Returns:
385
+ TimeWindowLimits instance with values from environment or defaults.
386
+ """
387
+ return TimeWindowLimits(
388
+ total_seconds_min=int(os.getenv("TRUTHOUND_TIMEWINDOW_MIN", "1")),
389
+ total_seconds_max=int(os.getenv("TRUTHOUND_TIMEWINDOW_MAX", "604800")),
390
+ days_max=int(os.getenv("TRUTHOUND_TIMEWINDOW_DAYS_MAX", "7")),
391
+ )
392
+
393
+
394
+ def clear_limits_cache() -> None:
395
+ """Clear the cached limit instances.
396
+
397
+ Useful for testing when environment variables change.
398
+ """
399
+ get_deduplication_limits.cache_clear()
400
+ get_throttling_limits.cache_clear()
401
+ get_escalation_limits.cache_clear()
402
+ get_time_window_limits.cache_clear()
403
+
404
+
405
+ # =============================================================================
406
+ # Validation Exception
407
+ # =============================================================================
408
+
409
+
410
+ class ValidationLimitError(ValueError):
411
+ """Exception raised when validation limits are violated.
412
+
413
+ This exception provides detailed information about the
414
+ validation failure, including the parameter name, value,
415
+ and the limit that was exceeded.
416
+
417
+ Attributes:
418
+ parameter: Name of the parameter that failed validation.
419
+ value: The invalid value.
420
+ message: Detailed error message.
421
+ """
422
+
423
+ def __init__(
424
+ self,
425
+ message: str,
426
+ parameter: str | None = None,
427
+ value: Any = None,
428
+ ) -> None:
429
+ """Initialize validation error.
430
+
431
+ Args:
432
+ message: Error message.
433
+ parameter: Name of invalid parameter.
434
+ value: The invalid value.
435
+ """
436
+ self.parameter = parameter
437
+ self.value = value
438
+ self.message = message
439
+ super().__init__(message)
440
+
441
+ def __repr__(self) -> str:
442
+ return f"ValidationLimitError(parameter={self.parameter!r}, value={self.value!r})"
443
+
444
+
445
+ # =============================================================================
446
+ # Utility Functions
447
+ # =============================================================================
448
+
449
+
450
+ def validate_positive_int(
451
+ value: int,
452
+ name: str,
453
+ min_value: int = 1,
454
+ max_value: int | None = None,
455
+ ) -> None:
456
+ """Validate a positive integer parameter.
457
+
458
+ Args:
459
+ value: Value to validate.
460
+ name: Parameter name for error messages.
461
+ min_value: Minimum allowed value.
462
+ max_value: Maximum allowed value (optional).
463
+
464
+ Raises:
465
+ ValidationLimitError: If validation fails.
466
+ """
467
+ if value < min_value:
468
+ raise ValidationLimitError(
469
+ f"{name} must be at least {min_value}, got {value}",
470
+ parameter=name,
471
+ value=value,
472
+ )
473
+ if max_value is not None and value > max_value:
474
+ raise ValidationLimitError(
475
+ f"{name} must not exceed {max_value}, got {value}",
476
+ parameter=name,
477
+ value=value,
478
+ )
479
+
480
+
481
+ def validate_positive_float(
482
+ value: float,
483
+ name: str,
484
+ min_value: float = 0.0,
485
+ max_value: float | None = None,
486
+ ) -> None:
487
+ """Validate a positive float parameter.
488
+
489
+ Args:
490
+ value: Value to validate.
491
+ name: Parameter name for error messages.
492
+ min_value: Minimum allowed value.
493
+ max_value: Maximum allowed value (optional).
494
+
495
+ Raises:
496
+ ValidationLimitError: If validation fails.
497
+ """
498
+ if value < min_value:
499
+ raise ValidationLimitError(
500
+ f"{name} must be at least {min_value}, got {value}",
501
+ parameter=name,
502
+ value=value,
503
+ )
504
+ if max_value is not None and value > max_value:
505
+ raise ValidationLimitError(
506
+ f"{name} must not exceed {max_value}, got {value}",
507
+ parameter=name,
508
+ value=value,
509
+ )