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,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
+ )