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/config/export.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Configuration export helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
from configparser import ConfigParser
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from importlib import import_module, resources
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Final, cast
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from kstlib.config.loader import CONFIG_FILENAME
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
_TOMLI_W: Any = import_module("tomli_w")
|
|
20
|
+
except ModuleNotFoundError: # pragma: no cover - dependency optional for tests until installed
|
|
21
|
+
_TOMLI_W = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_SUPPORTED_EXTENSIONS: Final[dict[str, str]] = {
|
|
25
|
+
".yml": "yaml",
|
|
26
|
+
".yaml": "yaml",
|
|
27
|
+
".json": "json",
|
|
28
|
+
".toml": "toml",
|
|
29
|
+
".ini": "ini",
|
|
30
|
+
}
|
|
31
|
+
_DEFAULT_FORMAT: Final[str] = "yaml"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConfigExportError(RuntimeError):
|
|
35
|
+
"""Raised when configuration export fails."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ConfigExportOptions:
|
|
40
|
+
"""Options controlling the configuration export behavior."""
|
|
41
|
+
|
|
42
|
+
section: str | None = None
|
|
43
|
+
out_path: Path | None = None
|
|
44
|
+
stdout: bool = False
|
|
45
|
+
force: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ConfigExportResult:
|
|
50
|
+
"""Outcome of a configuration export."""
|
|
51
|
+
|
|
52
|
+
destination: Path | None
|
|
53
|
+
content: str | None
|
|
54
|
+
format_name: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def export_configuration(options: ConfigExportOptions) -> ConfigExportResult:
|
|
58
|
+
"""Export the packaged configuration to disk or stdout."""
|
|
59
|
+
if options.stdout and options.out_path is not None:
|
|
60
|
+
raise ConfigExportError("Cannot combine --stdout with --out; choose one destination.")
|
|
61
|
+
|
|
62
|
+
resource = resources.files("kstlib").joinpath(CONFIG_FILENAME)
|
|
63
|
+
with resources.as_file(resource) as source_path:
|
|
64
|
+
if not source_path.is_file():
|
|
65
|
+
raise ConfigExportError("Packaged configuration file is missing.")
|
|
66
|
+
|
|
67
|
+
if options.section is None:
|
|
68
|
+
return _export_full_config(source_path, options)
|
|
69
|
+
|
|
70
|
+
data = _load_yaml(source_path)
|
|
71
|
+
selected, path_parts = _select_section(data, options.section)
|
|
72
|
+
wrapped = _wrap_with_path(selected, path_parts)
|
|
73
|
+
format_name, destination = _resolve_output(options, _DEFAULT_FORMAT)
|
|
74
|
+
|
|
75
|
+
serialized = _serialize_data(wrapped, format_name)
|
|
76
|
+
|
|
77
|
+
if options.stdout:
|
|
78
|
+
return ConfigExportResult(destination=None, content=serialized, format_name=format_name)
|
|
79
|
+
|
|
80
|
+
_write_text(serialized, destination, options.force)
|
|
81
|
+
return ConfigExportResult(destination=destination, content=None, format_name=format_name)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _export_full_config(source_path: Path, options: ConfigExportOptions) -> ConfigExportResult:
|
|
85
|
+
if options.stdout:
|
|
86
|
+
return ConfigExportResult(
|
|
87
|
+
destination=None,
|
|
88
|
+
content=source_path.read_text(encoding="utf-8"),
|
|
89
|
+
format_name=_DEFAULT_FORMAT,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
format_name, destination = _resolve_output(options, _DEFAULT_FORMAT)
|
|
93
|
+
|
|
94
|
+
if format_name == "yaml":
|
|
95
|
+
_copy_file(source_path, destination, options.force)
|
|
96
|
+
return ConfigExportResult(destination=destination, content=None, format_name=format_name)
|
|
97
|
+
|
|
98
|
+
data = _load_yaml(source_path)
|
|
99
|
+
serialized = _serialize_data(data, format_name)
|
|
100
|
+
_write_text(serialized, destination, options.force)
|
|
101
|
+
return ConfigExportResult(destination=destination, content=None, format_name=format_name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _serialize_data(data: Any, format_name: str) -> str:
|
|
105
|
+
if format_name == "yaml":
|
|
106
|
+
return yaml.safe_dump(data, sort_keys=False)
|
|
107
|
+
if format_name == "json":
|
|
108
|
+
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
109
|
+
if format_name == "toml":
|
|
110
|
+
if _TOMLI_W is None:
|
|
111
|
+
raise ConfigExportError("TOML export requires the 'tomli-w' package.")
|
|
112
|
+
return cast("str", _TOMLI_W.dumps(data))
|
|
113
|
+
if format_name == "ini":
|
|
114
|
+
parser = ConfigParser()
|
|
115
|
+
flattened = _flatten_for_ini(data)
|
|
116
|
+
for section, values in flattened.items():
|
|
117
|
+
parser[section] = values
|
|
118
|
+
buffer = io.StringIO()
|
|
119
|
+
parser.write(buffer)
|
|
120
|
+
return buffer.getvalue()
|
|
121
|
+
raise ConfigExportError(f"Unsupported output format '{format_name}'.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _flatten_for_ini(data: Any) -> dict[str, dict[str, str]]:
|
|
125
|
+
if not isinstance(data, dict):
|
|
126
|
+
raise ConfigExportError("INI export requires dictionary data.")
|
|
127
|
+
|
|
128
|
+
result: dict[str, dict[str, str]] = {}
|
|
129
|
+
for section, value in data.items():
|
|
130
|
+
section_name = str(section)
|
|
131
|
+
entries: dict[str, str] = {}
|
|
132
|
+
if isinstance(value, dict):
|
|
133
|
+
for key, item in _walk_items(value):
|
|
134
|
+
entries[key] = _stringify(item)
|
|
135
|
+
else:
|
|
136
|
+
entries["value"] = _stringify(value)
|
|
137
|
+
result[section_name] = entries
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _walk_items(value: Any, prefix: str | None = None) -> list[tuple[str, Any]]:
|
|
142
|
+
if isinstance(value, dict):
|
|
143
|
+
items: list[tuple[str, Any]] = []
|
|
144
|
+
for key, child in value.items():
|
|
145
|
+
sub_key = f"{prefix}.{key}" if prefix else str(key)
|
|
146
|
+
items.extend(_walk_items(child, sub_key))
|
|
147
|
+
return items
|
|
148
|
+
if isinstance(value, list):
|
|
149
|
+
return [(f"{prefix}[{idx}]", child) for idx, child in enumerate(value)]
|
|
150
|
+
return [(prefix or "value", value)]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _stringify(value: Any) -> str:
|
|
154
|
+
if isinstance(value, dict | list):
|
|
155
|
+
return json.dumps(value, ensure_ascii=False)
|
|
156
|
+
return str(value)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _resolve_output(options: ConfigExportOptions, default_format: str) -> tuple[str, Path]:
|
|
160
|
+
out_path = options.out_path
|
|
161
|
+
if out_path is None:
|
|
162
|
+
destination = Path.cwd() / CONFIG_FILENAME
|
|
163
|
+
elif out_path.suffix and out_path.suffix in _SUPPORTED_EXTENSIONS:
|
|
164
|
+
destination = out_path
|
|
165
|
+
elif out_path.exists() and out_path.is_dir():
|
|
166
|
+
destination = out_path / CONFIG_FILENAME
|
|
167
|
+
elif out_path.suffix:
|
|
168
|
+
# Unknown suffix, treat as file but default format
|
|
169
|
+
destination = out_path
|
|
170
|
+
else:
|
|
171
|
+
destination = out_path / CONFIG_FILENAME
|
|
172
|
+
|
|
173
|
+
suffix = destination.suffix.lower()
|
|
174
|
+
format_name = _SUPPORTED_EXTENSIONS.get(suffix, default_format)
|
|
175
|
+
|
|
176
|
+
if suffix and suffix.lower() not in _SUPPORTED_EXTENSIONS:
|
|
177
|
+
if format_name != default_format:
|
|
178
|
+
raise ConfigExportError(f"Unsupported file extension '{suffix}'.")
|
|
179
|
+
destination = destination.with_suffix(f".{default_format}")
|
|
180
|
+
|
|
181
|
+
return format_name, destination
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _copy_file(source: Path, destination: Path, force: bool) -> None:
|
|
185
|
+
if destination.exists() and not force:
|
|
186
|
+
raise ConfigExportError(f"Destination '{destination}' already exists. Use --force to overwrite.")
|
|
187
|
+
destination.parent.mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
188
|
+
shutil.copy2(source, destination)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _write_text(content: str, destination: Path, force: bool) -> None:
|
|
192
|
+
if destination.exists() and not force:
|
|
193
|
+
raise ConfigExportError(f"Destination '{destination}' already exists. Use --force to overwrite.")
|
|
194
|
+
destination.parent.mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
195
|
+
destination.write_text(content, encoding="utf-8")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _load_yaml(path: Path) -> dict[str, Any]:
|
|
199
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _select_section(data: dict[str, Any], dotted_path: str) -> tuple[Any, list[str]]:
|
|
203
|
+
path_parts = dotted_path.split(".")
|
|
204
|
+
current: Any = data
|
|
205
|
+
for part in path_parts:
|
|
206
|
+
if isinstance(current, dict) and part in current:
|
|
207
|
+
current = current[part]
|
|
208
|
+
continue
|
|
209
|
+
raise ConfigExportError(f"Section '{dotted_path}' not found in default configuration.")
|
|
210
|
+
return current, path_parts
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _wrap_with_path(value: Any, path_parts: list[str]) -> Any:
|
|
214
|
+
wrapped: Any = value
|
|
215
|
+
for part in reversed(path_parts):
|
|
216
|
+
wrapped = {part: wrapped}
|
|
217
|
+
return wrapped
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__: Final[tuple[str, ...]] = (
|
|
221
|
+
"ConfigExportError",
|
|
222
|
+
"ConfigExportOptions",
|
|
223
|
+
"ConfigExportResult",
|
|
224
|
+
"export_configuration",
|
|
225
|
+
)
|