kstlib 0.0.1a0__py3-none-any.whl → 1.0.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 (166) hide show
  1. kstlib/__init__.py +266 -1
  2. kstlib/__main__.py +16 -0
  3. kstlib/alerts/__init__.py +110 -0
  4. kstlib/alerts/channels/__init__.py +36 -0
  5. kstlib/alerts/channels/base.py +197 -0
  6. kstlib/alerts/channels/email.py +227 -0
  7. kstlib/alerts/channels/slack.py +389 -0
  8. kstlib/alerts/exceptions.py +72 -0
  9. kstlib/alerts/manager.py +651 -0
  10. kstlib/alerts/models.py +142 -0
  11. kstlib/alerts/throttle.py +263 -0
  12. kstlib/auth/__init__.py +139 -0
  13. kstlib/auth/callback.py +399 -0
  14. kstlib/auth/config.py +502 -0
  15. kstlib/auth/errors.py +127 -0
  16. kstlib/auth/models.py +316 -0
  17. kstlib/auth/providers/__init__.py +14 -0
  18. kstlib/auth/providers/base.py +393 -0
  19. kstlib/auth/providers/oauth2.py +645 -0
  20. kstlib/auth/providers/oidc.py +821 -0
  21. kstlib/auth/session.py +338 -0
  22. kstlib/auth/token.py +482 -0
  23. kstlib/cache/__init__.py +50 -0
  24. kstlib/cache/decorator.py +261 -0
  25. kstlib/cache/strategies.py +516 -0
  26. kstlib/cli/__init__.py +8 -0
  27. kstlib/cli/app.py +195 -0
  28. kstlib/cli/commands/__init__.py +5 -0
  29. kstlib/cli/commands/auth/__init__.py +39 -0
  30. kstlib/cli/commands/auth/common.py +122 -0
  31. kstlib/cli/commands/auth/login.py +325 -0
  32. kstlib/cli/commands/auth/logout.py +74 -0
  33. kstlib/cli/commands/auth/providers.py +57 -0
  34. kstlib/cli/commands/auth/status.py +291 -0
  35. kstlib/cli/commands/auth/token.py +199 -0
  36. kstlib/cli/commands/auth/whoami.py +106 -0
  37. kstlib/cli/commands/config.py +89 -0
  38. kstlib/cli/commands/ops/__init__.py +39 -0
  39. kstlib/cli/commands/ops/attach.py +49 -0
  40. kstlib/cli/commands/ops/common.py +269 -0
  41. kstlib/cli/commands/ops/list_sessions.py +252 -0
  42. kstlib/cli/commands/ops/logs.py +49 -0
  43. kstlib/cli/commands/ops/start.py +98 -0
  44. kstlib/cli/commands/ops/status.py +138 -0
  45. kstlib/cli/commands/ops/stop.py +60 -0
  46. kstlib/cli/commands/rapi/__init__.py +60 -0
  47. kstlib/cli/commands/rapi/call.py +341 -0
  48. kstlib/cli/commands/rapi/list.py +99 -0
  49. kstlib/cli/commands/rapi/show.py +206 -0
  50. kstlib/cli/commands/secrets/__init__.py +35 -0
  51. kstlib/cli/commands/secrets/common.py +425 -0
  52. kstlib/cli/commands/secrets/decrypt.py +88 -0
  53. kstlib/cli/commands/secrets/doctor.py +743 -0
  54. kstlib/cli/commands/secrets/encrypt.py +242 -0
  55. kstlib/cli/commands/secrets/shred.py +96 -0
  56. kstlib/cli/common.py +86 -0
  57. kstlib/config/__init__.py +76 -0
  58. kstlib/config/exceptions.py +110 -0
  59. kstlib/config/export.py +225 -0
  60. kstlib/config/loader.py +963 -0
  61. kstlib/config/sops.py +287 -0
  62. kstlib/db/__init__.py +54 -0
  63. kstlib/db/aiosqlcipher.py +137 -0
  64. kstlib/db/cipher.py +112 -0
  65. kstlib/db/database.py +367 -0
  66. kstlib/db/exceptions.py +25 -0
  67. kstlib/db/pool.py +302 -0
  68. kstlib/helpers/__init__.py +35 -0
  69. kstlib/helpers/exceptions.py +11 -0
  70. kstlib/helpers/time_trigger.py +396 -0
  71. kstlib/kstlib.conf.yml +890 -0
  72. kstlib/limits.py +963 -0
  73. kstlib/logging/__init__.py +108 -0
  74. kstlib/logging/manager.py +633 -0
  75. kstlib/mail/__init__.py +42 -0
  76. kstlib/mail/builder.py +626 -0
  77. kstlib/mail/exceptions.py +27 -0
  78. kstlib/mail/filesystem.py +248 -0
  79. kstlib/mail/transport.py +224 -0
  80. kstlib/mail/transports/__init__.py +19 -0
  81. kstlib/mail/transports/gmail.py +268 -0
  82. kstlib/mail/transports/resend.py +324 -0
  83. kstlib/mail/transports/smtp.py +326 -0
  84. kstlib/meta.py +72 -0
  85. kstlib/metrics/__init__.py +88 -0
  86. kstlib/metrics/decorators.py +1090 -0
  87. kstlib/metrics/exceptions.py +14 -0
  88. kstlib/monitoring/__init__.py +116 -0
  89. kstlib/monitoring/_styles.py +163 -0
  90. kstlib/monitoring/cell.py +57 -0
  91. kstlib/monitoring/config.py +424 -0
  92. kstlib/monitoring/delivery.py +579 -0
  93. kstlib/monitoring/exceptions.py +63 -0
  94. kstlib/monitoring/image.py +220 -0
  95. kstlib/monitoring/kv.py +79 -0
  96. kstlib/monitoring/list.py +69 -0
  97. kstlib/monitoring/metric.py +88 -0
  98. kstlib/monitoring/monitoring.py +341 -0
  99. kstlib/monitoring/renderer.py +139 -0
  100. kstlib/monitoring/service.py +392 -0
  101. kstlib/monitoring/table.py +129 -0
  102. kstlib/monitoring/types.py +56 -0
  103. kstlib/ops/__init__.py +86 -0
  104. kstlib/ops/base.py +148 -0
  105. kstlib/ops/container.py +577 -0
  106. kstlib/ops/exceptions.py +209 -0
  107. kstlib/ops/manager.py +407 -0
  108. kstlib/ops/models.py +176 -0
  109. kstlib/ops/tmux.py +372 -0
  110. kstlib/ops/validators.py +287 -0
  111. kstlib/py.typed +0 -0
  112. kstlib/rapi/__init__.py +118 -0
  113. kstlib/rapi/client.py +875 -0
  114. kstlib/rapi/config.py +861 -0
  115. kstlib/rapi/credentials.py +887 -0
  116. kstlib/rapi/exceptions.py +213 -0
  117. kstlib/resilience/__init__.py +101 -0
  118. kstlib/resilience/circuit_breaker.py +440 -0
  119. kstlib/resilience/exceptions.py +95 -0
  120. kstlib/resilience/heartbeat.py +491 -0
  121. kstlib/resilience/rate_limiter.py +506 -0
  122. kstlib/resilience/shutdown.py +417 -0
  123. kstlib/resilience/watchdog.py +637 -0
  124. kstlib/secrets/__init__.py +29 -0
  125. kstlib/secrets/exceptions.py +19 -0
  126. kstlib/secrets/models.py +62 -0
  127. kstlib/secrets/providers/__init__.py +79 -0
  128. kstlib/secrets/providers/base.py +58 -0
  129. kstlib/secrets/providers/environment.py +66 -0
  130. kstlib/secrets/providers/keyring.py +107 -0
  131. kstlib/secrets/providers/kms.py +223 -0
  132. kstlib/secrets/providers/kwargs.py +101 -0
  133. kstlib/secrets/providers/sops.py +209 -0
  134. kstlib/secrets/resolver.py +221 -0
  135. kstlib/secrets/sensitive.py +130 -0
  136. kstlib/secure/__init__.py +23 -0
  137. kstlib/secure/fs.py +194 -0
  138. kstlib/secure/permissions.py +70 -0
  139. kstlib/ssl.py +347 -0
  140. kstlib/ui/__init__.py +23 -0
  141. kstlib/ui/exceptions.py +26 -0
  142. kstlib/ui/panels.py +484 -0
  143. kstlib/ui/spinner.py +864 -0
  144. kstlib/ui/tables.py +382 -0
  145. kstlib/utils/__init__.py +48 -0
  146. kstlib/utils/dict.py +36 -0
  147. kstlib/utils/formatting.py +338 -0
  148. kstlib/utils/http_trace.py +237 -0
  149. kstlib/utils/lazy.py +49 -0
  150. kstlib/utils/secure_delete.py +205 -0
  151. kstlib/utils/serialization.py +247 -0
  152. kstlib/utils/text.py +56 -0
  153. kstlib/utils/validators.py +124 -0
  154. kstlib/websocket/__init__.py +97 -0
  155. kstlib/websocket/exceptions.py +214 -0
  156. kstlib/websocket/manager.py +1102 -0
  157. kstlib/websocket/models.py +361 -0
  158. kstlib-1.0.1.dist-info/METADATA +201 -0
  159. kstlib-1.0.1.dist-info/RECORD +163 -0
  160. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
  161. kstlib-1.0.1.dist-info/entry_points.txt +2 -0
  162. kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
  163. kstlib-0.0.1a0.dist-info/METADATA +0 -29
  164. kstlib-0.0.1a0.dist-info/RECORD +0 -6
  165. kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
  166. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
