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
kstlib/__init__.py CHANGED
@@ -1 +1,266 @@
1
- __version__ = "0.0.1a0"
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"]