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.
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.0.dist-info/METADATA +201 -0
  159. kstlib-1.0.0.dist-info/RECORD +163 -0
  160. {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
  161. kstlib-1.0.0.dist-info/entry_points.txt +2 -0
  162. kstlib-1.0.0.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.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,242 @@
1
+ """Encrypt command for secrets subsystem."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ import typer
9
+
10
+ from kstlib.cli.common import (
11
+ CommandResult,
12
+ CommandStatus,
13
+ console,
14
+ exit_with_result,
15
+ render_result,
16
+ )
17
+
18
+ from .common import (
19
+ AGE_RECIPIENT_OPTION,
20
+ CONFIG_OPTION,
21
+ DATA_KEY_OPTION,
22
+ ENCRYPT_SOURCE_ARG,
23
+ FORCE_OPTION,
24
+ FORMATS_OPTION,
25
+ KMS_KEY_OPTION,
26
+ OUT_OPTION,
27
+ QUIET_OPTION,
28
+ SHRED_CHUNK_SIZE_OPTION,
29
+ SHRED_METHOD_OPTION,
30
+ SHRED_OPTION,
31
+ SHRED_PASSES_OPTION,
32
+ SHRED_ZERO_LAST_OPTION,
33
+ EncryptCommandOptions,
34
+ SecureDeleteCLIOptions,
35
+ format_arguments,
36
+ resolve_sops_binary,
37
+ run_sops_command,
38
+ shred_file,
39
+ )
40
+
41
+ if TYPE_CHECKING:
42
+ from subprocess import CompletedProcess
43
+
44
+ from kstlib.utils.secure_delete import SecureDeleteReport
45
+
46
+
47
+ def _ensure_encrypt_destination(options: EncryptCommandOptions) -> None:
48
+ """Abort when the target output is not writable."""
49
+ if options.out is None or not options.out.exists() or options.force:
50
+ return
51
+
52
+ result = CommandResult(
53
+ status=CommandStatus.ERROR,
54
+ message=f"Refuse to overwrite existing file: {options.out} (use --force).",
55
+ )
56
+ exit_with_result(result, options.quiet, exit_code=1)
57
+
58
+
59
+ def _run_encrypt_command(source: Path, options: EncryptCommandOptions) -> CompletedProcess[str]:
60
+ """Execute the ``sops`` encryption command and handle failures."""
61
+ try:
62
+ completed = run_sops_command(
63
+ options.binary,
64
+ _build_encrypt_args(source, options),
65
+ )
66
+ except FileNotFoundError as exc:
67
+ result = CommandResult(
68
+ status=CommandStatus.ERROR,
69
+ message=(f"SOPS binary '{options.binary}' not found. Install it or set secrets.sops.binary in the config."),
70
+ )
71
+ exit_with_result(result, options.quiet, exit_code=1, cause=exc)
72
+
73
+ if completed.returncode != 0:
74
+ message = completed.stderr.strip() or completed.stdout.strip() or "sops command failed"
75
+ result = CommandResult(status=CommandStatus.ERROR, message=message)
76
+ exit_with_result(result, options.quiet, exit_code=1)
77
+
78
+ return completed
79
+
80
+
81
+ def _maybe_print_encrypt_output(completed: CompletedProcess[str], options: EncryptCommandOptions) -> None:
82
+ """Display stdout emitted by ``sops`` when appropriate."""
83
+ if options.out is not None:
84
+ return
85
+ if options.quiet or not completed.stdout:
86
+ return
87
+ console.print(completed.stdout.rstrip("\n"))
88
+
89
+
90
+ def _handle_shred_request(source: Path, options: EncryptCommandOptions) -> SecureDeleteReport | None:
91
+ """Perform secure delete when requested, aborting on failure."""
92
+ if not options.shred.enabled:
93
+ return None
94
+
95
+ shred_opts = options.shred
96
+ report = shred_file(
97
+ source,
98
+ method=shred_opts.method,
99
+ passes=shred_opts.passes,
100
+ zero_last_pass=shred_opts.zero_last_pass,
101
+ chunk_size=shred_opts.chunk_size,
102
+ )
103
+
104
+ if report.success:
105
+ return report
106
+
107
+ reason = f" {report.message}" if report.message else ""
108
+ result = CommandResult(
109
+ status=CommandStatus.ERROR,
110
+ message=f"Failed to remove cleartext source '{source}'.{reason}",
111
+ )
112
+ return exit_with_result(result, options.quiet, exit_code=1)
113
+
114
+
115
+ def _compose_encrypt_success_message(
116
+ source: Path,
117
+ options: EncryptCommandOptions,
118
+ report: SecureDeleteReport | None,
119
+ ) -> str:
120
+ """Return the final success message for ``encrypt``."""
121
+ target_info = str(options.out) if options.out else "stdout"
122
+ message = f"Encrypted secrets written to {target_info}."
123
+
124
+ if report is not None:
125
+ detail = f"{report.method.value} ({report.passes} passes)"
126
+ if report.command:
127
+ detail += f" via {' '.join(report.command)}"
128
+ return message + f" Cleartext source '{source}' removed using {detail}."
129
+
130
+ if not options.shred.enabled:
131
+ message += f" Cleartext source '{source}' still exists; run 'kstlib secrets shred {source}' to remove it."
132
+
133
+ return message
134
+
135
+
136
+ def _build_encrypt_args(
137
+ source: Path,
138
+ options: EncryptCommandOptions,
139
+ ) -> list[str]:
140
+ """Build arguments for ``sops --encrypt``."""
141
+ arguments = _base_encrypt_args(options)
142
+ arguments.extend(format_arguments(*options.formats))
143
+ arguments.extend(_recipient_flags(options))
144
+ arguments.extend(_key_flags(options))
145
+ arguments.append(str(source))
146
+ return arguments
147
+
148
+
149
+ def _base_encrypt_args(options: EncryptCommandOptions) -> list[str]:
150
+ """Return base arguments shared by every encrypt invocation."""
151
+ arguments: list[str] = []
152
+ if options.config is not None:
153
+ arguments.extend(["--config", str(options.config)])
154
+ arguments.append("--encrypt")
155
+ if options.out is not None:
156
+ arguments.extend(["--output", str(options.out)])
157
+ return arguments
158
+
159
+
160
+ def _recipient_flags(options: EncryptCommandOptions) -> list[str]:
161
+ """Return age or KMS recipient flag arguments."""
162
+ arguments: list[str] = []
163
+ if options.age_recipients:
164
+ for recipient in options.age_recipients:
165
+ arguments.extend(["--age", recipient])
166
+ if options.kms_keys:
167
+ for kms_key in options.kms_keys:
168
+ arguments.extend(["--kms", kms_key])
169
+ return arguments
170
+
171
+
172
+ def _key_flags(options: EncryptCommandOptions) -> list[str]:
173
+ """Return raw data key flag arguments."""
174
+ arguments: list[str] = []
175
+ if options.data_keys:
176
+ for data_key in options.data_keys:
177
+ arguments.extend(["--key", data_key])
178
+ return arguments
179
+
180
+
181
+ def _execute_encrypt(source: Path, options: EncryptCommandOptions) -> None:
182
+ """Perform the encryption workflow using the provided options."""
183
+ _ensure_encrypt_destination(options)
184
+ completed = _run_encrypt_command(source, options)
185
+ _maybe_print_encrypt_output(completed, options)
186
+ shred_report = _handle_shred_request(source, options)
187
+ message = _compose_encrypt_success_message(source, options, shred_report)
188
+ result = CommandResult(status=CommandStatus.OK, message=message)
189
+ if options.quiet:
190
+ raise typer.Exit(code=0)
191
+ render_result(result)
192
+
193
+
194
+ # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
195
+ def encrypt(
196
+ source: Path = ENCRYPT_SOURCE_ARG,
197
+ *,
198
+ out: Path | None = OUT_OPTION,
199
+ config: Path | None = CONFIG_OPTION,
200
+ formats: tuple[str, str] = FORMATS_OPTION,
201
+ force: bool = FORCE_OPTION,
202
+ quiet: bool = QUIET_OPTION,
203
+ shred_enabled: bool = SHRED_OPTION,
204
+ shred_method: str | None = SHRED_METHOD_OPTION,
205
+ shred_passes: int | None = SHRED_PASSES_OPTION,
206
+ shred_zero_last_pass: bool | None = SHRED_ZERO_LAST_OPTION,
207
+ shred_chunk_size: int | None = SHRED_CHUNK_SIZE_OPTION,
208
+ age_recipient: list[str] | None = AGE_RECIPIENT_OPTION,
209
+ kms_key: list[str] | None = KMS_KEY_OPTION,
210
+ key: list[str] | None = DATA_KEY_OPTION,
211
+ ) -> None:
212
+ """Encrypt a cleartext file using sops."""
213
+ binary = resolve_sops_binary()
214
+ effective_config = config
215
+ if effective_config is None:
216
+ default_config = Path.home() / ".sops.yaml"
217
+ if default_config.exists():
218
+ effective_config = default_config
219
+
220
+ options = EncryptCommandOptions(
221
+ out=out,
222
+ binary=binary,
223
+ config=effective_config,
224
+ formats=formats,
225
+ force=force,
226
+ quiet=quiet,
227
+ shred=SecureDeleteCLIOptions(
228
+ enabled=shred_enabled,
229
+ method=shred_method,
230
+ passes=shred_passes,
231
+ zero_last_pass=shred_zero_last_pass,
232
+ chunk_size=shred_chunk_size,
233
+ ),
234
+ age_recipients=tuple(age_recipient or []),
235
+ kms_keys=tuple(kms_key or []),
236
+ data_keys=tuple(key or []),
237
+ )
238
+
239
+ _execute_encrypt(source, options)
240
+
241
+
242
+ __all__ = ["encrypt"]
@@ -0,0 +1,96 @@
1
+ """Shred command for secrets subsystem."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import typer
8
+
9
+ from kstlib.cli.common import (
10
+ CommandResult,
11
+ CommandStatus,
12
+ exit_with_result,
13
+ render_result,
14
+ )
15
+
16
+ from .common import (
17
+ SHRED_CMD_CHUNK_SIZE_OPTION,
18
+ SHRED_CMD_METHOD_OPTION,
19
+ SHRED_CMD_PASSES_OPTION,
20
+ SHRED_CMD_QUIET_OPTION,
21
+ SHRED_CMD_ZERO_LAST_OPTION,
22
+ SHRED_FORCE_OPTION,
23
+ SHRED_TARGET_ARG,
24
+ ShredCommandOptions,
25
+ shred_file,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from pathlib import Path
30
+ else: # pragma: no cover - runtime alias for Typer conversions
31
+ import pathlib
32
+
33
+ Path = pathlib.Path
34
+
35
+
36
+ def _execute_shred(target: Path, options: ShredCommandOptions) -> None:
37
+ """Perform the shredding workflow using the provided options."""
38
+ if not options.force:
39
+ confirmed = typer.confirm(f"Remove '{target}' permanently?", default=False)
40
+ if not confirmed:
41
+ result = CommandResult(
42
+ status=CommandStatus.WARNING,
43
+ message=f"Shred aborted; file '{target}' not removed.",
44
+ )
45
+ exit_with_result(result, options.quiet, exit_code=1)
46
+
47
+ report = shred_file(
48
+ target,
49
+ method=options.method,
50
+ passes=options.passes,
51
+ zero_last_pass=options.zero_last_pass,
52
+ chunk_size=options.chunk_size,
53
+ )
54
+ if not report.success:
55
+ result = CommandResult(
56
+ status=CommandStatus.ERROR,
57
+ message=report.message or f"Failed to remove '{target}'. Check permissions and try again.",
58
+ )
59
+ exit_with_result(result, options.quiet, exit_code=1)
60
+
61
+ detail = f"{report.method.value} ({report.passes} passes)"
62
+ if report.command:
63
+ detail += f" via {' '.join(report.command)}"
64
+ result = CommandResult(
65
+ status=CommandStatus.OK,
66
+ message=f"Secret file '{target}' removed using {detail}.",
67
+ )
68
+ if options.quiet:
69
+ raise typer.Exit(code=0)
70
+ render_result(result)
71
+
72
+
73
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
74
+ def shred(
75
+ target: Path = SHRED_TARGET_ARG,
76
+ *,
77
+ force: bool = SHRED_FORCE_OPTION,
78
+ method: str | None = SHRED_CMD_METHOD_OPTION,
79
+ passes: int | None = SHRED_CMD_PASSES_OPTION,
80
+ zero_last_pass: bool | None = SHRED_CMD_ZERO_LAST_OPTION,
81
+ chunk_size: int | None = SHRED_CMD_CHUNK_SIZE_OPTION,
82
+ quiet: bool = SHRED_CMD_QUIET_OPTION,
83
+ ) -> None:
84
+ """Remove a secrets file from disk."""
85
+ options = ShredCommandOptions(
86
+ force=force,
87
+ method=method,
88
+ passes=passes,
89
+ zero_last_pass=zero_last_pass,
90
+ chunk_size=chunk_size,
91
+ quiet=quiet,
92
+ )
93
+ _execute_shred(target, options)
94
+
95
+
96
+ __all__ = ["shred"]
kstlib/cli/common.py ADDED
@@ -0,0 +1,86 @@
1
+ """Shared CLI utilities for kstlib commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any, NoReturn
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.pretty import Pretty
13
+
14
+ console = Console()
15
+
16
+
17
+ class CommandStatus(str, Enum):
18
+ """Represents the outcome of a CLI command."""
19
+
20
+ OK = "ok"
21
+ WARNING = "warning"
22
+ ERROR = "error"
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class CommandResult:
27
+ """Summary payload produced by a command handler."""
28
+
29
+ status: CommandStatus
30
+ message: str
31
+ payload: dict[str, Any] | None = None
32
+
33
+
34
+ STYLE_MAP = {
35
+ CommandStatus.OK: "green",
36
+ CommandStatus.WARNING: "yellow",
37
+ CommandStatus.ERROR: "red",
38
+ }
39
+
40
+
41
+ def render_result(result: CommandResult) -> None:
42
+ """Render a command result using Rich components."""
43
+ style = STYLE_MAP[result.status]
44
+ console.print(Panel(result.message, title=result.status.value.upper(), style=style, border_style=style))
45
+ if result.payload is not None:
46
+ console.print(Pretty(result.payload))
47
+
48
+
49
+ def emit_result(result: CommandResult, quiet: bool) -> None:
50
+ """Output a command result honoring the quiet flag."""
51
+ if quiet:
52
+ console.print(result.message, style=STYLE_MAP[result.status])
53
+ else:
54
+ render_result(result)
55
+
56
+
57
+ def exit_with_result(
58
+ result: CommandResult,
59
+ quiet: bool,
60
+ exit_code: int,
61
+ *,
62
+ cause: Exception | None = None,
63
+ ) -> NoReturn:
64
+ """Render ``result`` and raise ``typer.Exit`` with ``exit_code``."""
65
+ emit_result(result, quiet)
66
+ if cause is None:
67
+ raise typer.Exit(code=exit_code)
68
+ raise typer.Exit(code=exit_code) from cause
69
+
70
+
71
+ def exit_error(message: str) -> NoReturn:
72
+ """Render an error result and exit with code 1."""
73
+ render_result(CommandResult(status=CommandStatus.ERROR, message=message))
74
+ raise typer.Exit(code=1)
75
+
76
+
77
+ __all__ = [
78
+ "STYLE_MAP",
79
+ "CommandResult",
80
+ "CommandStatus",
81
+ "console",
82
+ "emit_result",
83
+ "exit_error",
84
+ "exit_with_result",
85
+ "render_result",
86
+ ]
@@ -0,0 +1,76 @@
1
+ """Configuration management module.
2
+
3
+ This module provides flexible configuration loading from various file formats
4
+ with support for includes, environment variables, and cascading search.
5
+
6
+ SOPS Integration:
7
+ Files with .sops.yml, .sops.yaml, .sops.json, or .sops.toml extensions
8
+ are automatically decrypted via the SOPS binary when loaded.
9
+ """
10
+
11
+ # pylint: disable=duplicate-code
12
+ from kstlib.config.exceptions import (
13
+ ConfigCircularIncludeError,
14
+ ConfigError,
15
+ ConfigFileNotFoundError,
16
+ ConfigFormatError,
17
+ ConfigIncludeDepthError,
18
+ ConfigNotLoadedError,
19
+ ConfigSopsError,
20
+ ConfigSopsNotAvailableError,
21
+ KstlibError,
22
+ )
23
+ from kstlib.config.export import (
24
+ ConfigExportError,
25
+ ConfigExportOptions,
26
+ ConfigExportResult,
27
+ export_configuration,
28
+ )
29
+ from kstlib.config.loader import (
30
+ CONFIG_FILENAME,
31
+ ConfigLoader,
32
+ clear_config,
33
+ get_config,
34
+ load_config,
35
+ load_from_env,
36
+ load_from_file,
37
+ require_config,
38
+ )
39
+ from kstlib.config.sops import (
40
+ SopsDecryptor,
41
+ get_decryptor,
42
+ get_real_extension,
43
+ has_encrypted_values,
44
+ is_sops_file,
45
+ reset_decryptor,
46
+ )
47
+
48
+ __all__ = [
49
+ "CONFIG_FILENAME",
50
+ "ConfigCircularIncludeError",
51
+ "ConfigError",
52
+ "ConfigExportError",
53
+ "ConfigExportOptions",
54
+ "ConfigExportResult",
55
+ "ConfigFileNotFoundError",
56
+ "ConfigFormatError",
57
+ "ConfigIncludeDepthError",
58
+ "ConfigLoader",
59
+ "ConfigNotLoadedError",
60
+ "ConfigSopsError",
61
+ "ConfigSopsNotAvailableError",
62
+ "KstlibError",
63
+ "SopsDecryptor",
64
+ "clear_config",
65
+ "export_configuration",
66
+ "get_config",
67
+ "get_decryptor",
68
+ "get_real_extension",
69
+ "has_encrypted_values",
70
+ "is_sops_file",
71
+ "load_config",
72
+ "load_from_env",
73
+ "load_from_file",
74
+ "require_config",
75
+ "reset_decryptor",
76
+ ]
@@ -0,0 +1,110 @@
1
+ """
2
+ Exception hierarchy for kstlib.
3
+
4
+ All kstlib exceptions inherit from KstlibError, allowing users to catch
5
+ all kstlib-specific errors with a single except clause.
6
+
7
+ Example:
8
+ try:
9
+ config = load_config()
10
+ except KstlibError as e:
11
+ # Catch any kstlib error
12
+ print(f"Error: {e}")
13
+ """
14
+
15
+
16
+ class KstlibError(Exception):
17
+ """
18
+ Base exception for all kstlib errors.
19
+
20
+ All kstlib-specific exceptions inherit from this class,
21
+ allowing for easy catching of any kstlib error.
22
+ """
23
+
24
+
25
+ # ============================================================================
26
+ # Configuration module exceptions
27
+ # ============================================================================
28
+
29
+
30
+ class ConfigError(KstlibError):
31
+ """
32
+ Base exception for configuration-related errors.
33
+
34
+ All config module exceptions inherit from this class.
35
+ """
36
+
37
+
38
+ class ConfigFileNotFoundError(ConfigError, FileNotFoundError):
39
+ """
40
+ Configuration file not found at specified location.
41
+
42
+ Raised when attempting to load a config file that doesn't exist.
43
+ """
44
+
45
+
46
+ class ConfigFormatError(ConfigError, ValueError):
47
+ """
48
+ Invalid configuration format or unsupported file type.
49
+
50
+ Raised when:
51
+ - File extension is not supported (.xml, etc.)
52
+ - Format mismatch in strict mode (YAML including JSON)
53
+ - Invalid content that cannot be parsed
54
+ """
55
+
56
+
57
+ class ConfigCircularIncludeError(ConfigError, ValueError):
58
+ """
59
+ Circular include detected in configuration files.
60
+
61
+ Raised when an include chain creates a cycle (A includes B, B includes A).
62
+ """
63
+
64
+
65
+ class ConfigIncludeDepthError(ConfigError, ValueError):
66
+ """
67
+ Include depth limit exceeded.
68
+
69
+ Raised when config includes are nested too deeply, which may indicate
70
+ a misconfiguration or an attempt to exhaust resources.
71
+ """
72
+
73
+
74
+ class ConfigNotLoadedError(ConfigError, RuntimeError):
75
+ """
76
+ Configuration not loaded yet.
77
+
78
+ Raised by require_config() when attempting to access config
79
+ before it has been loaded via get_config() or load_config().
80
+ """
81
+
82
+
83
+ class ConfigSopsError(ConfigError):
84
+ """
85
+ SOPS decryption failed for a configuration file.
86
+
87
+ Raised when SOPS binary fails to decrypt a .sops.* file.
88
+ """
89
+
90
+
91
+ class ConfigSopsNotAvailableError(ConfigSopsError):
92
+ """
93
+ SOPS binary not installed or not found in PATH.
94
+
95
+ Raised when attempting to decrypt a .sops.* file but the SOPS
96
+ binary is not available. Install from https://github.com/getsops/sops
97
+ """
98
+
99
+
100
+ __all__ = [
101
+ "ConfigCircularIncludeError",
102
+ "ConfigError",
103
+ "ConfigFileNotFoundError",
104
+ "ConfigFormatError",
105
+ "ConfigIncludeDepthError",
106
+ "ConfigNotLoadedError",
107
+ "ConfigSopsError",
108
+ "ConfigSopsNotAvailableError",
109
+ "KstlibError",
110
+ ]