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.
- 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.0.dist-info/METADATA +201 -0
- kstlib-1.0.0.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
- kstlib-1.0.0.dist-info/entry_points.txt +2 -0
- kstlib-1.0.0.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.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Simplified Monitoring API with decorator-based collectors.
|
|
2
|
+
|
|
3
|
+
This module provides a streamlined API for monitoring dashboards:
|
|
4
|
+
|
|
5
|
+
- Config in YAML (template, delivery settings)
|
|
6
|
+
- Collectors in Python (via @decorator)
|
|
7
|
+
- Simple run() to collect, render, and deliver
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
Basic usage with decorators:
|
|
11
|
+
|
|
12
|
+
>>> from kstlib.monitoring import Monitoring, MonitorKV
|
|
13
|
+
>>> mon = Monitoring(template="<p>{{ info | render }}</p>")
|
|
14
|
+
>>> @mon.collector
|
|
15
|
+
... def info():
|
|
16
|
+
... return MonitorKV(items={"status": "OK"})
|
|
17
|
+
>>> result = mon.run_sync()
|
|
18
|
+
>>> "OK" in result.html
|
|
19
|
+
True
|
|
20
|
+
|
|
21
|
+
Load from config:
|
|
22
|
+
|
|
23
|
+
>>> mon = Monitoring.from_config() # doctest: +SKIP
|
|
24
|
+
>>> @mon.collector # doctest: +SKIP
|
|
25
|
+
... def metrics(): # doctest: +SKIP
|
|
26
|
+
... return collect_metrics() # doctest: +SKIP
|
|
27
|
+
>>> mon.run_sync() # doctest: +SKIP
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
from collections.abc import Callable
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
36
|
+
|
|
37
|
+
from kstlib.monitoring.service import Collector, MonitoringResult, MonitoringService
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from kstlib.monitoring.delivery import DeliveryBackend, DeliveryResult
|
|
41
|
+
|
|
42
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Monitoring:
|
|
46
|
+
"""Simplified monitoring with decorator-based collectors.
|
|
47
|
+
|
|
48
|
+
This class provides a cleaner API than MonitoringService:
|
|
49
|
+
|
|
50
|
+
- Collectors are registered via ``@mon.collector`` decorator
|
|
51
|
+
- Config loaded from ``kstlib.conf.yml`` section ``monitoring:``
|
|
52
|
+
- Automatic template_file resolution
|
|
53
|
+
- Integrated delivery
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
template: Jinja2 template string (mutually exclusive with template_file).
|
|
57
|
+
template_file: Path to template file (mutually exclusive with template).
|
|
58
|
+
inline_css: Use inline CSS for email compatibility (default True).
|
|
59
|
+
fail_fast: Raise on first collector error (default False).
|
|
60
|
+
delivery: Optional delivery backend (FileDelivery or MailDelivery).
|
|
61
|
+
name: Dashboard name (for delivery subject, default "monitoring").
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
Direct instantiation:
|
|
65
|
+
|
|
66
|
+
>>> mon = Monitoring(template="<p>{{ msg }}</p>")
|
|
67
|
+
>>> @mon.collector
|
|
68
|
+
... def msg():
|
|
69
|
+
... return "Hello"
|
|
70
|
+
>>> mon.run_sync().html
|
|
71
|
+
'<p>Hello</p>'
|
|
72
|
+
|
|
73
|
+
From config file:
|
|
74
|
+
|
|
75
|
+
>>> mon = Monitoring.from_config() # doctest: +SKIP
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__( # noqa: PLR0913
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
template: str | None = None,
|
|
82
|
+
template_file: str | Path | None = None,
|
|
83
|
+
inline_css: bool = True,
|
|
84
|
+
fail_fast: bool = False,
|
|
85
|
+
delivery: DeliveryBackend | _DeferredMailDelivery | None = None,
|
|
86
|
+
name: str = "monitoring",
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Initialize monitoring instance."""
|
|
89
|
+
if template is None and template_file is None:
|
|
90
|
+
raise ValueError("Either 'template' or 'template_file' is required")
|
|
91
|
+
if template is not None and template_file is not None:
|
|
92
|
+
raise ValueError("Cannot specify both 'template' and 'template_file'")
|
|
93
|
+
|
|
94
|
+
# Resolve template
|
|
95
|
+
if template_file is not None:
|
|
96
|
+
path = Path(template_file)
|
|
97
|
+
if not path.exists():
|
|
98
|
+
raise FileNotFoundError(f"Template not found: {path}")
|
|
99
|
+
template = path.read_text(encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
# After validation, template is guaranteed to be str
|
|
102
|
+
assert template is not None # for mypy
|
|
103
|
+
self._template: str = template
|
|
104
|
+
self._inline_css = inline_css
|
|
105
|
+
self._fail_fast = fail_fast
|
|
106
|
+
self._delivery: DeliveryBackend | _DeferredMailDelivery | None = delivery
|
|
107
|
+
self._name = name
|
|
108
|
+
self._collectors: dict[str, Collector] = {}
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_config(
|
|
112
|
+
cls,
|
|
113
|
+
config_section: str = "monitoring",
|
|
114
|
+
*,
|
|
115
|
+
base_dir: Path | None = None,
|
|
116
|
+
) -> Monitoring:
|
|
117
|
+
"""Create Monitoring instance from kstlib.conf.yml.
|
|
118
|
+
|
|
119
|
+
Loads the ``monitoring:`` section from the config file.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
config_section: Config section name (default "monitoring").
|
|
123
|
+
base_dir: Base directory for resolving template_file paths.
|
|
124
|
+
Defaults to current working directory.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Configured Monitoring instance.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If config section is missing or invalid.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
>>> mon = Monitoring.from_config() # doctest: +SKIP
|
|
134
|
+
>>> @mon.collector # doctest: +SKIP
|
|
135
|
+
... def data(): # doctest: +SKIP
|
|
136
|
+
... return {"key": "value"} # doctest: +SKIP
|
|
137
|
+
"""
|
|
138
|
+
from kstlib.config import get_config, load_config
|
|
139
|
+
|
|
140
|
+
# Load config if not already loaded
|
|
141
|
+
try:
|
|
142
|
+
config = get_config()
|
|
143
|
+
except Exception:
|
|
144
|
+
config = load_config()
|
|
145
|
+
|
|
146
|
+
# Get monitoring section (Box.get is untyped)
|
|
147
|
+
mon_config: dict[str, Any] = config.get(config_section, {}) # type: ignore[no-untyped-call]
|
|
148
|
+
if not mon_config:
|
|
149
|
+
raise ValueError(f"Config section '{config_section}' not found or empty")
|
|
150
|
+
|
|
151
|
+
# Resolve base directory
|
|
152
|
+
if base_dir is None:
|
|
153
|
+
base_dir = Path.cwd()
|
|
154
|
+
|
|
155
|
+
# Extract settings
|
|
156
|
+
template = mon_config.get("template")
|
|
157
|
+
template_file = mon_config.get("template_file")
|
|
158
|
+
|
|
159
|
+
# Resolve template_file path
|
|
160
|
+
if template_file is not None:
|
|
161
|
+
template_file = base_dir / template_file
|
|
162
|
+
|
|
163
|
+
# Build delivery backend if configured
|
|
164
|
+
delivery = None
|
|
165
|
+
delivery_config = mon_config.get("delivery")
|
|
166
|
+
if delivery_config:
|
|
167
|
+
delivery = cls._build_delivery(delivery_config)
|
|
168
|
+
|
|
169
|
+
return cls(
|
|
170
|
+
template=template,
|
|
171
|
+
template_file=template_file,
|
|
172
|
+
inline_css=mon_config.get("inline_css", True),
|
|
173
|
+
fail_fast=mon_config.get("fail_fast", False),
|
|
174
|
+
delivery=delivery,
|
|
175
|
+
name=mon_config.get("name", "monitoring"),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _build_delivery(config: dict[str, Any]) -> DeliveryBackend | _DeferredMailDelivery:
|
|
180
|
+
"""Build delivery backend from config dict."""
|
|
181
|
+
from kstlib.monitoring.delivery import FileDelivery
|
|
182
|
+
|
|
183
|
+
delivery_type = config.get("type", "file")
|
|
184
|
+
|
|
185
|
+
if delivery_type == "file":
|
|
186
|
+
return FileDelivery(
|
|
187
|
+
output_dir=config.get("output_dir", "./reports"),
|
|
188
|
+
max_files=config.get("max_files", 100),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if delivery_type == "mail":
|
|
192
|
+
# Mail delivery requires transport - defer to run() time
|
|
193
|
+
# Store config for later
|
|
194
|
+
return _DeferredMailDelivery(config)
|
|
195
|
+
|
|
196
|
+
raise ValueError(f"Unknown delivery type: {delivery_type}")
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def name(self) -> str:
|
|
200
|
+
"""Return dashboard name."""
|
|
201
|
+
return self._name
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def collector_names(self) -> list[str]:
|
|
205
|
+
"""Return list of registered collector names."""
|
|
206
|
+
return list(self._collectors)
|
|
207
|
+
|
|
208
|
+
def collector(self, func: F) -> F:
|
|
209
|
+
"""Decorator to register a collector function.
|
|
210
|
+
|
|
211
|
+
The function name becomes the template variable name.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
func: Function returning data for the template.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The original function (unmodified).
|
|
218
|
+
|
|
219
|
+
Examples:
|
|
220
|
+
>>> mon = Monitoring(template="{{ status }}")
|
|
221
|
+
>>> @mon.collector
|
|
222
|
+
... def status():
|
|
223
|
+
... return "OK"
|
|
224
|
+
>>> mon.run_sync().html
|
|
225
|
+
'OK'
|
|
226
|
+
"""
|
|
227
|
+
self._collectors[func.__name__] = func
|
|
228
|
+
return func
|
|
229
|
+
|
|
230
|
+
def add_collector(self, name: str, func: Collector) -> Monitoring:
|
|
231
|
+
"""Add a collector with explicit name.
|
|
232
|
+
|
|
233
|
+
Use this when you need a different name than the function name.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name: Name to use in template.
|
|
237
|
+
func: Collector function.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Self for chaining.
|
|
241
|
+
"""
|
|
242
|
+
self._collectors[name] = func
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
def _create_service(self) -> MonitoringService:
|
|
246
|
+
"""Create the underlying MonitoringService."""
|
|
247
|
+
return MonitoringService(
|
|
248
|
+
template=self._template,
|
|
249
|
+
collectors=self._collectors,
|
|
250
|
+
inline_css=self._inline_css,
|
|
251
|
+
fail_fast=self._fail_fast,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
async def run(self, *, deliver: bool = True) -> MonitoringResult:
|
|
255
|
+
"""Collect data, render template, and optionally deliver.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
deliver: If True and delivery is configured, send the result.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
MonitoringResult with HTML and metadata.
|
|
262
|
+
"""
|
|
263
|
+
service = self._create_service()
|
|
264
|
+
result = await service.run()
|
|
265
|
+
|
|
266
|
+
# Deliver if configured and requested
|
|
267
|
+
if deliver and self._delivery is not None:
|
|
268
|
+
await self._deliver(result)
|
|
269
|
+
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
def run_sync(self, *, deliver: bool = True) -> MonitoringResult:
|
|
273
|
+
"""Synchronous version of run().
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
deliver: If True and delivery is configured, send the result.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
MonitoringResult with HTML and metadata.
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
loop = asyncio.get_running_loop()
|
|
283
|
+
except RuntimeError:
|
|
284
|
+
loop = None
|
|
285
|
+
|
|
286
|
+
if loop is not None:
|
|
287
|
+
import concurrent.futures
|
|
288
|
+
|
|
289
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
290
|
+
future = executor.submit(asyncio.run, self.run(deliver=deliver))
|
|
291
|
+
return future.result()
|
|
292
|
+
|
|
293
|
+
return asyncio.run(self.run(deliver=deliver))
|
|
294
|
+
|
|
295
|
+
async def _deliver(self, result: MonitoringResult) -> DeliveryResult:
|
|
296
|
+
"""Deliver the result using configured backend."""
|
|
297
|
+
if self._delivery is None:
|
|
298
|
+
raise RuntimeError("No delivery backend configured")
|
|
299
|
+
|
|
300
|
+
if isinstance(self._delivery, _DeferredMailDelivery):
|
|
301
|
+
# Build actual mail delivery with transport
|
|
302
|
+
delivery = await self._delivery.build()
|
|
303
|
+
return await delivery.deliver(result, self._name)
|
|
304
|
+
|
|
305
|
+
return await self._delivery.deliver(result, self._name)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class _DeferredMailDelivery:
|
|
309
|
+
"""Placeholder for mail delivery that needs OAuth token at runtime."""
|
|
310
|
+
|
|
311
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
312
|
+
self._config = config
|
|
313
|
+
|
|
314
|
+
async def build(self) -> DeliveryBackend:
|
|
315
|
+
"""Build actual MailDelivery with OAuth transport."""
|
|
316
|
+
from kstlib.auth import OAuth2Provider
|
|
317
|
+
from kstlib.mail.transports import GmailTransport
|
|
318
|
+
from kstlib.monitoring.delivery import MailDelivery, MailDeliveryConfig
|
|
319
|
+
|
|
320
|
+
# Get OAuth token
|
|
321
|
+
provider = OAuth2Provider.from_config("google")
|
|
322
|
+
token = provider.get_token()
|
|
323
|
+
if token is None or token.is_expired:
|
|
324
|
+
raise RuntimeError("Gmail token not available or expired. Run 'kstlib auth login google' first.")
|
|
325
|
+
|
|
326
|
+
transport = GmailTransport(token=token)
|
|
327
|
+
|
|
328
|
+
config = MailDeliveryConfig(
|
|
329
|
+
sender=self._config.get("sender", ""),
|
|
330
|
+
recipients=self._config.get("recipients", []),
|
|
331
|
+
cc=self._config.get("cc", []),
|
|
332
|
+
bcc=self._config.get("bcc", []),
|
|
333
|
+
subject_template=self._config.get("subject_template", "Monitoring: {name}"),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return MailDelivery(transport=transport, config=config)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
__all__ = [
|
|
340
|
+
"Monitoring",
|
|
341
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Jinja2 rendering integration for monitoring render types.
|
|
2
|
+
|
|
3
|
+
Provides a Jinja2 filter ``render`` that dispatches to ``Renderable.render()``
|
|
4
|
+
for monitoring objects, and HTML-escapes primitives. Includes a pre-configured
|
|
5
|
+
environment factory and a standalone template rendering helper.
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
Render a template with monitoring objects:
|
|
9
|
+
|
|
10
|
+
>>> from kstlib.monitoring.renderer import render_template
|
|
11
|
+
>>> from kstlib.monitoring.cell import StatusCell
|
|
12
|
+
>>> from kstlib.monitoring.types import StatusLevel
|
|
13
|
+
>>> cell = StatusCell("UP", StatusLevel.OK)
|
|
14
|
+
>>> html = render_template("<p>{{ s | render }}</p>", {"s": cell})
|
|
15
|
+
>>> "status-ok" in html
|
|
16
|
+
True
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import html as _html
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from jinja2 import Environment
|
|
25
|
+
from markupsafe import Markup
|
|
26
|
+
|
|
27
|
+
from kstlib.monitoring._styles import get_css_classes
|
|
28
|
+
from kstlib.monitoring.types import Renderable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_html(value: Any, inline_css: bool = False) -> Markup:
|
|
32
|
+
"""Jinja2 filter that renders a value as safe HTML.
|
|
33
|
+
|
|
34
|
+
When *value* implements :class:`~kstlib.monitoring.types.Renderable`,
|
|
35
|
+
its ``.render()`` method is called. Otherwise the value is converted
|
|
36
|
+
to a string and HTML-escaped.
|
|
37
|
+
|
|
38
|
+
Register this filter under the name ``render`` on a Jinja2 environment
|
|
39
|
+
so templates can use ``{{ data | render }}``.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: Any value to render. ``Renderable`` objects are dispatched
|
|
43
|
+
to their own ``render()`` method; everything else is escaped.
|
|
44
|
+
inline_css: Forwarded to ``Renderable.render(inline_css=...)``.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A :class:`jinja2.Markup` instance (marked safe to prevent
|
|
48
|
+
double-escaping by Jinja2 autoescape).
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> from kstlib.monitoring.renderer import render_html
|
|
52
|
+
>>> str(render_html("<b>bold</b>"))
|
|
53
|
+
'<b>bold</b>'
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(value, Renderable):
|
|
56
|
+
return Markup(value.render(inline_css=inline_css)) # noqa: S704
|
|
57
|
+
return Markup(_html.escape(str(value))) # noqa: S704
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_environment(**kwargs: Any) -> Environment:
|
|
61
|
+
"""Create a Jinja2 :class:`~jinja2.Environment` with monitoring filters.
|
|
62
|
+
|
|
63
|
+
The returned environment has:
|
|
64
|
+
|
|
65
|
+
- ``autoescape=True`` by default (overridable via *kwargs*).
|
|
66
|
+
- The ``render`` filter bound to :func:`render_html`.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
**kwargs: Forwarded to :class:`jinja2.Environment`. Common
|
|
70
|
+
options include ``loader``, ``autoescape``, ``trim_blocks``.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A configured :class:`jinja2.Environment`.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> from kstlib.monitoring.renderer import create_environment
|
|
77
|
+
>>> env = create_environment()
|
|
78
|
+
>>> "render" in env.filters
|
|
79
|
+
True
|
|
80
|
+
"""
|
|
81
|
+
kwargs.setdefault("autoescape", True)
|
|
82
|
+
env = Environment(**kwargs) # noqa: S701
|
|
83
|
+
env.filters["render"] = render_html
|
|
84
|
+
return env
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def render_template(
|
|
88
|
+
source: str,
|
|
89
|
+
context: dict[str, Any] | None = None,
|
|
90
|
+
*,
|
|
91
|
+
inline_css: bool = False,
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Render a Jinja2 template string with monitoring support.
|
|
94
|
+
|
|
95
|
+
This is a high-level convenience function that creates a temporary
|
|
96
|
+
environment, compiles *source* as a template, and renders it with
|
|
97
|
+
*context*.
|
|
98
|
+
|
|
99
|
+
When ``inline_css=False`` (default), the CSS class definitions from
|
|
100
|
+
:func:`~kstlib.monitoring._styles.get_css_classes` are prepended to
|
|
101
|
+
the output so that class-based rendering works out of the box.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
source: Jinja2 template source string.
|
|
105
|
+
context: Template variables. ``None`` is treated as an empty dict.
|
|
106
|
+
inline_css: If ``True``, skip the ``<style>`` block prepend.
|
|
107
|
+
Useful when styles are inlined into each element.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Rendered HTML string.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
TypeError: If *source* is not a ``str`` or *context* is not
|
|
114
|
+
a ``dict`` (or ``None``).
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
>>> from kstlib.monitoring.renderer import render_template
|
|
118
|
+
>>> render_template("Hello {{ name }}", {"name": "World"}, inline_css=True)
|
|
119
|
+
'Hello World'
|
|
120
|
+
"""
|
|
121
|
+
if not isinstance(source, str):
|
|
122
|
+
raise TypeError(f"source must be str, got {type(source).__name__}")
|
|
123
|
+
if context is not None and not isinstance(context, dict):
|
|
124
|
+
raise TypeError(f"context must be dict or None, got {type(context).__name__}")
|
|
125
|
+
|
|
126
|
+
env = create_environment()
|
|
127
|
+
template = env.from_string(source)
|
|
128
|
+
rendered = template.render(**(context or {}))
|
|
129
|
+
|
|
130
|
+
if inline_css:
|
|
131
|
+
return rendered
|
|
132
|
+
return get_css_classes() + "\n" + rendered
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"create_environment",
|
|
137
|
+
"render_html",
|
|
138
|
+
"render_template",
|
|
139
|
+
]
|