kstlib/limits.py ADDED
@@ -0,0 +1,963 @@
1
+ """Config-driven limits with hard-coded deep defense maximums.
2
+
3
+ This module provides configurable resource limits that users can customize
4
+ via kstlib.conf.yml, while enforcing hard maximums in code for deep defense
5
+ against misconfiguration or malicious input.
6
+
7
+ Example:
8
+ >>> from kstlib.limits import get_mail_limits, get_cache_limits
9
+ >>> mail_limits = get_mail_limits()
10
+ >>> mail_limits.max_attachment_size
11
+ 26214400
12
+ >>> mail_limits.max_attachment_size_display
13
+ '25.0 MiB'
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Mapping
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ from kstlib.utils.formatting import format_bytes, parse_size_string
23
+
24
+ __all__ = [
25
+ "HARD_MAX_DATETIME_FORMAT_LENGTH",
26
+ "HARD_MAX_DISPLAY_VALUE_LENGTH",
27
+ "HARD_MAX_ENDPOINT_REF_LENGTH",
28
+ "HARD_MAX_EPOCH_TIMESTAMP",
29
+ "HARD_MAX_TIMEZONE_LENGTH",
30
+ "HARD_MIN_EPOCH_TIMESTAMP",
31
+ "AlertsLimits",
32
+ "CacheLimits",
33
+ "DatabaseLimits",
34
+ "MailLimits",
35
+ "RapiLimits",
36
+ "RapiRenderConfig",
37
+ "ResilienceLimits",
38
+ "SopsLimits",
39
+ "WebSocketLimits",
40
+ "clamp_with_limits",
41
+ "get_alerts_limits",
42
+ "get_cache_limits",
43
+ "get_db_limits",
44
+ "get_mail_limits",
45
+ "get_rapi_limits",
46
+ "get_rapi_render_config",
47
+ "get_resilience_limits",
48
+ "get_sops_limits",
49
+ "get_websocket_limits",
50
+ ]
51
+
52
+ # =============================================================================
53
+ # Hard limits (deep defense - cannot be exceeded via config)
54
+ # =============================================================================
55
+
56
+ #: Absolute maximum attachment size (25 MiB) - protects against memory exhaustion.
57
+ HARD_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024
58
+
59
+ #: Absolute maximum attachments per message - protects against resource exhaustion.
60
+ HARD_MAX_ATTACHMENTS = 50
61
+
62
+ #: Absolute maximum cache file size (100 MiB) - protects against OOM.
63
+ HARD_MAX_CACHE_FILE_SIZE = 100 * 1024 * 1024
64
+
65
+ #: Absolute maximum SOPS cache entries - protects against memory exhaustion.
66
+ HARD_MAX_SOPS_CACHE_ENTRIES = 256
67
+
68
+ #: Heartbeat interval bounds (seconds) - protects against too frequent or stale checks.
69
+ HARD_MIN_HEARTBEAT_INTERVAL = 1
70
+ HARD_MAX_HEARTBEAT_INTERVAL = 300 # 5 minutes
71
+
72
+ #: Shutdown timeout bounds (seconds) - protects against hanging or too fast shutdowns.
73
+ HARD_MIN_SHUTDOWN_TIMEOUT = 5
74
+ HARD_MAX_SHUTDOWN_TIMEOUT = 300 # 5 minutes
75
+
76
+ #: Circuit breaker failure count bounds - protects against trivial or impossible thresholds.
77
+ HARD_MIN_CIRCUIT_FAILURES = 1
78
+ HARD_MAX_CIRCUIT_FAILURES = 100
79
+
80
+ #: Circuit breaker reset timeout bounds (seconds) - protects against too short or too long cooldowns.
81
+ HARD_MIN_CIRCUIT_RESET_TIMEOUT = 1
82
+ HARD_MAX_CIRCUIT_RESET_TIMEOUT = 3600 # 1 hour
83
+
84
+ #: Circuit breaker half-open calls bounds - protects against trivial or too aggressive testing.
85
+ HARD_MIN_HALF_OPEN_CALLS = 1
86
+ HARD_MAX_HALF_OPEN_CALLS = 10
87
+
88
+ #: Watchdog timeout bounds (seconds) - protects against too short or impossibly long timeouts.
89
+ HARD_MIN_WATCHDOG_TIMEOUT = 1
90
+ HARD_MAX_WATCHDOG_TIMEOUT = 3600 # 1 hour
91
+
92
+ #: Database pool min_size bounds - protects against invalid pool configuration.
93
+ #: min_size=0 is valid (lazy pool, connections created on demand).
94
+ HARD_MIN_POOL_MIN_SIZE = 0
95
+ HARD_MAX_POOL_MIN_SIZE = 10
96
+
97
+ #: Database pool max_size bounds - protects against resource exhaustion.
98
+ HARD_MIN_POOL_MAX_SIZE = 1
99
+ HARD_MAX_POOL_MAX_SIZE = 100
100
+
101
+ #: Database pool acquire timeout bounds (seconds) - protects against deadlocks or no-wait.
102
+ HARD_MIN_POOL_ACQUIRE_TIMEOUT = 1.0
103
+ HARD_MAX_POOL_ACQUIRE_TIMEOUT = 300.0 # 5 minutes
104
+
105
+ #: Database retry attempts bounds - protects against infinite retries or no retry.
106
+ HARD_MIN_DB_MAX_RETRIES = 1
107
+ HARD_MAX_DB_MAX_RETRIES = 10
108
+
109
+ #: Database retry delay bounds (seconds) - protects against too fast or too slow retries.
110
+ HARD_MIN_DB_RETRY_DELAY = 0.1
111
+ HARD_MAX_DB_RETRY_DELAY = 60.0
112
+
113
+ #: RAPI timeout bounds (seconds) - protects against too short or infinite waits.
114
+ HARD_MIN_RAPI_TIMEOUT = 1.0
115
+ HARD_MAX_RAPI_TIMEOUT = 300.0 # 5 minutes
116
+
117
+ #: RAPI max response size (100 MiB) - protects against memory exhaustion.
118
+ HARD_MAX_RAPI_RESPONSE_SIZE = 100 * 1024 * 1024
119
+
120
+ #: RAPI retry attempts bounds - protects against infinite retries.
121
+ HARD_MIN_RAPI_RETRIES = 0
122
+ HARD_MAX_RAPI_RETRIES = 10
123
+
124
+ #: RAPI retry delay bounds (seconds) - protects against too fast or too slow retries.
125
+ HARD_MIN_RAPI_RETRY_DELAY = 0.1
126
+ HARD_MAX_RAPI_RETRY_DELAY = 60.0
127
+
128
+ #: RAPI backoff multiplier bounds - protects against too aggressive or too slow backoff.
129
+ HARD_MIN_RAPI_BACKOFF = 1.0
130
+ HARD_MAX_RAPI_BACKOFF = 5.0
131
+
132
+ #: Alert throttle rate bounds - protects against too permissive or impossible thresholds.
133
+ HARD_MIN_THROTTLE_RATE = 1
134
+ HARD_MAX_THROTTLE_RATE = 1000
135
+
136
+ #: Alert throttle period bounds (seconds) - protects against too short or impossibly long periods.
137
+ HARD_MIN_THROTTLE_PER = 1.0
138
+ HARD_MAX_THROTTLE_PER = 86400.0 # 1 day
139
+
140
+ #: Alert channel timeout bounds (seconds) - protects against too short or hanging requests.
141
+ HARD_MIN_CHANNEL_TIMEOUT = 1.0
142
+ HARD_MAX_CHANNEL_TIMEOUT = 120.0
143
+
144
+ #: Alert channel retry bounds - protects against infinite retries.
145
+ HARD_MIN_CHANNEL_RETRIES = 0
146
+ HARD_MAX_CHANNEL_RETRIES = 5
147
+
148
+ #: WebSocket ping interval bounds (seconds) - protects against too frequent or stale checks.
149
+ HARD_MIN_WS_PING_INTERVAL = 5.0
150
+ HARD_MAX_WS_PING_INTERVAL = 60.0
151
+
152
+ #: WebSocket ping timeout bounds (seconds) - protects against too short or too long timeouts.
153
+ HARD_MIN_WS_PING_TIMEOUT = 5.0
154
+ HARD_MAX_WS_PING_TIMEOUT = 30.0
155
+
156
+ #: WebSocket connection timeout bounds (seconds) - protects against too short or infinite waits.
157
+ HARD_MIN_WS_CONNECTION_TIMEOUT = 5.0
158
+ HARD_MAX_WS_CONNECTION_TIMEOUT = 120.0
159
+
160
+ #: WebSocket reconnect delay bounds (seconds) - immediate allowed, max 5 minutes.
161
+ HARD_MIN_WS_RECONNECT_DELAY = 0.0
162
+ HARD_MAX_WS_RECONNECT_DELAY = 300.0
163
+
164
+ #: WebSocket max reconnect delay bounds (seconds) - for exponential backoff cap.
165
+ HARD_MIN_WS_MAX_RECONNECT_DELAY = 1.0
166
+ HARD_MAX_WS_MAX_RECONNECT_DELAY = 600.0
167
+
168
+ #: WebSocket max reconnect attempts bounds - 0 means no retry.
169
+ HARD_MIN_WS_RECONNECT_ATTEMPTS = 0
170
+ HARD_MAX_WS_RECONNECT_ATTEMPTS = 100
171
+
172
+ #: WebSocket message queue size bounds - 0 means unlimited.
173
+ HARD_MIN_WS_QUEUE_SIZE = 0
174
+ HARD_MAX_WS_QUEUE_SIZE = 10000
175
+
176
+ #: WebSocket disconnect check interval bounds (seconds) - for proactive control.
177
+ HARD_MIN_WS_DISCONNECT_CHECK = 1.0
178
+ HARD_MAX_WS_DISCONNECT_CHECK = 60.0
179
+
180
+ #: WebSocket reconnect check interval bounds (seconds) - for proactive control.
181
+ HARD_MIN_WS_RECONNECT_CHECK = 0.5
182
+ HARD_MAX_WS_RECONNECT_CHECK = 60.0
183
+
184
+ #: WebSocket proactive disconnect margin bounds (seconds) - before platform limits.
185
+ HARD_MIN_WS_DISCONNECT_MARGIN = 60.0
186
+ HARD_MAX_WS_DISCONNECT_MARGIN = 3600.0
187
+
188
+ #: Maximum endpoint reference length (api.endpoint format) - protects against DoS.
189
+ HARD_MAX_ENDPOINT_REF_LENGTH = 256
190
+
191
+ #: Maximum display length for values in rapi show (truncate long strings).
192
+ HARD_MAX_DISPLAY_VALUE_LENGTH = 200
193
+
194
+ #: Maximum datetime format string length - protects against DoS.
195
+ HARD_MAX_DATETIME_FORMAT_LENGTH = 64
196
+
197
+ #: Maximum timezone string length - protects against DoS.
198
+ HARD_MAX_TIMEZONE_LENGTH = 64
199
+
200
+ #: Minimum valid epoch timestamp (1970-01-01 00:00:00 UTC).
201
+ HARD_MIN_EPOCH_TIMESTAMP = 0
202
+
203
+ #: Maximum valid epoch timestamp (year 2100) - protects against overflow.
204
+ HARD_MAX_EPOCH_TIMESTAMP = 4102444800
205
+
206
+ # =============================================================================
207
+ # Default limits (used when config is not available)
208
+ # =============================================================================
209
+
210
+ DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024 # 25 MiB
211
+ DEFAULT_MAX_ATTACHMENTS = 20
212
+ DEFAULT_MAX_CACHE_FILE_SIZE = 50 * 1024 * 1024 # 50 MiB
213
+ DEFAULT_MAX_SOPS_CACHE_ENTRIES = 64
214
+
215
+ DEFAULT_HEARTBEAT_INTERVAL = 10 # seconds
216
+ DEFAULT_SHUTDOWN_TIMEOUT = 30 # seconds
217
+ DEFAULT_CIRCUIT_MAX_FAILURES = 5
218
+ DEFAULT_CIRCUIT_RESET_TIMEOUT = 60 # seconds
219
+ DEFAULT_HALF_OPEN_MAX_CALLS = 1
220
+ DEFAULT_WATCHDOG_TIMEOUT = 30 # seconds
221
+
222
+ DEFAULT_POOL_MIN_SIZE = 1
223
+ DEFAULT_POOL_MAX_SIZE = 10
224
+ DEFAULT_POOL_ACQUIRE_TIMEOUT = 30.0 # seconds
225
+ DEFAULT_DB_MAX_RETRIES = 3
226
+ DEFAULT_DB_RETRY_DELAY = 0.5 # seconds
227
+
228
+ DEFAULT_RAPI_TIMEOUT = 30.0 # seconds
229
+ DEFAULT_RAPI_MAX_RESPONSE_SIZE = 10 * 1024 * 1024 # 10 MiB
230
+ DEFAULT_RAPI_MAX_RETRIES = 3
231
+ DEFAULT_RAPI_RETRY_DELAY = 1.0 # seconds
232
+ DEFAULT_RAPI_BACKOFF = 2.0
233
+
234
+ DEFAULT_THROTTLE_RATE = 10 # alerts per period
235
+ DEFAULT_THROTTLE_PER = 60.0 # seconds
236
+ DEFAULT_THROTTLE_BURST = 5 # initial capacity
237
+ DEFAULT_CHANNEL_TIMEOUT = 30.0 # seconds
238
+ DEFAULT_CHANNEL_RETRIES = 2
239
+
240
+ DEFAULT_WS_PING_INTERVAL = 20.0 # seconds
241
+ DEFAULT_WS_PING_TIMEOUT = 10.0 # seconds
242
+ DEFAULT_WS_CONNECTION_TIMEOUT = 30.0 # seconds
243
+ DEFAULT_WS_RECONNECT_DELAY = 1.0 # seconds
244
+ DEFAULT_WS_MAX_RECONNECT_DELAY = 60.0 # seconds
245
+ DEFAULT_WS_RECONNECT_ATTEMPTS = 10
246
+ DEFAULT_WS_QUEUE_SIZE = 1000 # messages
247
+ DEFAULT_WS_DISCONNECT_CHECK = 10.0 # seconds
248
+ DEFAULT_WS_RECONNECT_CHECK = 5.0 # seconds
249
+ DEFAULT_WS_DISCONNECT_MARGIN = 300.0 # seconds (5 minutes before 24h limit)
250
+
251
+
252
+ @dataclass(frozen=True, slots=True)
253
+ class MailLimits:
254
+ """Resolved mail resource limits."""
255
+
256
+ max_attachment_size: int
257
+ max_attachments: int
258
+
259
+ @property
260
+ def max_attachment_size_display(self) -> str:
261
+ """Human-readable attachment size limit."""
262
+ return format_bytes(self.max_attachment_size)
263
+
264
+
265
+ @dataclass(frozen=True, slots=True)
266
+ class CacheLimits:
267
+ """Resolved cache resource limits."""
268
+
269
+ max_file_size: int
270
+
271
+ @property
272
+ def max_file_size_display(self) -> str:
273
+ """Human-readable cache file size limit."""
274
+ return format_bytes(self.max_file_size)
275
+
276
+
277
+ @dataclass(frozen=True, slots=True)
278
+ class SopsLimits:
279
+ """Resolved SOPS provider limits."""
280
+
281
+ max_cache_entries: int
282
+
283
+
284
+ def _load_config() -> Mapping[str, Any] | None:
285
+ """Attempt to load the global configuration."""
286
+ # pylint: disable=import-outside-toplevel
287
+ try:
288
+ # Lazy imports to avoid circular dependencies
289
+ from kstlib.config import get_config
290
+ from kstlib.config.exceptions import ConfigNotLoadedError
291
+ except ImportError: # pragma: no cover - defensive branch
292
+ return None # pragma: no cover - defensive branch
293
+
294
+ try:
295
+ return get_config()
296
+ except ConfigNotLoadedError: # pragma: no cover - defensive branch
297
+ return None # pragma: no cover - defensive branch
298
+
299
+
300
+ def _get_nested(config: Mapping[str, Any] | None, *keys: str, default: Any = None) -> Any:
301
+ """Safely traverse nested config keys."""
302
+ if config is None:
303
+ return default
304
+ current: Any = config
305
+ for key in keys:
306
+ if not isinstance(current, Mapping):
307
+ return default
308
+ current = current.get(key)
309
+ if current is None:
310
+ return default
311
+ return current
312
+
313
+
314
+ def _parse_float_config(raw_value: Any, default: float, hard_min: float, hard_max: float) -> float:
315
+ """Parse a float config value with clamping."""
316
+ if raw_value is None:
317
+ return clamp_with_limits(default, hard_min, hard_max)
318
+ try:
319
+ value = float(raw_value)
320
+ except (TypeError, ValueError):
321
+ value = default
322
+ return clamp_with_limits(value, hard_min, hard_max)
323
+
324
+
325
+ def _parse_int_config(raw_value: Any, default: int, hard_min: int, hard_max: int) -> int:
326
+ """Parse an int config value with clamping."""
327
+ if raw_value is None:
328
+ return int(clamp_with_limits(default, hard_min, hard_max))
329
+ try:
330
+ value = int(raw_value)
331
+ except (TypeError, ValueError):
332
+ value = default
333
+ return int(clamp_with_limits(value, hard_min, hard_max))
334
+
335
+
336
+ def get_mail_limits(config: Mapping[str, Any] | None = None) -> MailLimits:
337
+ """Resolve mail limits from config with hard limit enforcement.
338
+
339
+ Args:
340
+ config: Optional config mapping. If None, loads from get_config().
341
+
342
+ Returns:
343
+ MailLimits with resolved values clamped to hard maximums.
344
+ """
345
+ if config is None:
346
+ config = _load_config()
347
+
348
+ # Read configured values
349
+ raw_size = _get_nested(config, "mail", "limits", "max_attachment_size")
350
+ raw_count = _get_nested(config, "mail", "limits", "max_attachments")
351
+
352
+ # Parse and clamp attachment size
353
+ if raw_size is not None:
354
+ try:
355
+ configured_size = parse_size_string(raw_size)
356
+ except ValueError:
357
+ configured_size = DEFAULT_MAX_ATTACHMENT_SIZE
358
+ else:
359
+ configured_size = DEFAULT_MAX_ATTACHMENT_SIZE
360
+
361
+ max_attachment_size = min(configured_size, HARD_MAX_ATTACHMENT_SIZE)
362
+
363
+ # Parse and clamp attachment count
364
+ if raw_count is not None:
365
+ try:
366
+ configured_count = int(raw_count)
367
+ except (TypeError, ValueError):
368
+ configured_count = DEFAULT_MAX_ATTACHMENTS
369
+ else:
370
+ configured_count = DEFAULT_MAX_ATTACHMENTS
371
+
372
+ max_attachments = min(max(1, configured_count), HARD_MAX_ATTACHMENTS)
373
+
374
+ return MailLimits(
375
+ max_attachment_size=max_attachment_size,
376
+ max_attachments=max_attachments,
377
+ )
378
+
379
+
380
+ def get_cache_limits(config: Mapping[str, Any] | None = None) -> CacheLimits:
381
+ """Resolve cache limits from config with hard limit enforcement.
382
+
383
+ Args:
384
+ config: Optional config mapping. If None, loads from get_config().
385
+
386
+ Returns:
387
+ CacheLimits with resolved values clamped to hard maximums.
388
+ """
389
+ if config is None:
390
+ config = _load_config()
391
+
392
+ raw_size = _get_nested(config, "cache", "file", "max_file_size")
393
+
394
+ if raw_size is not None:
395
+ try:
396
+ configured_size = parse_size_string(raw_size)
397
+ except ValueError:
398
+ configured_size = DEFAULT_MAX_CACHE_FILE_SIZE
399
+ else:
400
+ configured_size = DEFAULT_MAX_CACHE_FILE_SIZE
401
+
402
+ max_file_size = min(configured_size, HARD_MAX_CACHE_FILE_SIZE)
403
+
404
+ return CacheLimits(max_file_size=max_file_size)
405
+
406
+
407
+ def get_sops_limits(config: Mapping[str, Any] | None = None) -> SopsLimits:
408
+ """Resolve SOPS limits from config with hard limit enforcement.
409
+
410
+ Args:
411
+ config: Optional config mapping. If None, loads from get_config().
412
+
413
+ Returns:
414
+ SopsLimits with resolved values clamped to hard maximums.
415
+ """
416
+ if config is None:
417
+ config = _load_config()
418
+
419
+ raw_entries = _get_nested(config, "secrets", "sops", "max_cache_entries")
420
+
421
+ if raw_entries is not None:
422
+ try:
423
+ configured_entries = int(raw_entries)
424
+ except (TypeError, ValueError):
425
+ configured_entries = DEFAULT_MAX_SOPS_CACHE_ENTRIES
426
+ else:
427
+ configured_entries = DEFAULT_MAX_SOPS_CACHE_ENTRIES
428
+
429
+ max_cache_entries = min(max(1, configured_entries), HARD_MAX_SOPS_CACHE_ENTRIES)
430
+
431
+ return SopsLimits(max_cache_entries=max_cache_entries)
432
+
433
+
434
+ @dataclass(frozen=True, slots=True)
435
+ class ResilienceLimits:
436
+ """Resolved resilience configuration limits.
437
+
438
+ Attributes:
439
+ heartbeat_interval: Seconds between heartbeats.
440
+ shutdown_timeout: Total timeout for cleanup callbacks.
441
+ circuit_max_failures: Failures before opening circuit.
442
+ circuit_reset_timeout: Cooldown before recovery attempt.
443
+ circuit_half_open_calls: Calls allowed in half-open state.
444
+ watchdog_timeout: Seconds before watchdog triggers timeout.
445
+ """
446
+
447
+ heartbeat_interval: float
448
+ shutdown_timeout: float
449
+ circuit_max_failures: int
450
+ circuit_reset_timeout: float
451
+ circuit_half_open_calls: int
452
+ watchdog_timeout: float
453
+
454
+
455
+ def clamp_with_limits(value: float, hard_min: float, hard_max: float) -> float:
456
+ """Clamp a value between hard minimum and maximum bounds.
457
+
458
+ Utility function for applying hard limits to user-provided values.
459
+ Used throughout the resilience module for defensive programming.
460
+
461
+ Args:
462
+ value: The value to clamp.
463
+ hard_min: Minimum allowed value (inclusive).
464
+ hard_max: Maximum allowed value (inclusive).
465
+
466
+ Returns:
467
+ The clamped value within [hard_min, hard_max].
468
+
469
+ Examples:
470
+ >>> clamp_with_limits(50, 1, 100)
471
+ 50
472
+ >>> clamp_with_limits(0, 1, 100)
473
+ 1
474
+ >>> clamp_with_limits(200, 1, 100)
475
+ 100
476
+ """
477
+ return max(hard_min, min(value, hard_max))
478
+
479
+
480
+ def get_resilience_limits(
481
+ config: Mapping[str, Any] | None = None,
482
+ ) -> ResilienceLimits:
483
+ """Resolve resilience limits from config with hard limit enforcement.
484
+
485
+ Args:
486
+ config: Optional config mapping. If None, loads from get_config().
487
+
488
+ Returns:
489
+ ResilienceLimits with resolved values clamped to hard bounds.
490
+
491
+ Examples:
492
+ >>> limits = get_resilience_limits()
493
+ >>> int(limits.heartbeat_interval)
494
+ 10
495
+ >>> limits.circuit_max_failures
496
+ 5
497
+ """
498
+ if config is None:
499
+ config = _load_config()
500
+
501
+ return ResilienceLimits(
502
+ heartbeat_interval=_parse_float_config(
503
+ _get_nested(config, "resilience", "heartbeat", "interval"),
504
+ DEFAULT_HEARTBEAT_INTERVAL,
505
+ HARD_MIN_HEARTBEAT_INTERVAL,
506
+ HARD_MAX_HEARTBEAT_INTERVAL,
507
+ ),
508
+ shutdown_timeout=_parse_float_config(
509
+ _get_nested(config, "resilience", "shutdown", "timeout"),
510
+ DEFAULT_SHUTDOWN_TIMEOUT,
511
+ HARD_MIN_SHUTDOWN_TIMEOUT,
512
+ HARD_MAX_SHUTDOWN_TIMEOUT,
513
+ ),
514
+ circuit_max_failures=_parse_int_config(
515
+ _get_nested(config, "resilience", "circuit_breaker", "max_failures"),
516
+ DEFAULT_CIRCUIT_MAX_FAILURES,
517
+ HARD_MIN_CIRCUIT_FAILURES,
518
+ HARD_MAX_CIRCUIT_FAILURES,
519
+ ),
520
+ circuit_reset_timeout=_parse_float_config(
521
+ _get_nested(config, "resilience", "circuit_breaker", "reset_timeout"),
522
+ DEFAULT_CIRCUIT_RESET_TIMEOUT,
523
+ HARD_MIN_CIRCUIT_RESET_TIMEOUT,
524
+ HARD_MAX_CIRCUIT_RESET_TIMEOUT,
525
+ ),
526
+ circuit_half_open_calls=_parse_int_config(
527
+ _get_nested(config, "resilience", "circuit_breaker", "half_open_max_calls"),
528
+ DEFAULT_HALF_OPEN_MAX_CALLS,
529
+ HARD_MIN_HALF_OPEN_CALLS,
530
+ HARD_MAX_HALF_OPEN_CALLS,
531
+ ),
532
+ watchdog_timeout=_parse_float_config(
533
+ _get_nested(config, "resilience", "watchdog", "timeout"),
534
+ DEFAULT_WATCHDOG_TIMEOUT,
535
+ HARD_MIN_WATCHDOG_TIMEOUT,
536
+ HARD_MAX_WATCHDOG_TIMEOUT,
537
+ ),
538
+ )
539
+
540
+
541
+ @dataclass(frozen=True, slots=True)
542
+ class DatabaseLimits:
543
+ """Resolved database configuration limits.
544
+
545
+ Attributes:
546
+ pool_min_size: Minimum connections to maintain in pool.
547
+ pool_max_size: Maximum connections allowed in pool.
548
+ pool_acquire_timeout: Timeout for acquiring a connection (seconds).
549
+ max_retries: Retry attempts on connection failure.
550
+ retry_delay: Delay between retries (seconds).
551
+ """
552
+
553
+ pool_min_size: int
554
+ pool_max_size: int
555
+ pool_acquire_timeout: float
556
+ max_retries: int
557
+ retry_delay: float
558
+
559
+
560
+ def get_db_limits(
561
+ config: Mapping[str, Any] | None = None,
562
+ ) -> DatabaseLimits:
563
+ """Resolve database limits from config with hard limit enforcement.
564
+
565
+ Args:
566
+ config: Optional config mapping. If None, loads from get_config().
567
+
568
+ Returns:
569
+ DatabaseLimits with resolved values clamped to hard bounds.
570
+
571
+ Examples:
572
+ >>> limits = get_db_limits()
573
+ >>> limits.pool_min_size
574
+ 1
575
+ >>> limits.pool_max_size
576
+ 10
577
+ """
578
+ if config is None:
579
+ config = _load_config()
580
+
581
+ pool_min = _parse_int_config(
582
+ _get_nested(config, "db", "pool", "min_size"),
583
+ DEFAULT_POOL_MIN_SIZE,
584
+ HARD_MIN_POOL_MIN_SIZE,
585
+ HARD_MAX_POOL_MIN_SIZE,
586
+ )
587
+ pool_max = _parse_int_config(
588
+ _get_nested(config, "db", "pool", "max_size"),
589
+ DEFAULT_POOL_MAX_SIZE,
590
+ HARD_MIN_POOL_MAX_SIZE,
591
+ HARD_MAX_POOL_MAX_SIZE,
592
+ )
593
+
594
+ # Ensure min_size <= max_size
595
+ pool_min = min(pool_min, pool_max)
596
+
597
+ return DatabaseLimits(
598
+ pool_min_size=pool_min,
599
+ pool_max_size=pool_max,
600
+ pool_acquire_timeout=_parse_float_config(
601
+ _get_nested(config, "db", "pool", "acquire_timeout"),
602
+ DEFAULT_POOL_ACQUIRE_TIMEOUT,
603
+ HARD_MIN_POOL_ACQUIRE_TIMEOUT,
604
+ HARD_MAX_POOL_ACQUIRE_TIMEOUT,
605
+ ),
606
+ max_retries=_parse_int_config(
607
+ _get_nested(config, "db", "retry", "max_attempts"),
608
+ DEFAULT_DB_MAX_RETRIES,
609
+ HARD_MIN_DB_MAX_RETRIES,
610
+ HARD_MAX_DB_MAX_RETRIES,
611
+ ),
612
+ retry_delay=_parse_float_config(
613
+ _get_nested(config, "db", "retry", "delay"),
614
+ DEFAULT_DB_RETRY_DELAY,
615
+ HARD_MIN_DB_RETRY_DELAY,
616
+ HARD_MAX_DB_RETRY_DELAY,
617
+ ),
618
+ )
619
+
620
+
621
+ @dataclass(frozen=True, slots=True)
622
+ class RapiLimits:
623
+ """Resolved RAPI configuration limits.
624
+
625
+ Attributes:
626
+ timeout: Request timeout in seconds.
627
+ max_response_size: Maximum response size in bytes.
628
+ max_retries: Maximum retry attempts.
629
+ retry_delay: Delay between retries in seconds.
630
+ retry_backoff: Backoff multiplier for exponential retry.
631
+ """
632
+
633
+ timeout: float
634
+ max_response_size: int
635
+ max_retries: int
636
+ retry_delay: float
637
+ retry_backoff: float
638
+
639
+ @property
640
+ def max_response_size_display(self) -> str:
641
+ """Human-readable response size limit."""
642
+ return format_bytes(self.max_response_size)
643
+
644
+
645
+ def get_rapi_limits(
646
+ config: Mapping[str, Any] | None = None,
647
+ ) -> RapiLimits:
648
+ """Resolve RAPI limits from config with hard limit enforcement.
649
+
650
+ Args:
651
+ config: Optional config mapping. If None, loads from get_config().
652
+
653
+ Returns:
654
+ RapiLimits with resolved values clamped to hard bounds.
655
+
656
+ Examples:
657
+ >>> limits = get_rapi_limits()
658
+ >>> limits.timeout
659
+ 30.0
660
+ >>> limits.max_retries
661
+ 3
662
+ """
663
+ if config is None:
664
+ config = _load_config()
665
+
666
+ # Parse max_response_size (supports human-readable strings like "10M")
667
+ raw_response_size = _get_nested(config, "rapi", "limits", "max_response_size")
668
+ if raw_response_size is not None:
669
+ try:
670
+ configured_size = parse_size_string(raw_response_size)
671
+ except ValueError:
672
+ configured_size = DEFAULT_RAPI_MAX_RESPONSE_SIZE
673
+ else:
674
+ configured_size = DEFAULT_RAPI_MAX_RESPONSE_SIZE
675
+
676
+ max_response_size = min(configured_size, HARD_MAX_RAPI_RESPONSE_SIZE)
677
+
678
+ return RapiLimits(
679
+ timeout=_parse_float_config(
680
+ _get_nested(config, "rapi", "limits", "timeout"),
681
+ DEFAULT_RAPI_TIMEOUT,
682
+ HARD_MIN_RAPI_TIMEOUT,
683
+ HARD_MAX_RAPI_TIMEOUT,
684
+ ),
685
+ max_response_size=max_response_size,
686
+ max_retries=_parse_int_config(
687
+ _get_nested(config, "rapi", "limits", "max_retries"),
688
+ DEFAULT_RAPI_MAX_RETRIES,
689
+ HARD_MIN_RAPI_RETRIES,
690
+ HARD_MAX_RAPI_RETRIES,
691
+ ),
692
+ retry_delay=_parse_float_config(
693
+ _get_nested(config, "rapi", "limits", "retry_delay"),
694
+ DEFAULT_RAPI_RETRY_DELAY,
695
+ HARD_MIN_RAPI_RETRY_DELAY,
696
+ HARD_MAX_RAPI_RETRY_DELAY,
697
+ ),
698
+ retry_backoff=_parse_float_config(
699
+ _get_nested(config, "rapi", "limits", "retry_backoff"),
700
+ DEFAULT_RAPI_BACKOFF,
701
+ HARD_MIN_RAPI_BACKOFF,
702
+ HARD_MAX_RAPI_BACKOFF,
703
+ ),
704
+ )
705
+
706
+
707
+ #: Default JSON indentation for pretty-print (spaces)
708
+ DEFAULT_RAPI_JSON_INDENT = 2
709
+
710
+ #: Default XML pretty-print enabled
711
+ DEFAULT_RAPI_XML_PRETTY = True
712
+
713
+
714
+ @dataclass(frozen=True, slots=True)
715
+ class RapiRenderConfig:
716
+ """RAPI CLI output rendering configuration.
717
+
718
+ Attributes:
719
+ json_indent: JSON indentation (spaces). None or 0 to disable pretty-print.
720
+ xml_pretty: Whether to enable XML pretty-printing.
721
+ """
722
+
723
+ json_indent: int | None
724
+ xml_pretty: bool
725
+
726
+
727
+ def get_rapi_render_config(
728
+ config: Mapping[str, Any] | None = None,
729
+ ) -> RapiRenderConfig:
730
+ """Resolve RAPI rendering config for CLI output.
731
+
732
+ Args:
733
+ config: Optional config mapping. If None, loads from get_config().
734
+
735
+ Returns:
736
+ RapiRenderConfig with resolved values.
737
+
738
+ Examples:
739
+ >>> render_config = get_rapi_render_config()
740
+ >>> render_config.json_indent
741
+ 2
742
+ >>> render_config.xml_pretty
743
+ True
744
+ """
745
+ if config is None:
746
+ config = _load_config()
747
+
748
+ # Parse JSON indent (int or None)
749
+ raw_json = _get_nested(config, "rapi", "pretty_render", "json")
750
+ if raw_json is None:
751
+ json_indent: int | None = DEFAULT_RAPI_JSON_INDENT
752
+ elif raw_json == 0:
753
+ json_indent = None
754
+ else:
755
+ try:
756
+ json_indent = int(raw_json)
757
+ # Clamp to reasonable bounds (1-8 spaces)
758
+ json_indent = max(1, min(json_indent, 8))
759
+ except (TypeError, ValueError):
760
+ json_indent = DEFAULT_RAPI_JSON_INDENT
761
+
762
+ # Parse XML pretty (bool)
763
+ raw_xml = _get_nested(config, "rapi", "pretty_render", "xml")
764
+ xml_pretty = DEFAULT_RAPI_XML_PRETTY if raw_xml is None else bool(raw_xml)
765
+
766
+ return RapiRenderConfig(
767
+ json_indent=json_indent,
768
+ xml_pretty=xml_pretty,
769
+ )
770
+
771
+
772
+ @dataclass(frozen=True, slots=True)
773
+ class AlertsLimits:
774
+ """Resolved alerts configuration limits.
775
+
776
+ Attributes:
777
+ throttle_rate: Maximum alerts per period.
778
+ throttle_per: Period duration in seconds.
779
+ throttle_burst: Initial burst capacity.
780
+ channel_timeout: Timeout for sending alerts (seconds).
781
+ channel_retries: Retry attempts on delivery failure.
782
+ """
783
+
784
+ throttle_rate: int
785
+ throttle_per: float
786
+ throttle_burst: int
787
+ channel_timeout: float
788
+ channel_retries: int
789
+
790
+
791
+ def get_alerts_limits(
792
+ config: Mapping[str, Any] | None = None,
793
+ ) -> AlertsLimits:
794
+ """Resolve alerts limits from config with hard limit enforcement.
795
+
796
+ Args:
797
+ config: Optional config mapping. If None, loads from get_config().
798
+
799
+ Returns:
800
+ AlertsLimits with resolved values clamped to hard bounds.
801
+
802
+ Examples:
803
+ >>> limits = get_alerts_limits()
804
+ >>> limits.throttle_rate
805
+ 10
806
+ >>> limits.throttle_per
807
+ 60.0
808
+ """
809
+ if config is None:
810
+ config = _load_config()
811
+
812
+ rate = _parse_int_config(
813
+ _get_nested(config, "alerts", "throttle", "rate"),
814
+ DEFAULT_THROTTLE_RATE,
815
+ HARD_MIN_THROTTLE_RATE,
816
+ HARD_MAX_THROTTLE_RATE,
817
+ )
818
+
819
+ per = _parse_float_config(
820
+ _get_nested(config, "alerts", "throttle", "per"),
821
+ DEFAULT_THROTTLE_PER,
822
+ HARD_MIN_THROTTLE_PER,
823
+ HARD_MAX_THROTTLE_PER,
824
+ )
825
+
826
+ # Burst defaults to rate if not specified, clamped to [1, rate]
827
+ raw_burst = _get_nested(config, "alerts", "throttle", "burst")
828
+ burst = rate if raw_burst is None else _parse_int_config(raw_burst, rate, 1, rate)
829
+
830
+ return AlertsLimits(
831
+ throttle_rate=rate,
832
+ throttle_per=per,
833
+ throttle_burst=burst,
834
+ channel_timeout=_parse_float_config(
835
+ _get_nested(config, "alerts", "channels", "timeout"),
836
+ DEFAULT_CHANNEL_TIMEOUT,
837
+ HARD_MIN_CHANNEL_TIMEOUT,
838
+ HARD_MAX_CHANNEL_TIMEOUT,
839
+ ),
840
+ channel_retries=_parse_int_config(
841
+ _get_nested(config, "alerts", "channels", "max_retries"),
842
+ DEFAULT_CHANNEL_RETRIES,
843
+ HARD_MIN_CHANNEL_RETRIES,
844
+ HARD_MAX_CHANNEL_RETRIES,
845
+ ),
846
+ )
847
+
848
+
849
+ @dataclass(frozen=True, slots=True)
850
+ class WebSocketLimits:
851
+ """Resolved WebSocket configuration limits.
852
+
853
+ Includes settings for connection management, reconnection behavior,
854
+ and proactive control features.
855
+
856
+ Attributes:
857
+ ping_interval: Seconds between ping frames.
858
+ ping_timeout: Seconds to wait for pong response.
859
+ connection_timeout: Timeout for initial connection.
860
+ reconnect_delay: Initial delay between reconnect attempts.
861
+ max_reconnect_delay: Maximum delay for exponential backoff.
862
+ max_reconnect_attempts: Maximum consecutive reconnection attempts.
863
+ queue_size: Maximum messages in queue (0 = unlimited).
864
+ disconnect_check_interval: Seconds between should_disconnect checks.
865
+ reconnect_check_interval: Seconds between should_reconnect checks.
866
+ disconnect_margin: Seconds before platform limit to disconnect.
867
+ """
868
+
869
+ ping_interval: float
870
+ ping_timeout: float
871
+ connection_timeout: float
872
+ reconnect_delay: float
873
+ max_reconnect_delay: float
874
+ max_reconnect_attempts: int
875
+ queue_size: int
876
+ disconnect_check_interval: float
877
+ reconnect_check_interval: float
878
+ disconnect_margin: float
879
+
880
+
881
+ def get_websocket_limits(
882
+ config: Mapping[str, Any] | None = None,
883
+ ) -> WebSocketLimits:
884
+ """Resolve WebSocket limits from config with hard limit enforcement.
885
+
886
+ Args:
887
+ config: Optional config mapping. If None, loads from get_config().
888
+
889
+ Returns:
890
+ WebSocketLimits with resolved values clamped to hard bounds.
891
+
892
+ Examples:
893
+ >>> limits = get_websocket_limits()
894
+ >>> limits.ping_interval
895
+ 20.0
896
+ >>> limits.max_reconnect_attempts
897
+ 10
898
+ """
899
+ if config is None:
900
+ config = _load_config()
901
+
902
+ return WebSocketLimits(
903
+ ping_interval=_parse_float_config(
904
+ _get_nested(config, "websocket", "ping", "interval"),
905
+ DEFAULT_WS_PING_INTERVAL,
906
+ HARD_MIN_WS_PING_INTERVAL,
907
+ HARD_MAX_WS_PING_INTERVAL,
908
+ ),
909
+ ping_timeout=_parse_float_config(
910
+ _get_nested(config, "websocket", "ping", "timeout"),
911
+ DEFAULT_WS_PING_TIMEOUT,
912
+ HARD_MIN_WS_PING_TIMEOUT,
913
+ HARD_MAX_WS_PING_TIMEOUT,
914
+ ),
915
+ connection_timeout=_parse_float_config(
916
+ _get_nested(config, "websocket", "connection", "timeout"),
917
+ DEFAULT_WS_CONNECTION_TIMEOUT,
918
+ HARD_MIN_WS_CONNECTION_TIMEOUT,
919
+ HARD_MAX_WS_CONNECTION_TIMEOUT,
920
+ ),
921
+ reconnect_delay=_parse_float_config(
922
+ _get_nested(config, "websocket", "reconnect", "delay"),
923
+ DEFAULT_WS_RECONNECT_DELAY,
924
+ HARD_MIN_WS_RECONNECT_DELAY,
925
+ HARD_MAX_WS_RECONNECT_DELAY,
926
+ ),
927
+ max_reconnect_delay=_parse_float_config(
928
+ _get_nested(config, "websocket", "reconnect", "max_delay"),
929
+ DEFAULT_WS_MAX_RECONNECT_DELAY,
930
+ HARD_MIN_WS_MAX_RECONNECT_DELAY,
931
+ HARD_MAX_WS_MAX_RECONNECT_DELAY,
932
+ ),
933
+ max_reconnect_attempts=_parse_int_config(
934
+ _get_nested(config, "websocket", "reconnect", "max_attempts"),
935
+ DEFAULT_WS_RECONNECT_ATTEMPTS,
936
+ HARD_MIN_WS_RECONNECT_ATTEMPTS,
937
+ HARD_MAX_WS_RECONNECT_ATTEMPTS,
938
+ ),
939
+ queue_size=_parse_int_config(
940
+ _get_nested(config, "websocket", "queue", "size"),
941
+ DEFAULT_WS_QUEUE_SIZE,
942
+ HARD_MIN_WS_QUEUE_SIZE,
943
+ HARD_MAX_WS_QUEUE_SIZE,
944
+ ),
945
+ disconnect_check_interval=_parse_float_config(
946
+ _get_nested(config, "websocket", "proactive", "disconnect_check_interval"),
947
+ DEFAULT_WS_DISCONNECT_CHECK,
948
+ HARD_MIN_WS_DISCONNECT_CHECK,
949
+ HARD_MAX_WS_DISCONNECT_CHECK,
950
+ ),
951
+ reconnect_check_interval=_parse_float_config(
952
+ _get_nested(config, "websocket", "proactive", "reconnect_check_interval"),
953
+ DEFAULT_WS_RECONNECT_CHECK,
954
+ HARD_MIN_WS_RECONNECT_CHECK,
955
+ HARD_MAX_WS_RECONNECT_CHECK,
956
+ ),
957
+ disconnect_margin=_parse_float_config(
958
+ _get_nested(config, "websocket", "proactive", "disconnect_margin"),
959
+ DEFAULT_WS_DISCONNECT_MARGIN,
960
+ HARD_MIN_WS_DISCONNECT_MARGIN,
961
+ HARD_MAX_WS_DISCONNECT_MARGIN,
962
+ ),
963
+ )