kstlib 0.0.1a0__py3-none-any.whl → 1.0.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.
- 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.0.dist-info/METADATA +201 -0
- kstlib-1.0.0.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
- kstlib-1.0.0.dist-info/entry_points.txt +2 -0
- kstlib-1.0.0.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.0.dist-info}/top_level.txt +0 -0
kstlib/ops/validators.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Input validation for kstlib.ops module.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions for all user-exposed values
|
|
4
|
+
in the ops module, implementing deep defense against malformed or
|
|
5
|
+
malicious input.
|
|
6
|
+
|
|
7
|
+
Hard limits are enforced to prevent resource exhaustion and ensure
|
|
8
|
+
predictable behavior across all backends.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# Constants - Hard Limits
|
|
17
|
+
# ============================================================================
|
|
18
|
+
|
|
19
|
+
# Session name limits
|
|
20
|
+
MAX_SESSION_NAME_LENGTH = 64
|
|
21
|
+
SESSION_NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
|
|
22
|
+
|
|
23
|
+
# Container image limits
|
|
24
|
+
MAX_IMAGE_NAME_LENGTH = 256
|
|
25
|
+
# OCI image name: registry/path:tag@digest format
|
|
26
|
+
IMAGE_NAME_PATTERN = re.compile(
|
|
27
|
+
r"^[a-z0-9]([a-z0-9._-]*[a-z0-9])?(/[a-z0-9]([a-z0-9._-]*[a-z0-9])?)*"
|
|
28
|
+
r"(:[a-zA-Z0-9._-]+)?(@sha256:[a-f0-9]{64})?$"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Volume limits
|
|
32
|
+
MAX_VOLUMES = 20
|
|
33
|
+
VOLUME_PATTERN = re.compile(r"^[^:]+:[^:]+(:ro|:rw)?$")
|
|
34
|
+
|
|
35
|
+
# Port limits
|
|
36
|
+
MAX_PORTS = 50
|
|
37
|
+
PORT_PATTERN = re.compile(r"^(\d{1,5}:)?\d{1,5}(/tcp|/udp)?$")
|
|
38
|
+
|
|
39
|
+
# Environment variable limits
|
|
40
|
+
MAX_ENV_VARS = 100
|
|
41
|
+
MAX_ENV_KEY_LENGTH = 128
|
|
42
|
+
MAX_ENV_VALUE_LENGTH = 32768 # 32KB
|
|
43
|
+
|
|
44
|
+
# Command limits
|
|
45
|
+
MAX_COMMAND_LENGTH = 4096
|
|
46
|
+
|
|
47
|
+
# Dangerous patterns to block in commands
|
|
48
|
+
DANGEROUS_PATTERNS = [
|
|
49
|
+
r";\s*rm\s+-rf", # rm -rf after semicolon
|
|
50
|
+
r"\$\(.*\)", # Command substitution
|
|
51
|
+
r"`.*`", # Backtick substitution
|
|
52
|
+
r"\|\s*sh\b", # Pipe to shell
|
|
53
|
+
r"\|\s*bash\b", # Pipe to bash
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# Validation Functions
|
|
59
|
+
# ============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_session_name(name: str) -> str:
|
|
63
|
+
"""Validate and return session name.
|
|
64
|
+
|
|
65
|
+
Rules:
|
|
66
|
+
- Cannot be empty
|
|
67
|
+
- Max 64 characters (hard limit)
|
|
68
|
+
- Must start with letter
|
|
69
|
+
- Only alphanumeric, underscore, hyphen allowed
|
|
70
|
+
- No shell metacharacters
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
name: Session name to validate.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The validated session name (unchanged).
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If name is invalid.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> validate_session_name("mybot")
|
|
83
|
+
'mybot'
|
|
84
|
+
>>> validate_session_name("my-bot_01")
|
|
85
|
+
'my-bot_01'
|
|
86
|
+
>>> validate_session_name("")
|
|
87
|
+
Traceback (most recent call last):
|
|
88
|
+
...
|
|
89
|
+
ValueError: Session name cannot be empty
|
|
90
|
+
"""
|
|
91
|
+
if not name:
|
|
92
|
+
raise ValueError("Session name cannot be empty")
|
|
93
|
+
if len(name) > MAX_SESSION_NAME_LENGTH:
|
|
94
|
+
raise ValueError(f"Session name too long (max {MAX_SESSION_NAME_LENGTH} chars)")
|
|
95
|
+
if not SESSION_NAME_PATTERN.match(name):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Session name must start with letter and contain only alphanumeric, underscore, or hyphen characters"
|
|
98
|
+
)
|
|
99
|
+
return name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_image_name(image: str) -> str:
|
|
103
|
+
"""Validate container image name.
|
|
104
|
+
|
|
105
|
+
Rules:
|
|
106
|
+
- Max 256 characters
|
|
107
|
+
- Valid Docker/OCI image format
|
|
108
|
+
- No shell injection characters
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
image: Container image name to validate.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The validated image name (unchanged).
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If image name is invalid.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
>>> validate_image_name("python:3.10-slim")
|
|
121
|
+
'python:3.10-slim'
|
|
122
|
+
>>> validate_image_name("registry.io/path/image:tag")
|
|
123
|
+
'registry.io/path/image:tag'
|
|
124
|
+
"""
|
|
125
|
+
if not image:
|
|
126
|
+
raise ValueError("Image name cannot be empty")
|
|
127
|
+
if len(image) > MAX_IMAGE_NAME_LENGTH:
|
|
128
|
+
raise ValueError(f"Image name too long (max {MAX_IMAGE_NAME_LENGTH} chars)")
|
|
129
|
+
if not IMAGE_NAME_PATTERN.match(image):
|
|
130
|
+
raise ValueError(f"Invalid image name format: {image}")
|
|
131
|
+
return image
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_volumes(volumes: list[str]) -> list[str]:
|
|
135
|
+
"""Validate volume mappings.
|
|
136
|
+
|
|
137
|
+
Rules:
|
|
138
|
+
- Max 20 volumes
|
|
139
|
+
- Valid format: host:container[:ro|:rw]
|
|
140
|
+
- No path traversal (..)
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
volumes: List of volume mappings to validate.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The validated volumes list (unchanged).
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If volumes are invalid.
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
>>> validate_volumes(["./data:/app/data"])
|
|
153
|
+
['./data:/app/data']
|
|
154
|
+
>>> validate_volumes(["./logs:/app/logs:ro"])
|
|
155
|
+
['./logs:/app/logs:ro']
|
|
156
|
+
"""
|
|
157
|
+
if len(volumes) > MAX_VOLUMES:
|
|
158
|
+
raise ValueError(f"Too many volumes (max {MAX_VOLUMES})")
|
|
159
|
+
for vol in volumes:
|
|
160
|
+
if not VOLUME_PATTERN.match(vol):
|
|
161
|
+
raise ValueError(f"Invalid volume format: {vol}")
|
|
162
|
+
if ".." in vol:
|
|
163
|
+
raise ValueError(f"Path traversal not allowed in volume: {vol}")
|
|
164
|
+
return volumes
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def validate_ports(ports: list[str]) -> list[str]:
|
|
168
|
+
"""Validate port mappings.
|
|
169
|
+
|
|
170
|
+
Rules:
|
|
171
|
+
- Max 50 ports
|
|
172
|
+
- Valid format: [host:]container[/tcp|/udp]
|
|
173
|
+
- Port numbers in range 1-65535
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
ports: List of port mappings to validate.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The validated ports list (unchanged).
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
ValueError: If ports are invalid.
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
>>> validate_ports(["8080:80"])
|
|
186
|
+
['8080:80']
|
|
187
|
+
>>> validate_ports(["8080"])
|
|
188
|
+
['8080']
|
|
189
|
+
>>> validate_ports(["8080:80/tcp"])
|
|
190
|
+
['8080:80/tcp']
|
|
191
|
+
"""
|
|
192
|
+
if len(ports) > MAX_PORTS:
|
|
193
|
+
raise ValueError(f"Too many ports (max {MAX_PORTS})")
|
|
194
|
+
for port in ports:
|
|
195
|
+
if not PORT_PATTERN.match(port):
|
|
196
|
+
raise ValueError(f"Invalid port format: {port}")
|
|
197
|
+
# Validate port numbers are in valid range
|
|
198
|
+
nums = re.findall(r"\d+", port)
|
|
199
|
+
for num in nums:
|
|
200
|
+
port_num = int(num)
|
|
201
|
+
if port_num < 1 or port_num > 65535:
|
|
202
|
+
raise ValueError(f"Port number out of range (1-65535): {num}")
|
|
203
|
+
return ports
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def validate_env(env: dict[str, str]) -> dict[str, str]:
|
|
207
|
+
"""Validate environment variables.
|
|
208
|
+
|
|
209
|
+
Rules:
|
|
210
|
+
- Max 100 env vars
|
|
211
|
+
- Key max 128 characters
|
|
212
|
+
- Value max 32KB
|
|
213
|
+
- Key must be valid identifier
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
env: Dictionary of environment variables to validate.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The validated env dict (unchanged).
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
ValueError: If env vars are invalid.
|
|
223
|
+
|
|
224
|
+
Examples:
|
|
225
|
+
>>> validate_env({"APP_ENV": "production"})
|
|
226
|
+
{'APP_ENV': 'production'}
|
|
227
|
+
"""
|
|
228
|
+
if len(env) > MAX_ENV_VARS:
|
|
229
|
+
raise ValueError(f"Too many env vars (max {MAX_ENV_VARS})")
|
|
230
|
+
for key, value in env.items():
|
|
231
|
+
if len(key) > MAX_ENV_KEY_LENGTH:
|
|
232
|
+
raise ValueError(f"Env key too long (max {MAX_ENV_KEY_LENGTH}): {key[:20]}...")
|
|
233
|
+
if len(value) > MAX_ENV_VALUE_LENGTH:
|
|
234
|
+
raise ValueError(f"Env value too long (max {MAX_ENV_VALUE_LENGTH}) for key: {key}")
|
|
235
|
+
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key):
|
|
236
|
+
raise ValueError(f"Invalid env key format: {key}")
|
|
237
|
+
return env
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def validate_command(command: str | None) -> str | None:
|
|
241
|
+
"""Validate command string.
|
|
242
|
+
|
|
243
|
+
Rules:
|
|
244
|
+
- Max 4096 characters
|
|
245
|
+
- No dangerous shell patterns
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
command: Command string to validate (can be None).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
The validated command (unchanged).
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If command is invalid or contains dangerous patterns.
|
|
255
|
+
|
|
256
|
+
Examples:
|
|
257
|
+
>>> validate_command("python -m app")
|
|
258
|
+
'python -m app'
|
|
259
|
+
>>> validate_command(None) is None
|
|
260
|
+
True
|
|
261
|
+
"""
|
|
262
|
+
if command is None:
|
|
263
|
+
return None
|
|
264
|
+
if len(command) > MAX_COMMAND_LENGTH:
|
|
265
|
+
raise ValueError(f"Command too long (max {MAX_COMMAND_LENGTH} chars)")
|
|
266
|
+
for pattern in DANGEROUS_PATTERNS:
|
|
267
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
268
|
+
raise ValueError("Potentially dangerous command pattern detected")
|
|
269
|
+
return command
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
__all__ = [
|
|
273
|
+
"MAX_COMMAND_LENGTH",
|
|
274
|
+
"MAX_ENV_KEY_LENGTH",
|
|
275
|
+
"MAX_ENV_VALUE_LENGTH",
|
|
276
|
+
"MAX_ENV_VARS",
|
|
277
|
+
"MAX_IMAGE_NAME_LENGTH",
|
|
278
|
+
"MAX_PORTS",
|
|
279
|
+
"MAX_SESSION_NAME_LENGTH",
|
|
280
|
+
"MAX_VOLUMES",
|
|
281
|
+
"validate_command",
|
|
282
|
+
"validate_env",
|
|
283
|
+
"validate_image_name",
|
|
284
|
+
"validate_ports",
|
|
285
|
+
"validate_session_name",
|
|
286
|
+
"validate_volumes",
|
|
287
|
+
]
|
kstlib/py.typed
ADDED
|
File without changes
|
kstlib/rapi/__init__.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""REST API wrapper module (config-driven).
|
|
2
|
+
|
|
3
|
+
This module provides a config-driven REST API client with multi-source
|
|
4
|
+
credential resolution and detailed logging.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Config-driven endpoints from kstlib.conf.yml or external *.rapi.yml files
|
|
8
|
+
- Auto-discovery of *.rapi.yml files in current directory
|
|
9
|
+
- Multi-source credentials (env, file, sops, provider)
|
|
10
|
+
- Header merging at three levels (service, endpoint, runtime)
|
|
11
|
+
- Automatic retry with exponential backoff
|
|
12
|
+
- TRACE-level logging for debugging
|
|
13
|
+
- Hard limits with deep defense
|
|
14
|
+
|
|
15
|
+
Quick Start:
|
|
16
|
+
>>> from kstlib.rapi import call
|
|
17
|
+
>>> response = call("httpbin.get_ip") # doctest: +SKIP
|
|
18
|
+
>>> response.data # doctest: +SKIP
|
|
19
|
+
{'origin': '...'}
|
|
20
|
+
|
|
21
|
+
With Client Instance:
|
|
22
|
+
>>> from kstlib.rapi import RapiClient
|
|
23
|
+
>>> client = RapiClient() # doctest: +SKIP
|
|
24
|
+
>>> response = client.call("httpbin.post_data", body={"key": "value"}) # doctest: +SKIP
|
|
25
|
+
|
|
26
|
+
From External YAML File:
|
|
27
|
+
>>> from kstlib.rapi import RapiClient
|
|
28
|
+
>>> client = RapiClient.from_file("github.rapi.yml") # doctest: +SKIP
|
|
29
|
+
>>> response = client.call("github.user") # doctest: +SKIP
|
|
30
|
+
|
|
31
|
+
Auto-Discovery:
|
|
32
|
+
>>> from kstlib.rapi import RapiClient
|
|
33
|
+
>>> client = RapiClient.discover() # Finds *.rapi.yml in cwd # doctest: +SKIP
|
|
34
|
+
>>> client.list_apis() # doctest: +SKIP
|
|
35
|
+
['github', 'slack']
|
|
36
|
+
|
|
37
|
+
Async:
|
|
38
|
+
>>> from kstlib.rapi import call_async
|
|
39
|
+
>>> response = await call_async("httpbin.get_ip") # doctest: +SKIP
|
|
40
|
+
|
|
41
|
+
Configuration:
|
|
42
|
+
Configure endpoints in kstlib.conf.yml:
|
|
43
|
+
|
|
44
|
+
.. code-block:: yaml
|
|
45
|
+
|
|
46
|
+
rapi:
|
|
47
|
+
limits:
|
|
48
|
+
timeout: 30
|
|
49
|
+
max_response_size: "10M"
|
|
50
|
+
max_retries: 3
|
|
51
|
+
|
|
52
|
+
api:
|
|
53
|
+
httpbin:
|
|
54
|
+
base_url: "https://httpbin.org"
|
|
55
|
+
endpoints:
|
|
56
|
+
get_ip:
|
|
57
|
+
path: "/ip"
|
|
58
|
+
post_data:
|
|
59
|
+
path: "/post"
|
|
60
|
+
method: POST
|
|
61
|
+
|
|
62
|
+
Or use external *.rapi.yml files (simplified format):
|
|
63
|
+
|
|
64
|
+
.. code-block:: yaml
|
|
65
|
+
|
|
66
|
+
# github.rapi.yml
|
|
67
|
+
name: github
|
|
68
|
+
base_url: "https://api.github.com"
|
|
69
|
+
credentials:
|
|
70
|
+
type: sops
|
|
71
|
+
path: "./tokens/github.sops.json"
|
|
72
|
+
token_path: ".access_token"
|
|
73
|
+
auth:
|
|
74
|
+
type: bearer
|
|
75
|
+
endpoints:
|
|
76
|
+
user:
|
|
77
|
+
path: "/user"
|
|
78
|
+
repos:
|
|
79
|
+
path: "/user/repos"
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
from kstlib.rapi.client import RapiClient, RapiResponse, call, call_async
|
|
83
|
+
from kstlib.rapi.config import (
|
|
84
|
+
ApiConfig,
|
|
85
|
+
EndpointConfig,
|
|
86
|
+
HmacConfig,
|
|
87
|
+
RapiConfigManager,
|
|
88
|
+
load_rapi_config,
|
|
89
|
+
)
|
|
90
|
+
from kstlib.rapi.credentials import CredentialRecord, CredentialResolver
|
|
91
|
+
from kstlib.rapi.exceptions import (
|
|
92
|
+
CredentialError,
|
|
93
|
+
EndpointAmbiguousError,
|
|
94
|
+
EndpointNotFoundError,
|
|
95
|
+
RapiError,
|
|
96
|
+
RequestError,
|
|
97
|
+
ResponseTooLargeError,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"ApiConfig",
|
|
102
|
+
"CredentialError",
|
|
103
|
+
"CredentialRecord",
|
|
104
|
+
"CredentialResolver",
|
|
105
|
+
"EndpointAmbiguousError",
|
|
106
|
+
"EndpointConfig",
|
|
107
|
+
"EndpointNotFoundError",
|
|
108
|
+
"HmacConfig",
|
|
109
|
+
"RapiClient",
|
|
110
|
+
"RapiConfigManager",
|
|
111
|
+
"RapiError",
|
|
112
|
+
"RapiResponse",
|
|
113
|
+
"RequestError",
|
|
114
|
+
"ResponseTooLargeError",
|
|
115
|
+
"call",
|
|
116
|
+
"call_async",
|
|
117
|
+
"load_rapi_config",
|
|
118
|
+
]
|