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.
- kstlib/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.1.dist-info/METADATA +201 -0
- kstlib-1.0.1.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
- kstlib-1.0.1.dist-info/entry_points.txt +2 -0
- kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {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
|
+
)
|