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
@@ -0,0 +1,341 @@
1
+ """Make API calls from the command line."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated, Any, cast
7
+
8
+ import typer
9
+
10
+ from kstlib.cli.common import CommandResult, CommandStatus, console, exit_error, exit_with_result
11
+ from kstlib.limits import get_rapi_render_config
12
+ from kstlib.rapi import (
13
+ CredentialError,
14
+ EndpointAmbiguousError,
15
+ EndpointNotFoundError,
16
+ RapiClient,
17
+ RapiResponse,
18
+ RequestError,
19
+ ResponseTooLargeError,
20
+ load_rapi_config,
21
+ )
22
+ from kstlib.utils.serialization import is_xml_content, to_json, to_xml
23
+
24
+
25
+ def _parse_args(
26
+ args: list[str],
27
+ ) -> tuple[list[str], dict[str, str]]:
28
+ """Parse positional and keyword arguments.
29
+
30
+ Args:
31
+ args: List of arguments like ["3", "foo=bar", "count=42"].
32
+
33
+ Returns:
34
+ Tuple of (positional_args, keyword_args).
35
+
36
+ Examples:
37
+ >>> _parse_args(["3", "foo=bar", "count=42"])
38
+ (['3'], {'foo': 'bar', 'count': '42'})
39
+ >>> _parse_args(["value1", "value2"])
40
+ (['value1', 'value2'], {})
41
+ """
42
+ positional: list[str] = []
43
+ keyword: dict[str, str] = {}
44
+
45
+ for arg in args:
46
+ if "=" in arg:
47
+ key, value = arg.split("=", 1)
48
+ keyword[key] = value
49
+ else:
50
+ positional.append(arg)
51
+
52
+ return positional, keyword
53
+
54
+
55
+ def _parse_headers(headers: list[str]) -> dict[str, str]:
56
+ """Parse header arguments.
57
+
58
+ Args:
59
+ headers: List of headers like ["Accept: application/json", "X-Debug: true"].
60
+
61
+ Returns:
62
+ Dictionary of header name to value.
63
+
64
+ Raises:
65
+ typer.Exit: If header format is invalid.
66
+ """
67
+ result: dict[str, str] = {}
68
+ for header in headers:
69
+ if ":" not in header:
70
+ exit_error(f"Invalid header format: '{header}'\nExpected: 'Header-Name: value'")
71
+ name, value = header.split(":", 1)
72
+ result[name.strip()] = value.strip()
73
+ return result
74
+
75
+
76
+ def _parse_body(body: str | None) -> dict[str, Any] | list[Any] | None:
77
+ """Parse JSON body string or load from file.
78
+
79
+ Supports reading from file with @filename syntax (like curl).
80
+
81
+ Args:
82
+ body: JSON string, @filename reference, or None.
83
+
84
+ Returns:
85
+ Parsed JSON object or None.
86
+
87
+ Raises:
88
+ typer.Exit: If body is not valid JSON or file not found.
89
+
90
+ Examples:
91
+ >>> _parse_body('{"key": "value"}')
92
+ {'key': 'value'}
93
+ >>> _parse_body('@data.json') # Reads from file
94
+ {'key': 'value'}
95
+ """
96
+ if body is None:
97
+ return None
98
+
99
+ # Support @filename syntax (like curl)
100
+ if body.startswith("@"):
101
+ from pathlib import Path
102
+
103
+ filepath = Path(body[1:])
104
+ try:
105
+ content = filepath.read_text(encoding="utf-8")
106
+ except FileNotFoundError:
107
+ exit_error(f"Body file not found: {filepath}")
108
+ except OSError as e:
109
+ exit_error(f"Failed to read body file '{filepath}': {e}")
110
+ else:
111
+ content = body
112
+
113
+ try:
114
+ return json.loads(content) # type: ignore[no-any-return]
115
+ except json.JSONDecodeError as e:
116
+ exit_error(f"Invalid JSON body: {e}")
117
+
118
+
119
+ def _format_output(
120
+ response: RapiResponse,
121
+ fmt: str,
122
+ quiet: bool,
123
+ out_file: str | None = None,
124
+ ) -> None:
125
+ """Format and print response output.
126
+
127
+ Args:
128
+ response: The API response to format.
129
+ fmt: Output format (json, text, full).
130
+ quiet: Whether to suppress rich formatting.
131
+ out_file: Optional file path to write output to.
132
+ """
133
+ # Load render config for pretty-print settings
134
+ render_config = get_rapi_render_config()
135
+ content_type = response.headers.get("content-type", "")
136
+
137
+ # Build output content
138
+ if fmt == "full":
139
+ result_data = {
140
+ "endpoint": response.endpoint_ref,
141
+ "status_code": response.status_code,
142
+ "ok": response.ok,
143
+ "elapsed": f"{response.elapsed:.3f}s",
144
+ "headers": dict(response.headers),
145
+ "data": response.data,
146
+ }
147
+ content = to_json(result_data, indent=render_config.json_indent or 2)
148
+ elif fmt == "text":
149
+ # Text format: apply XML pretty-print if enabled and content is XML
150
+ if render_config.xml_pretty and is_xml_content(response.text, content_type):
151
+ content = to_xml(response.text)
152
+ else:
153
+ content = response.text
154
+ elif response.data is not None:
155
+ # JSON data available: format with configured indent
156
+ content = to_json(response.data, indent=render_config.json_indent or 2)
157
+ elif render_config.xml_pretty and is_xml_content(response.text, content_type):
158
+ # No JSON data but XML detected: pretty-print if enabled
159
+ content = to_xml(response.text)
160
+ else:
161
+ # Raw text fallback
162
+ content = response.text
163
+
164
+ # Write to file or print
165
+ if out_file:
166
+ from pathlib import Path
167
+
168
+ Path(out_file).write_text(content, encoding="utf-8")
169
+ if not quiet:
170
+ console.print(f"[green]Output written to:[/green] {out_file}")
171
+ elif quiet or fmt == "text" or (fmt == "json" and response.data is None):
172
+ print(content)
173
+ else:
174
+ console.print_json(content)
175
+
176
+
177
+ def call(
178
+ endpoint: Annotated[
179
+ str,
180
+ typer.Argument(help="Endpoint reference (e.g., 'github.user' or 'api.endpoint')."),
181
+ ],
182
+ args: Annotated[
183
+ list[str] | None,
184
+ typer.Argument(
185
+ help="Path/query params: positional for path, key=value for query.",
186
+ ),
187
+ ] = None,
188
+ body: Annotated[
189
+ str | None,
190
+ typer.Option(
191
+ "--body",
192
+ "-b",
193
+ help="JSON body or @filename to read from file.",
194
+ ),
195
+ ] = None,
196
+ header: Annotated[
197
+ list[str] | None,
198
+ typer.Option(
199
+ "--header",
200
+ "-H",
201
+ help="Custom header (can be repeated). Format: 'Name: value'.",
202
+ ),
203
+ ] = None,
204
+ fmt: Annotated[
205
+ str,
206
+ typer.Option(
207
+ "--format",
208
+ "-f",
209
+ help="Output format: json, text, or full.",
210
+ ),
211
+ ] = "json",
212
+ out: Annotated[
213
+ str | None,
214
+ typer.Option(
215
+ "--out",
216
+ "-o",
217
+ help="Write output to file (for scripting).",
218
+ ),
219
+ ] = None,
220
+ quiet: Annotated[
221
+ bool,
222
+ typer.Option(
223
+ "--quiet",
224
+ "-q",
225
+ help="Suppress status messages, only output response.",
226
+ ),
227
+ ] = False,
228
+ ) -> None:
229
+ """Make an API call to a configured endpoint.
230
+
231
+ Examples:
232
+ # Simple GET (implicit call)
233
+ kstlib rapi github.user
234
+
235
+ # GET with path parameters
236
+ kstlib rapi github.repos-get owner=KaminoU repo=igcv3
237
+
238
+ # POST with JSON body from file (recommended for complex JSON)
239
+ kstlib rapi myapi.create-item -b @data.json
240
+
241
+ # Custom headers
242
+ kstlib rapi github.user -H "X-Debug: true"
243
+
244
+ # Output to file (for scripting)
245
+ kstlib rapi github.user -o user.json
246
+
247
+ # Full format with file output
248
+ kstlib rapi github.user -f full -o result.json
249
+
250
+ # Quiet mode (JSON only, no formatting)
251
+ kstlib rapi github.rate-limit -q
252
+ """
253
+ # Parse arguments
254
+ positional_args, keyword_args = _parse_args(args or [])
255
+ headers = _parse_headers(header or [])
256
+ parsed_body = _parse_body(body)
257
+
258
+ # Validate output format
259
+ if fmt not in ("json", "text", "full"):
260
+ exit_error(f"Invalid output format: '{fmt}'\nValid formats: json, text, full")
261
+
262
+ try:
263
+ # Create client and make call
264
+ config_manager = load_rapi_config()
265
+ client = RapiClient(config_manager=config_manager)
266
+
267
+ response = client.call(
268
+ endpoint,
269
+ *positional_args,
270
+ body=parsed_body,
271
+ headers=headers if headers else None,
272
+ **cast("dict[str, Any]", keyword_args),
273
+ )
274
+
275
+ # Format and print output
276
+ _format_output(response, fmt, quiet, out)
277
+
278
+ # Exit with appropriate code
279
+ if not response.ok:
280
+ raise typer.Exit(code=1)
281
+
282
+ except EndpointNotFoundError as e:
283
+ exit_with_result(
284
+ CommandResult(
285
+ status=CommandStatus.ERROR,
286
+ message=f"Endpoint not found: {e.endpoint_ref}",
287
+ payload={"searched_apis": e.searched_apis} if e.searched_apis else None,
288
+ ),
289
+ quiet=quiet,
290
+ exit_code=1,
291
+ cause=e,
292
+ )
293
+ except EndpointAmbiguousError as e:
294
+ exit_with_result(
295
+ CommandResult(
296
+ status=CommandStatus.ERROR,
297
+ message=f"Ambiguous endpoint: '{e.endpoint_name}' exists in multiple APIs",
298
+ payload={"matching_apis": e.matching_apis},
299
+ ),
300
+ quiet=quiet,
301
+ exit_code=1,
302
+ cause=e,
303
+ )
304
+ except CredentialError as e:
305
+ exit_with_result(
306
+ CommandResult(
307
+ status=CommandStatus.ERROR,
308
+ message=f"Credential error: {e}",
309
+ payload={"credential_name": e.credential_name} if e.credential_name else None,
310
+ ),
311
+ quiet=quiet,
312
+ exit_code=1,
313
+ cause=e,
314
+ )
315
+ except RequestError as e:
316
+ exit_with_result(
317
+ CommandResult(
318
+ status=CommandStatus.ERROR,
319
+ message=f"Request failed: {e}",
320
+ payload={
321
+ "status_code": e.status_code,
322
+ "retryable": e.retryable,
323
+ },
324
+ ),
325
+ quiet=quiet,
326
+ exit_code=1,
327
+ cause=e,
328
+ )
329
+ except ResponseTooLargeError as e:
330
+ exit_with_result(
331
+ CommandResult(
332
+ status=CommandStatus.ERROR,
333
+ message=f"Response too large: {e.response_size} bytes (max: {e.max_size})",
334
+ ),
335
+ quiet=quiet,
336
+ exit_code=1,
337
+ cause=e,
338
+ )
339
+
340
+
341
+ __all__ = ["call"]
@@ -0,0 +1,99 @@
1
+ """List available API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.table import Table
9
+
10
+ from kstlib.cli.common import console
11
+ from kstlib.rapi import load_rapi_config
12
+
13
+
14
+ def list_endpoints(
15
+ api: Annotated[
16
+ str | None,
17
+ typer.Argument(help="Filter by API name (optional)."),
18
+ ] = None,
19
+ verbose: Annotated[
20
+ bool,
21
+ typer.Option(
22
+ "--verbose",
23
+ "-v",
24
+ help="Show additional details (method, auth, headers).",
25
+ ),
26
+ ] = False,
27
+ ) -> None:
28
+ """List all configured API endpoints.
29
+
30
+ Examples:
31
+ # List all endpoints
32
+ kstlib rapi list
33
+
34
+ # List endpoints for specific API
35
+ kstlib rapi list github
36
+
37
+ # Verbose output with methods and auth
38
+ kstlib rapi list -v
39
+ """
40
+ try:
41
+ config_manager = load_rapi_config()
42
+ except Exception as e: # pylint: disable=broad-exception-caught
43
+ console.print(f"[red]Failed to load rapi config: {e}[/]")
44
+ raise typer.Exit(code=1) from e
45
+
46
+ apis = config_manager.apis
47
+
48
+ if not apis:
49
+ console.print("[yellow]No APIs configured in kstlib.conf.yml[/]")
50
+ console.print("[dim]Add APIs under 'rapi.api' section.[/]")
51
+ raise typer.Exit(code=0)
52
+
53
+ # Filter by API name if specified
54
+ if api:
55
+ if api not in apis:
56
+ console.print(f"[red]API '{api}' not found.[/]")
57
+ console.print(f"[dim]Available APIs: {', '.join(apis.keys())}[/]")
58
+ raise typer.Exit(code=1)
59
+ apis = {api: apis[api]}
60
+
61
+ # Build table
62
+ if verbose:
63
+ table = Table(title="Available Endpoints", show_lines=True)
64
+ table.add_column("Reference", style="cyan")
65
+ table.add_column("Method", style="green")
66
+ table.add_column("Path")
67
+ table.add_column("Query", style="yellow")
68
+ else:
69
+ table = Table(title="Available Endpoints")
70
+ table.add_column("Reference", style="cyan")
71
+ table.add_column("Path")
72
+
73
+ for api_name, api_config in sorted(apis.items()):
74
+ for ep_name, ep_config in sorted(api_config.endpoints.items()):
75
+ ref = f"{api_name}.{ep_name}"
76
+
77
+ # Build path display with query param indicator
78
+ path_display = f"[dim]{ep_config.path}[/]"
79
+ if ep_config.query:
80
+ path_display += f" [yellow]({len(ep_config.query)})[/]"
81
+
82
+ if verbose:
83
+ method = ep_config.method.upper()
84
+ # Show query param keys or "-"
85
+ query_info = ", ".join(ep_config.query.keys()) if ep_config.query else "-"
86
+
87
+ table.add_row(ref, method, path_display, query_info)
88
+ else:
89
+ table.add_row(ref, path_display)
90
+
91
+ console.print(table)
92
+
93
+ # Summary
94
+ total_apis = len(apis)
95
+ total_endpoints = sum(len(api.endpoints) for api in apis.values())
96
+ console.print(f"\n[dim]{total_endpoints} endpoints across {total_apis} API(s)[/]")
97
+
98
+
99
+ __all__ = ["list_endpoints"]
@@ -0,0 +1,206 @@
1
+ """Show detailed information for a specific API endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from typing import TYPE_CHECKING, Annotated
8
+
9
+ import typer
10
+ from rich.markup import escape
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from kstlib.cli.common import console
15
+ from kstlib.limits import HARD_MAX_DISPLAY_VALUE_LENGTH, HARD_MAX_ENDPOINT_REF_LENGTH
16
+ from kstlib.rapi import EndpointNotFoundError, load_rapi_config
17
+ from kstlib.rapi.config import _PATH_PARAM_PATTERN
18
+
19
+ if TYPE_CHECKING:
20
+ from kstlib.rapi.config import ApiConfig, EndpointConfig
21
+
22
+ # Allowed characters for endpoint reference: alphanum + underscore + dot + hyphen
23
+ _ENDPOINT_REF_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
24
+
25
+
26
+ def _truncate(value: str, max_length: int = HARD_MAX_DISPLAY_VALUE_LENGTH) -> str:
27
+ """Truncate a string and append ellipsis if too long."""
28
+ if len(value) <= max_length:
29
+ return value
30
+ return value[: max_length - 3] + "..."
31
+
32
+
33
+ def _validate_endpoint_ref(endpoint_ref: str) -> None:
34
+ """Validate endpoint reference for security (deep defense)."""
35
+ if len(endpoint_ref) > HARD_MAX_ENDPOINT_REF_LENGTH:
36
+ console.print(f"[red]Endpoint reference too long: {len(endpoint_ref)} > {HARD_MAX_ENDPOINT_REF_LENGTH}[/]")
37
+ raise typer.Exit(code=1)
38
+
39
+ if not _ENDPOINT_REF_PATTERN.match(endpoint_ref):
40
+ console.print("[red]Endpoint reference contains invalid characters.[/]")
41
+ console.print("[dim]Allowed: alphanumeric, underscore, dot, hyphen[/]")
42
+ raise typer.Exit(code=1)
43
+
44
+
45
+ def _print_basic_info(api_config: ApiConfig, ep_config: EndpointConfig) -> None:
46
+ """Print basic endpoint information table."""
47
+ info_table = Table(show_header=False, box=None, padding=(0, 2))
48
+ info_table.add_column("Label", style="dim")
49
+ info_table.add_column("Value")
50
+
51
+ info_table.add_row("Path:", f"[green]{escape(ep_config.path)}[/]")
52
+ info_table.add_row("Method:", f"[yellow]{ep_config.method}[/]")
53
+ info_table.add_row("API:", api_config.name)
54
+ info_table.add_row("Base URL:", escape(api_config.base_url))
55
+
56
+ console.print(info_table)
57
+ console.print()
58
+
59
+
60
+ def _print_path_params(path_params: list[str]) -> None:
61
+ """Print path parameters section."""
62
+ console.print("[bold]Path Parameters:[/]")
63
+ if path_params:
64
+ for param in path_params:
65
+ suffix = " [dim](positional)[/]" if param.isdigit() else ""
66
+ console.print(f" [cyan]{{{param}}}[/]{suffix}")
67
+ else:
68
+ console.print(" [dim](none)[/]")
69
+ console.print()
70
+
71
+
72
+ def _print_query_params(ep_config: EndpointConfig) -> None:
73
+ """Print default query parameters section."""
74
+ console.print("[bold]Default Query Parameters:[/]")
75
+ if ep_config.query:
76
+ for key, value in ep_config.query.items():
77
+ safe_value = escape(_truncate(str(value)))
78
+ console.print(f" [cyan]{escape(key)}[/] = {safe_value}")
79
+ else:
80
+ console.print(" [dim](none)[/]")
81
+ console.print()
82
+
83
+
84
+ def _print_headers(api_config: ApiConfig, ep_config: EndpointConfig) -> None:
85
+ """Print headers section."""
86
+ console.print("[bold]Headers:[/]")
87
+ service_headers = len(api_config.headers) if api_config.headers else 0
88
+ ep_headers = len(ep_config.headers) if ep_config.headers else 0
89
+ console.print(f" Service: [dim]{service_headers} header(s)[/]")
90
+ console.print(f" Endpoint: [dim]{ep_headers} header(s)[/]")
91
+ console.print()
92
+
93
+
94
+ def _print_auth(api_config: ApiConfig, ep_config: EndpointConfig) -> None:
95
+ """Print authentication section."""
96
+ console.print("[bold]Authentication:[/]")
97
+ if api_config.auth_type:
98
+ console.print(" Required: [yellow]Yes[/]")
99
+ console.print(f" Type: [cyan]{api_config.auth_type}[/]")
100
+ if ep_config.auth is False:
101
+ console.print(" [dim]Note: Auth disabled for this endpoint[/]")
102
+ else:
103
+ console.print(" Required: [green]No[/]")
104
+ console.print()
105
+
106
+
107
+ def _print_body_template(ep_config: EndpointConfig) -> None:
108
+ """Print body template section."""
109
+ console.print("[bold]Body Template:[/]")
110
+ if ep_config.body_template:
111
+ body_str = json.dumps(ep_config.body_template, indent=2)
112
+ safe_body = escape(_truncate(body_str))
113
+ console.print(f" {safe_body}")
114
+ elif ep_config.method in ("POST", "PUT", "PATCH"):
115
+ console.print(" [dim](none - provide via --body)[/]")
116
+ else:
117
+ console.print(f" [dim](none - {ep_config.method} request)[/]")
118
+ console.print()
119
+
120
+
121
+ def _print_examples(ep_config: EndpointConfig, path_params: list[str]) -> None:
122
+ """Print usage examples section."""
123
+ console.print("[bold]Examples:[/]")
124
+ base_cmd = f"kstlib rapi {ep_config.full_ref}"
125
+
126
+ # Build path params string (required in ALL examples)
127
+ # Use escape() to prevent Rich from interpreting <param> as markup tags
128
+ path_args = ""
129
+ if path_params:
130
+ named_args = " ".join(escape(f"<{p}>") for p in path_params if not p.isdigit())
131
+ positional_args = " ".join(escape(f"<arg{p}>") for p in path_params if p.isdigit())
132
+ path_args = " ".join(filter(None, [named_args, positional_args]))
133
+
134
+ # Basic example with required path params
135
+ if path_args:
136
+ console.print(f" [dim]{base_cmd} {path_args}[/]")
137
+ else:
138
+ console.print(f" [dim]{base_cmd}[/]")
139
+
140
+ # Example with query param (path params are REQUIRED, query is optional)
141
+ if ep_config.query:
142
+ first_key = escape(next(iter(ep_config.query)))
143
+ if path_args:
144
+ console.print(f" [dim]{base_cmd} {path_args} {first_key}={escape('<value>')}[/]")
145
+ else:
146
+ console.print(f" [dim]{base_cmd} {first_key}={escape('<value>')}[/]")
147
+
148
+ # Example with body (path params are REQUIRED)
149
+ if ep_config.method in ("POST", "PUT", "PATCH"):
150
+ if path_args:
151
+ console.print(f' [dim]{base_cmd} {path_args} --body \'{{"key": "value"}}\'[/]')
152
+ else:
153
+ console.print(f' [dim]{base_cmd} --body \'{{"key": "value"}}\'[/]')
154
+
155
+ console.print()
156
+
157
+
158
+ def show_endpoint(
159
+ endpoint_ref: Annotated[
160
+ str,
161
+ typer.Argument(help="Endpoint reference (api.endpoint or short form)."),
162
+ ],
163
+ ) -> None:
164
+ """Show detailed information for an API endpoint.
165
+
166
+ Displays full configuration including path parameters, query parameters,
167
+ headers, authentication requirements, and usage examples.
168
+
169
+ Examples:
170
+ # Show endpoint details
171
+ kstlib rapi show httpbin.get_ip
172
+
173
+ # Short form (if unique)
174
+ kstlib rapi show get_ip
175
+ """
176
+ _validate_endpoint_ref(endpoint_ref)
177
+
178
+ try:
179
+ config_manager = load_rapi_config()
180
+ except Exception as e: # pylint: disable=broad-exception-caught
181
+ console.print(f"[red]Failed to load rapi config: {e}[/]")
182
+ raise typer.Exit(code=1) from e
183
+
184
+ try:
185
+ api_config, ep_config = config_manager.resolve(endpoint_ref)
186
+ except EndpointNotFoundError as e:
187
+ console.print(f"[red]Endpoint not found: {endpoint_ref}[/]")
188
+ console.print(f"[dim]Available APIs: {', '.join(e.searched_apis)}[/]")
189
+ raise typer.Exit(code=1) from e
190
+
191
+ path_params = _PATH_PARAM_PATTERN.findall(ep_config.path)
192
+
193
+ console.print()
194
+ console.print(Panel(f"[bold cyan]{ep_config.full_ref}[/]", expand=False))
195
+ console.print()
196
+
197
+ _print_basic_info(api_config, ep_config)
198
+ _print_path_params(path_params)
199
+ _print_query_params(ep_config)
200
+ _print_headers(api_config, ep_config)
201
+ _print_auth(api_config, ep_config)
202
+ _print_body_template(ep_config)
203
+ _print_examples(ep_config, path_params)
204
+
205
+
206
+ __all__ = ["show_endpoint"]
@@ -0,0 +1,35 @@
1
+ """CLI commands for secrets management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .decrypt import decrypt
8
+ from .doctor import doctor, init
9
+ from .encrypt import encrypt
10
+ from .shred import shred
11
+
12
+ secrets_app = typer.Typer(help="Manage encrypted secrets and diagnostics.")
13
+
14
+ # Register commands on the secrets_app
15
+ secrets_app.command()(doctor)
16
+ secrets_app.command()(init)
17
+ secrets_app.command()(encrypt)
18
+ secrets_app.command()(decrypt)
19
+ secrets_app.command()(shred)
20
+
21
+
22
+ def register_cli(app: typer.Typer) -> None:
23
+ """Register the secrets sub-commands on the root Typer app."""
24
+ app.add_typer(secrets_app, name="secrets")
25
+
26
+
27
+ __all__ = [
28
+ "decrypt",
29
+ "doctor",
30
+ "encrypt",
31
+ "init",
32
+ "register_cli",
33
+ "secrets_app",
34
+ "shred",
35
+ ]