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/__init__.py
CHANGED
|
@@ -1 +1,266 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
################################################################################################## ### ### ####
|
|
3
|
+
################################################################################################## ### ### ####
|
|
4
|
+
## ##
|
|
5
|
+
## ################## .#######: .######. ########################################## ##
|
|
6
|
+
## ##################..#########. .#########. ########################################## ##
|
|
7
|
+
## ### ########. :###########: ### ##
|
|
8
|
+
## ### ######. .##########:. ### ##
|
|
9
|
+
## ### ####. .#########:. ### ##
|
|
10
|
+
## ### ##. .####### ### ##
|
|
11
|
+
## ### " .###### ### ##
|
|
12
|
+
## ### .:###### ############### ##
|
|
13
|
+
## ### .##########:. .:#######. ############### ##
|
|
14
|
+
## ### :########################################. ### ##
|
|
15
|
+
## ### :################# L I B ##################: ### ##
|
|
16
|
+
## ### .########################################. ### ##
|
|
17
|
+
## ### :######:. ###########. ### ##
|
|
18
|
+
## ### :######:. ### ##
|
|
19
|
+
## ### . :######:. . ### ##
|
|
20
|
+
## ### ## .:######. ## ### ##
|
|
21
|
+
## ### #### .:#########. ### ### ##
|
|
22
|
+
## ### ###### :###########: ### ### ##
|
|
23
|
+
## ### ######## :###########: ### ### ##
|
|
24
|
+
## ##################..##########. ":#########" ################## ##
|
|
25
|
+
## ################## .#########: ":######:" ################## ##
|
|
26
|
+
## ##
|
|
27
|
+
###################################################################################################[ Michel TRUONG ]####
|
|
28
|
+
########################################################################################################################
|
|
29
|
+
kstlib package - PEP 562 lazy loading for fast imports.
|
|
30
|
+
|
|
31
|
+
All public symbols are loaded lazily on first access.
|
|
32
|
+
Import time target: < 100ms (from ~330ms with eager loading).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# pylint: disable=global-statement,import-outside-toplevel,invalid-name
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import importlib
|
|
40
|
+
from typing import TYPE_CHECKING, Any
|
|
41
|
+
|
|
42
|
+
# Public API exports (sorted alphabetically)
|
|
43
|
+
__all__ = [
|
|
44
|
+
"ConfigCircularIncludeError",
|
|
45
|
+
"ConfigError",
|
|
46
|
+
"ConfigFileNotFoundError",
|
|
47
|
+
"ConfigFormatError",
|
|
48
|
+
"ConfigLoader",
|
|
49
|
+
"ConfigNotLoadedError",
|
|
50
|
+
"KstlibError",
|
|
51
|
+
"LogManager",
|
|
52
|
+
"MonitoringError",
|
|
53
|
+
"PanelManager",
|
|
54
|
+
"PanelRenderingError",
|
|
55
|
+
"alerts",
|
|
56
|
+
"app",
|
|
57
|
+
"auth",
|
|
58
|
+
"cache",
|
|
59
|
+
"clear_config",
|
|
60
|
+
"db",
|
|
61
|
+
"get_config",
|
|
62
|
+
"install_rich_traceback",
|
|
63
|
+
"load_config",
|
|
64
|
+
"load_from_env",
|
|
65
|
+
"load_from_file",
|
|
66
|
+
"mail",
|
|
67
|
+
"metrics",
|
|
68
|
+
"monitoring",
|
|
69
|
+
"rapi",
|
|
70
|
+
"require_config",
|
|
71
|
+
"resilience",
|
|
72
|
+
"secrets",
|
|
73
|
+
"secure",
|
|
74
|
+
"ui",
|
|
75
|
+
"utils",
|
|
76
|
+
"websocket",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# Lazy-loaded submodules
|
|
80
|
+
_SUBMODULES: frozenset[str] = frozenset(
|
|
81
|
+
{
|
|
82
|
+
"alerts",
|
|
83
|
+
"app",
|
|
84
|
+
"auth",
|
|
85
|
+
"cache",
|
|
86
|
+
"db",
|
|
87
|
+
"mail",
|
|
88
|
+
"metrics",
|
|
89
|
+
"monitoring",
|
|
90
|
+
"rapi",
|
|
91
|
+
"resilience",
|
|
92
|
+
"secrets",
|
|
93
|
+
"secure",
|
|
94
|
+
"ui",
|
|
95
|
+
"utils",
|
|
96
|
+
"websocket",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Mapping of attribute names to their import paths
|
|
101
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
102
|
+
# Config exceptions and classes
|
|
103
|
+
"ConfigCircularIncludeError": ("kstlib.config", "ConfigCircularIncludeError"),
|
|
104
|
+
"ConfigError": ("kstlib.config", "ConfigError"),
|
|
105
|
+
"ConfigFileNotFoundError": ("kstlib.config", "ConfigFileNotFoundError"),
|
|
106
|
+
"ConfigFormatError": ("kstlib.config", "ConfigFormatError"),
|
|
107
|
+
"ConfigLoader": ("kstlib.config", "ConfigLoader"),
|
|
108
|
+
"ConfigNotLoadedError": ("kstlib.config.exceptions", "ConfigNotLoadedError"),
|
|
109
|
+
"KstlibError": ("kstlib.config", "KstlibError"),
|
|
110
|
+
# Config functional API
|
|
111
|
+
"clear_config": ("kstlib.config.loader", "clear_config"),
|
|
112
|
+
"get_config": ("kstlib.config.loader", "get_config"),
|
|
113
|
+
"load_config": ("kstlib.config.loader", "load_config"),
|
|
114
|
+
"load_from_env": ("kstlib.config.loader", "load_from_env"),
|
|
115
|
+
"load_from_file": ("kstlib.config.loader", "load_from_file"),
|
|
116
|
+
"require_config": ("kstlib.config.loader", "require_config"),
|
|
117
|
+
# Logging
|
|
118
|
+
"LogManager": ("kstlib.logging", "LogManager"),
|
|
119
|
+
# Monitoring
|
|
120
|
+
"MonitoringError": ("kstlib.monitoring", "MonitoringError"),
|
|
121
|
+
# UI
|
|
122
|
+
"PanelManager": ("kstlib.ui", "PanelManager"),
|
|
123
|
+
"PanelRenderingError": ("kstlib.ui", "PanelRenderingError"),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Cache for loaded modules/attributes
|
|
127
|
+
_loaded: dict[str, Any] = {}
|
|
128
|
+
|
|
129
|
+
# Flag to track if rich traceback has been installed
|
|
130
|
+
_traceback_installed: bool = False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def install_rich_traceback() -> None:
|
|
134
|
+
"""Install Rich enhanced tracebacks.
|
|
135
|
+
|
|
136
|
+
Call this explicitly if you want enhanced tracebacks. By default, tracebacks
|
|
137
|
+
are not installed to keep import time fast.
|
|
138
|
+
|
|
139
|
+
To auto-install on import (legacy behavior), set KSTLIB_TRACEBACK=1:
|
|
140
|
+
|
|
141
|
+
- Bash/zsh: export KSTLIB_TRACEBACK=1
|
|
142
|
+
- PowerShell: $env:KSTLIB_TRACEBACK = "1"
|
|
143
|
+
"""
|
|
144
|
+
global _traceback_installed
|
|
145
|
+
if _traceback_installed:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
import shutil
|
|
149
|
+
|
|
150
|
+
from rich.traceback import install
|
|
151
|
+
|
|
152
|
+
terminal_width = shutil.get_terminal_size(fallback=(120, 20)).columns
|
|
153
|
+
install(
|
|
154
|
+
show_locals=True,
|
|
155
|
+
word_wrap=True,
|
|
156
|
+
width=terminal_width,
|
|
157
|
+
extra_lines=7,
|
|
158
|
+
)
|
|
159
|
+
_traceback_installed = True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def __getattr__(name: str) -> Any:
|
|
163
|
+
"""Lazily load modules and attributes on first access (PEP 562)."""
|
|
164
|
+
# Check cache first
|
|
165
|
+
if name in _loaded:
|
|
166
|
+
return _loaded[name]
|
|
167
|
+
|
|
168
|
+
# Handle submodules
|
|
169
|
+
if name in _SUBMODULES:
|
|
170
|
+
if name == "app":
|
|
171
|
+
# Import the Typer app object, not the module
|
|
172
|
+
module = importlib.import_module("kstlib.cli")
|
|
173
|
+
_loaded[name] = module.app
|
|
174
|
+
return module.app
|
|
175
|
+
if name == "cache":
|
|
176
|
+
module = importlib.import_module("kstlib.cache")
|
|
177
|
+
_loaded[name] = module.cache
|
|
178
|
+
return module.cache
|
|
179
|
+
module = importlib.import_module(f"kstlib.{name}")
|
|
180
|
+
_loaded[name] = module
|
|
181
|
+
return module
|
|
182
|
+
|
|
183
|
+
# Handle lazy imports
|
|
184
|
+
if name in _LAZY_IMPORTS:
|
|
185
|
+
module_path, attr_name = _LAZY_IMPORTS[name]
|
|
186
|
+
try:
|
|
187
|
+
module = importlib.import_module(module_path)
|
|
188
|
+
attr = getattr(module, attr_name)
|
|
189
|
+
_loaded[name] = attr
|
|
190
|
+
return attr
|
|
191
|
+
except (ImportError, AttributeError):
|
|
192
|
+
# ConfigNotLoadedError might not exist in minimal installs
|
|
193
|
+
if name == "ConfigNotLoadedError":
|
|
194
|
+
_loaded[name] = None
|
|
195
|
+
return None
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
raise AttributeError(f"module 'kstlib' has no attribute {name!r}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Auto-install rich traceback if KSTLIB_TRACEBACK=1 (opt-in for fast imports)
|
|
202
|
+
# Default changed from "1" to "0" for metricsormance - users must opt-in now
|
|
203
|
+
if TYPE_CHECKING:
|
|
204
|
+
# For static analysis - provide type hints for lazy-loaded symbols
|
|
205
|
+
# pylint: disable=useless-import-alias
|
|
206
|
+
from kstlib import alerts as alerts
|
|
207
|
+
from kstlib import auth as auth
|
|
208
|
+
from kstlib import db as db
|
|
209
|
+
from kstlib import mail as mail
|
|
210
|
+
from kstlib import metrics as metrics
|
|
211
|
+
from kstlib import monitoring as monitoring
|
|
212
|
+
from kstlib import rapi as rapi
|
|
213
|
+
from kstlib import resilience as resilience
|
|
214
|
+
from kstlib import secrets as secrets
|
|
215
|
+
from kstlib import secure as secure
|
|
216
|
+
from kstlib import ui as ui
|
|
217
|
+
from kstlib import utils as utils
|
|
218
|
+
from kstlib import websocket as websocket
|
|
219
|
+
from kstlib.cache import cache as cache
|
|
220
|
+
from kstlib.cli import app as app # Typer app object
|
|
221
|
+
from kstlib.config import (
|
|
222
|
+
ConfigCircularIncludeError as ConfigCircularIncludeError,
|
|
223
|
+
)
|
|
224
|
+
from kstlib.config import (
|
|
225
|
+
ConfigError as ConfigError,
|
|
226
|
+
)
|
|
227
|
+
from kstlib.config import (
|
|
228
|
+
ConfigFileNotFoundError as ConfigFileNotFoundError,
|
|
229
|
+
)
|
|
230
|
+
from kstlib.config import (
|
|
231
|
+
ConfigFormatError as ConfigFormatError,
|
|
232
|
+
)
|
|
233
|
+
from kstlib.config import (
|
|
234
|
+
ConfigLoader as ConfigLoader,
|
|
235
|
+
)
|
|
236
|
+
from kstlib.config import (
|
|
237
|
+
KstlibError as KstlibError,
|
|
238
|
+
)
|
|
239
|
+
from kstlib.config.exceptions import ConfigNotLoadedError as ConfigNotLoadedError
|
|
240
|
+
from kstlib.config.loader import (
|
|
241
|
+
clear_config as clear_config,
|
|
242
|
+
)
|
|
243
|
+
from kstlib.config.loader import (
|
|
244
|
+
get_config as get_config,
|
|
245
|
+
)
|
|
246
|
+
from kstlib.config.loader import (
|
|
247
|
+
load_config as load_config,
|
|
248
|
+
)
|
|
249
|
+
from kstlib.config.loader import (
|
|
250
|
+
load_from_env as load_from_env,
|
|
251
|
+
)
|
|
252
|
+
from kstlib.config.loader import (
|
|
253
|
+
load_from_file as load_from_file,
|
|
254
|
+
)
|
|
255
|
+
from kstlib.config.loader import (
|
|
256
|
+
require_config as require_config,
|
|
257
|
+
)
|
|
258
|
+
from kstlib.logging import LogManager as LogManager
|
|
259
|
+
from kstlib.monitoring import MonitoringError as MonitoringError
|
|
260
|
+
from kstlib.ui import PanelManager as PanelManager
|
|
261
|
+
from kstlib.ui import PanelRenderingError as PanelRenderingError
|
|
262
|
+
else:
|
|
263
|
+
import os as _os
|
|
264
|
+
|
|
265
|
+
if _os.getenv("KSTLIB_TRACEBACK", "0") == "1":
|
|
266
|
+
install_rich_traceback()
|
kstlib/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Entry point for kstlib CLI application.
|
|
2
|
+
|
|
3
|
+
This module serves as the entry point when kstlib is invoked as a module
|
|
4
|
+
(python -m kstlib) or through the installed console script.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from kstlib.cli import app
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
"""Run the kstlib CLI application."""
|
|
12
|
+
app()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Multi-channel alerting module with Slack and Email support.
|
|
2
|
+
|
|
3
|
+
This module provides a flexible alerting system for sending notifications
|
|
4
|
+
to multiple channels (Slack, Email) with level-based filtering and
|
|
5
|
+
rate limiting.
|
|
6
|
+
|
|
7
|
+
Key components:
|
|
8
|
+
|
|
9
|
+
- :class:`AlertManager`: Orchestrates multi-channel delivery
|
|
10
|
+
- :class:`AlertMessage`: The alert payload with title, body, and level
|
|
11
|
+
- :class:`AlertLevel`: Severity levels (INFO, WARNING, CRITICAL)
|
|
12
|
+
- :class:`AlertThrottle`: Rate limiting for alert floods
|
|
13
|
+
|
|
14
|
+
Channels:
|
|
15
|
+
|
|
16
|
+
- :class:`SlackChannel`: Slack webhook delivery
|
|
17
|
+
- :class:`EmailChannel`: Email delivery via kstlib.mail transports
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
Basic Slack alert::
|
|
21
|
+
|
|
22
|
+
from kstlib.alerts import AlertManager, AlertMessage, AlertLevel
|
|
23
|
+
from kstlib.alerts.channels import SlackChannel
|
|
24
|
+
|
|
25
|
+
channel = SlackChannel(
|
|
26
|
+
webhook_url="https://hooks.slack.com/services/T.../B.../xxx"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
manager = AlertManager().add_channel(channel)
|
|
30
|
+
|
|
31
|
+
alert = AlertMessage(
|
|
32
|
+
title="Deployment Complete",
|
|
33
|
+
body="Version 2.1.0 deployed to production",
|
|
34
|
+
level=AlertLevel.INFO,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
results = await manager.send(alert)
|
|
38
|
+
|
|
39
|
+
Multi-channel with level filtering::
|
|
40
|
+
|
|
41
|
+
from kstlib.alerts.channels import EmailChannel
|
|
42
|
+
from kstlib.mail.transports import SMTPTransport
|
|
43
|
+
|
|
44
|
+
smtp = SMTPTransport(host="smtp.example.com", port=587)
|
|
45
|
+
email_channel = EmailChannel(
|
|
46
|
+
transport=smtp,
|
|
47
|
+
sender="alerts@example.com",
|
|
48
|
+
recipients=["oncall@example.com"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
manager = (
|
|
52
|
+
AlertManager()
|
|
53
|
+
.add_channel(slack_channel, min_level=AlertLevel.WARNING)
|
|
54
|
+
.add_channel(email_channel, min_level=AlertLevel.CRITICAL)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# This goes to Slack only (WARNING level)
|
|
58
|
+
await manager.send(AlertMessage(
|
|
59
|
+
title="High Memory",
|
|
60
|
+
body="Memory at 85%",
|
|
61
|
+
level=AlertLevel.WARNING,
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
# This goes to both Slack and Email (CRITICAL level)
|
|
65
|
+
await manager.send(AlertMessage(
|
|
66
|
+
title="Server Down",
|
|
67
|
+
body="API server not responding",
|
|
68
|
+
level=AlertLevel.CRITICAL,
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
With rate limiting::
|
|
72
|
+
|
|
73
|
+
from kstlib.alerts.throttle import AlertThrottle
|
|
74
|
+
|
|
75
|
+
throttle = AlertThrottle(rate=10, per=60.0) # 10 per minute
|
|
76
|
+
|
|
77
|
+
manager = AlertManager().add_channel(
|
|
78
|
+
slack_channel,
|
|
79
|
+
throttle=throttle,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
From configuration::
|
|
83
|
+
|
|
84
|
+
manager = AlertManager.from_config(
|
|
85
|
+
config=app_config["alerts"],
|
|
86
|
+
credential_resolver=resolver,
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
from kstlib.alerts.exceptions import (
|
|
91
|
+
AlertConfigurationError,
|
|
92
|
+
AlertDeliveryError,
|
|
93
|
+
AlertError,
|
|
94
|
+
AlertThrottledError,
|
|
95
|
+
)
|
|
96
|
+
from kstlib.alerts.manager import AlertManager
|
|
97
|
+
from kstlib.alerts.models import AlertLevel, AlertMessage, AlertResult
|
|
98
|
+
from kstlib.alerts.throttle import AlertThrottle
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"AlertConfigurationError",
|
|
102
|
+
"AlertDeliveryError",
|
|
103
|
+
"AlertError",
|
|
104
|
+
"AlertLevel",
|
|
105
|
+
"AlertManager",
|
|
106
|
+
"AlertMessage",
|
|
107
|
+
"AlertResult",
|
|
108
|
+
"AlertThrottle",
|
|
109
|
+
"AlertThrottledError",
|
|
110
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Alert channel implementations.
|
|
2
|
+
|
|
3
|
+
Provides both sync and async channel interfaces for different alert
|
|
4
|
+
delivery backends (Slack, Email, etc.).
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Using SlackChannel::
|
|
8
|
+
|
|
9
|
+
from kstlib.alerts.channels import SlackChannel
|
|
10
|
+
|
|
11
|
+
channel = SlackChannel(webhook_url="https://hooks.slack.com/...")
|
|
12
|
+
await channel.send(alert)
|
|
13
|
+
|
|
14
|
+
Using EmailChannel with existing mail transport::
|
|
15
|
+
|
|
16
|
+
from kstlib.alerts.channels import EmailChannel
|
|
17
|
+
from kstlib.mail.transports import SMTPTransport
|
|
18
|
+
|
|
19
|
+
transport = SMTPTransport(host="smtp.example.com", port=587)
|
|
20
|
+
channel = EmailChannel(
|
|
21
|
+
transport=transport,
|
|
22
|
+
sender="alerts@example.com",
|
|
23
|
+
recipients=["oncall@example.com"],
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from kstlib.alerts.channels.base import AlertChannel, AsyncAlertChannel
|
|
28
|
+
from kstlib.alerts.channels.email import EmailChannel
|
|
29
|
+
from kstlib.alerts.channels.slack import SlackChannel
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AlertChannel",
|
|
33
|
+
"AsyncAlertChannel",
|
|
34
|
+
"EmailChannel",
|
|
35
|
+
"SlackChannel",
|
|
36
|
+
]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Abstract base classes for alert channels.
|
|
2
|
+
|
|
3
|
+
Provides both sync and async channel interfaces following the same
|
|
4
|
+
pattern as :mod:`kstlib.mail.transport`.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Implementing a sync channel::
|
|
8
|
+
|
|
9
|
+
class WebhookChannel(AlertChannel):
|
|
10
|
+
@property
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
return "webhook"
|
|
13
|
+
|
|
14
|
+
def send(self, alert: AlertMessage) -> AlertResult:
|
|
15
|
+
# Send via HTTP POST
|
|
16
|
+
return AlertResult(channel=self.name, success=True)
|
|
17
|
+
|
|
18
|
+
Implementing an async channel::
|
|
19
|
+
|
|
20
|
+
class AsyncWebhookChannel(AsyncAlertChannel):
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "async_webhook"
|
|
24
|
+
|
|
25
|
+
async def send(self, alert: AlertMessage) -> AlertResult:
|
|
26
|
+
async with httpx.AsyncClient() as client:
|
|
27
|
+
# Send via HTTP POST
|
|
28
|
+
pass
|
|
29
|
+
return AlertResult(channel=self.name, success=True)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import asyncio
|
|
35
|
+
from abc import ABC, abstractmethod
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
40
|
+
|
|
41
|
+
from kstlib.alerts.models import AlertMessage, AlertResult
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AlertChannel(ABC):
|
|
45
|
+
"""Abstract sync channel for delivering alerts.
|
|
46
|
+
|
|
47
|
+
Subclass this for synchronous alert delivery implementations.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
Implementing a custom sync channel::
|
|
51
|
+
|
|
52
|
+
class LogChannel(AlertChannel):
|
|
53
|
+
@property
|
|
54
|
+
def name(self) -> str:
|
|
55
|
+
return "log"
|
|
56
|
+
|
|
57
|
+
def send(self, alert: AlertMessage) -> AlertResult:
|
|
58
|
+
print(f"[{alert.level.name}] {alert.title}")
|
|
59
|
+
return AlertResult(channel=self.name, success=True)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def name(self) -> str:
|
|
65
|
+
"""Return the unique name of this channel.
|
|
66
|
+
|
|
67
|
+
Used for logging, metrics, and result identification.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def send(self, alert: AlertMessage) -> AlertResult:
|
|
72
|
+
"""Deliver the alert via this channel.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
alert: The alert message to deliver.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
AlertResult with delivery status.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
AlertDeliveryError: If delivery fails.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncAlertChannel(ABC):
|
|
86
|
+
"""Abstract async channel for delivering alerts.
|
|
87
|
+
|
|
88
|
+
Subclass this for asynchronous alert delivery implementations
|
|
89
|
+
that use async HTTP clients or other async I/O.
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
Implementing a custom async channel::
|
|
93
|
+
|
|
94
|
+
class SlackChannel(AsyncAlertChannel):
|
|
95
|
+
@property
|
|
96
|
+
def name(self) -> str:
|
|
97
|
+
return "slack"
|
|
98
|
+
|
|
99
|
+
async def send(self, alert: AlertMessage) -> AlertResult:
|
|
100
|
+
async with httpx.AsyncClient() as client:
|
|
101
|
+
await client.post(...)
|
|
102
|
+
return AlertResult(channel=self.name, success=True)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def name(self) -> str:
|
|
108
|
+
"""Return the unique name of this channel.
|
|
109
|
+
|
|
110
|
+
Used for logging, metrics, and result identification.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
async def send(self, alert: AlertMessage) -> AlertResult:
|
|
115
|
+
"""Deliver the alert asynchronously via this channel.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
alert: The alert message to deliver.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
AlertResult with delivery status.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
AlertDeliveryError: If delivery fails.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AsyncChannelWrapper(AsyncAlertChannel):
|
|
129
|
+
"""Wrap a sync channel for async usage.
|
|
130
|
+
|
|
131
|
+
Executes the sync channel's send method in a thread pool executor
|
|
132
|
+
to avoid blocking the event loop. Follows the same pattern as
|
|
133
|
+
:class:`kstlib.mail.transport.AsyncTransportWrapper`.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
channel: The sync channel to wrap.
|
|
137
|
+
executor: Optional thread pool executor. If None, uses the default.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
Wrapping a sync channel::
|
|
141
|
+
|
|
142
|
+
sync_channel = LogChannel()
|
|
143
|
+
async_channel = AsyncChannelWrapper(sync_channel)
|
|
144
|
+
|
|
145
|
+
# Now usable in async context
|
|
146
|
+
await async_channel.send(alert)
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
channel: AlertChannel,
|
|
152
|
+
*,
|
|
153
|
+
executor: ThreadPoolExecutor | None = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Initialize the async wrapper.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
channel: The sync channel to wrap.
|
|
159
|
+
executor: Optional custom thread pool executor.
|
|
160
|
+
"""
|
|
161
|
+
self._channel = channel
|
|
162
|
+
self._executor = executor
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def name(self) -> str:
|
|
166
|
+
"""Return the name of the wrapped channel."""
|
|
167
|
+
return self._channel.name
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def channel(self) -> AlertChannel:
|
|
171
|
+
"""Return the wrapped sync channel."""
|
|
172
|
+
return self._channel
|
|
173
|
+
|
|
174
|
+
async def send(self, alert: AlertMessage) -> AlertResult:
|
|
175
|
+
"""Send alert asynchronously via the wrapped channel.
|
|
176
|
+
|
|
177
|
+
Runs the sync channel's send method in a thread pool to avoid
|
|
178
|
+
blocking the async event loop.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
alert: The alert message to send.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
AlertResult from the wrapped channel.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
AlertDeliveryError: If the underlying channel fails.
|
|
188
|
+
"""
|
|
189
|
+
loop = asyncio.get_running_loop()
|
|
190
|
+
return await loop.run_in_executor(
|
|
191
|
+
self._executor,
|
|
192
|
+
self._channel.send,
|
|
193
|
+
alert,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
__all__ = ["AlertChannel", "AsyncAlertChannel", "AsyncChannelWrapper"]
|