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/ui/panels.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Config-driven helpers for rendering Rich panels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# pylint: disable=duplicate-code
|
|
6
|
+
import asyncio
|
|
7
|
+
import copy
|
|
8
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
9
|
+
from numbers import Number
|
|
10
|
+
from typing import Any, TypeGuard, cast
|
|
11
|
+
|
|
12
|
+
from box import Box
|
|
13
|
+
from rich import box as rich_box
|
|
14
|
+
from rich.console import Console, RenderableType
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.pretty import Pretty
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from kstlib.config import ConfigNotLoadedError, get_config
|
|
21
|
+
from kstlib.ui.exceptions import PanelRenderingError
|
|
22
|
+
from kstlib.utils.dict import deep_merge
|
|
23
|
+
|
|
24
|
+
PanelPayload = RenderableType | Mapping[str, Any] | Sequence[tuple[Any, Any]] | str | None
|
|
25
|
+
|
|
26
|
+
PAIR_ENTRY_LENGTH = 2
|
|
27
|
+
DEFAULT_PADDING: tuple[int, int] = (1, 2)
|
|
28
|
+
VALID_PADDING_LENGTHS = {1, 2, 4}
|
|
29
|
+
DEFAULT_PRETTY_INDENT = 2
|
|
30
|
+
|
|
31
|
+
DEFAULT_PANEL_CONFIG: dict[str, Any] = {
|
|
32
|
+
"defaults": {
|
|
33
|
+
"panel": {
|
|
34
|
+
"border_style": "bright_blue",
|
|
35
|
+
"title_align": "left",
|
|
36
|
+
"subtitle_align": "left",
|
|
37
|
+
"padding": [1, 2],
|
|
38
|
+
"expand": True,
|
|
39
|
+
"highlight": False,
|
|
40
|
+
"box": "ROUNDED",
|
|
41
|
+
"icon": None,
|
|
42
|
+
"width": None,
|
|
43
|
+
},
|
|
44
|
+
"content": {
|
|
45
|
+
"box": "SIMPLE",
|
|
46
|
+
"expand": True,
|
|
47
|
+
"show_header": False,
|
|
48
|
+
"key_label": "Key",
|
|
49
|
+
"value_label": "Value",
|
|
50
|
+
"key_style": "bold white",
|
|
51
|
+
"value_style": None,
|
|
52
|
+
"header_style": "bold",
|
|
53
|
+
"pad_edge": False,
|
|
54
|
+
"sort_keys": False,
|
|
55
|
+
"use_markup": True,
|
|
56
|
+
"use_pretty": True,
|
|
57
|
+
"pretty_indent": 2,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
"presets": {
|
|
61
|
+
"info": {
|
|
62
|
+
"panel": {
|
|
63
|
+
"border_style": "cyan",
|
|
64
|
+
"title": "Information",
|
|
65
|
+
"icon": "[i]",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"success": {
|
|
69
|
+
"panel": {
|
|
70
|
+
"border_style": "sea_green3",
|
|
71
|
+
"title": "Success",
|
|
72
|
+
"icon": "[ok]",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"warning": {
|
|
76
|
+
"panel": {
|
|
77
|
+
"border_style": "orange3",
|
|
78
|
+
"title": "Warning",
|
|
79
|
+
"icon": "[!]",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"error": {
|
|
83
|
+
"panel": {
|
|
84
|
+
"border_style": "red3",
|
|
85
|
+
"title": "Error",
|
|
86
|
+
"icon": "[x]",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"summary": {
|
|
90
|
+
"panel": {
|
|
91
|
+
"border_style": "blue_violet",
|
|
92
|
+
"title": "Execution Summary",
|
|
93
|
+
"icon": "[summary]",
|
|
94
|
+
},
|
|
95
|
+
"content": {
|
|
96
|
+
"sort_keys": True,
|
|
97
|
+
"key_style": "bold cyan",
|
|
98
|
+
"value_style": "bold white",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class PanelManager:
|
|
106
|
+
"""Render Rich panels using config-driven presets.
|
|
107
|
+
|
|
108
|
+
Panel definitions are composed of defaults, named presets, and runtime overrides.
|
|
109
|
+
The merge order is ``kwargs > config preset > defaults``. Payloads can be plain
|
|
110
|
+
text, existing Rich renderables, mappings (rendered as two-column tables), or
|
|
111
|
+
sequences of ``(key, value)`` pairs.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: Optional configuration mapping (typically output of ``get_config()``).
|
|
115
|
+
console: Optional Rich console used for printing panels.
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
console: Console instance used for synchronous printing.
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
Create a panel manager:
|
|
122
|
+
|
|
123
|
+
>>> pm = PanelManager()
|
|
124
|
+
>>> pm.console is None
|
|
125
|
+
True
|
|
126
|
+
|
|
127
|
+
Render a simple text panel:
|
|
128
|
+
|
|
129
|
+
>>> panel = pm.render_panel(payload="Hello, World!")
|
|
130
|
+
>>> panel.title is None
|
|
131
|
+
True
|
|
132
|
+
|
|
133
|
+
Render with a preset:
|
|
134
|
+
|
|
135
|
+
>>> panel = pm.render_panel("info", payload="System status: OK")
|
|
136
|
+
>>> "Information" in str(panel.title)
|
|
137
|
+
True
|
|
138
|
+
|
|
139
|
+
Render a mapping as a table:
|
|
140
|
+
|
|
141
|
+
>>> panel = pm.render_panel(payload={"name": "Alice", "age": 30})
|
|
142
|
+
|
|
143
|
+
Override preset values:
|
|
144
|
+
|
|
145
|
+
>>> panel = pm.render_panel("error", payload="Oops!", title="Custom Title")
|
|
146
|
+
>>> "Custom Title" in str(panel.title)
|
|
147
|
+
True
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, config: Mapping[str, Any] | Box | None = None, console: Console | None = None) -> None:
|
|
151
|
+
"""Initialize the manager with optional config and console."""
|
|
152
|
+
self.console = console
|
|
153
|
+
self._config = self._prepare_config(config)
|
|
154
|
+
|
|
155
|
+
def render_panel(
|
|
156
|
+
self,
|
|
157
|
+
kind: str | None = None,
|
|
158
|
+
payload: PanelPayload = None,
|
|
159
|
+
**overrides: Any,
|
|
160
|
+
) -> Panel:
|
|
161
|
+
"""Build a ``Panel`` instance without printing it.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
kind: Name of the preset to use. If not found, defaults are used.
|
|
165
|
+
payload: Panel body (text, Rich renderable, mapping, or sequence of pairs).
|
|
166
|
+
**overrides: Runtime overrides applied on top of preset/default values.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Configured Rich ``Panel`` ready for rendering.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
PanelRenderingError: If the payload type is unsupported.
|
|
173
|
+
"""
|
|
174
|
+
panel_config = self._resolve_panel_config(kind, overrides)
|
|
175
|
+
renderable = self._build_renderable(payload, panel_config["content"])
|
|
176
|
+
panel_parameters = panel_config["panel"]
|
|
177
|
+
|
|
178
|
+
padding = self._coerce_padding(panel_parameters.get("padding"))
|
|
179
|
+
panel_box = self._resolve_box(panel_parameters.get("box"))
|
|
180
|
+
icon = panel_parameters.get("icon")
|
|
181
|
+
title = panel_parameters.get("title")
|
|
182
|
+
panel_title = self._compose_title(title, icon)
|
|
183
|
+
|
|
184
|
+
panel_kwargs: dict[str, Any] = {
|
|
185
|
+
"title": panel_title,
|
|
186
|
+
"title_align": panel_parameters.get("title_align", "left"),
|
|
187
|
+
"subtitle": panel_parameters.get("subtitle"),
|
|
188
|
+
"subtitle_align": panel_parameters.get("subtitle_align", "left"),
|
|
189
|
+
"border_style": panel_parameters.get("border_style"),
|
|
190
|
+
"padding": padding,
|
|
191
|
+
"expand": panel_parameters.get("expand", True),
|
|
192
|
+
"highlight": panel_parameters.get("highlight", False),
|
|
193
|
+
"box": panel_box,
|
|
194
|
+
"width": panel_parameters.get("width"),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
style_override = panel_parameters.get("style")
|
|
198
|
+
if style_override is not None:
|
|
199
|
+
panel_kwargs["style"] = style_override
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
return Panel(renderable, **panel_kwargs)
|
|
203
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
204
|
+
raise PanelRenderingError("Failed to render panel") from exc
|
|
205
|
+
|
|
206
|
+
def print_panel(
|
|
207
|
+
self,
|
|
208
|
+
kind: str | None = None,
|
|
209
|
+
payload: PanelPayload = None,
|
|
210
|
+
*,
|
|
211
|
+
console: Console | None = None,
|
|
212
|
+
**overrides: Any,
|
|
213
|
+
) -> Panel:
|
|
214
|
+
"""Render and print a panel synchronously.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
kind: Name of the preset to use.
|
|
218
|
+
payload: Panel body.
|
|
219
|
+
console: Optional console overriding the manager-level console.
|
|
220
|
+
**overrides: Runtime overrides applied on top of preset/default values.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The rendered ``Panel``.
|
|
224
|
+
"""
|
|
225
|
+
target_console = self._ensure_console(console)
|
|
226
|
+
panel = self.render_panel(kind=kind, payload=payload, **overrides)
|
|
227
|
+
target_console.print(panel)
|
|
228
|
+
return panel
|
|
229
|
+
|
|
230
|
+
async def print_panel_async(
|
|
231
|
+
self,
|
|
232
|
+
kind: str | None = None,
|
|
233
|
+
payload: PanelPayload = None,
|
|
234
|
+
*,
|
|
235
|
+
console: Console | None = None,
|
|
236
|
+
**overrides: Any,
|
|
237
|
+
) -> Panel:
|
|
238
|
+
"""Render and print a panel using an executor for async compatibility.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
kind: Name of the preset to use.
|
|
242
|
+
payload: Panel body.
|
|
243
|
+
console: Optional console overriding the manager-level console.
|
|
244
|
+
**overrides: Runtime overrides applied on top of preset/default values.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The rendered ``Panel``.
|
|
248
|
+
"""
|
|
249
|
+
target_console = self._ensure_console(console)
|
|
250
|
+
return await asyncio.to_thread(
|
|
251
|
+
self.print_panel,
|
|
252
|
+
kind,
|
|
253
|
+
payload,
|
|
254
|
+
console=target_console,
|
|
255
|
+
**overrides,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
# Internal helpers
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def _prepare_config(self, config: Mapping[str, Any] | Box | None) -> dict[str, Any]:
|
|
263
|
+
base_config = copy.deepcopy(DEFAULT_PANEL_CONFIG)
|
|
264
|
+
user_config = self._load_runtime_config(config)
|
|
265
|
+
if user_config:
|
|
266
|
+
deep_merge(base_config, user_config)
|
|
267
|
+
return base_config
|
|
268
|
+
|
|
269
|
+
def _load_runtime_config(self, config: Mapping[str, Any] | Box | None) -> dict[str, Any]:
|
|
270
|
+
if config is None:
|
|
271
|
+
try:
|
|
272
|
+
config = get_config()
|
|
273
|
+
except ConfigNotLoadedError:
|
|
274
|
+
return {}
|
|
275
|
+
if isinstance(config, Box):
|
|
276
|
+
config_mapping: Mapping[str, Any] = config.to_dict()
|
|
277
|
+
else:
|
|
278
|
+
config_mapping = dict(config)
|
|
279
|
+
|
|
280
|
+
ui_config = config_mapping.get("ui", {})
|
|
281
|
+
if not isinstance(ui_config, Mapping):
|
|
282
|
+
return {}
|
|
283
|
+
|
|
284
|
+
panels_config = ui_config.get("panels", {})
|
|
285
|
+
if isinstance(panels_config, Box):
|
|
286
|
+
return panels_config.to_dict()
|
|
287
|
+
if isinstance(panels_config, Mapping):
|
|
288
|
+
return dict(panels_config)
|
|
289
|
+
return {}
|
|
290
|
+
|
|
291
|
+
def _resolve_panel_config(self, kind: str | None, overrides: Mapping[str, Any]) -> dict[str, Any]:
|
|
292
|
+
defaults = copy.deepcopy(self._config["defaults"])
|
|
293
|
+
if not isinstance(defaults, dict):
|
|
294
|
+
raise PanelRenderingError("Panel defaults configuration must be a mapping")
|
|
295
|
+
config: dict[str, Any] = defaults
|
|
296
|
+
|
|
297
|
+
preset: Mapping[str, Any] = {}
|
|
298
|
+
raw_presets = self._config.get("presets", {})
|
|
299
|
+
if isinstance(raw_presets, Mapping):
|
|
300
|
+
candidate = raw_presets.get(kind or "", {})
|
|
301
|
+
if isinstance(candidate, Mapping):
|
|
302
|
+
preset = candidate
|
|
303
|
+
|
|
304
|
+
deep_merge(config, preset)
|
|
305
|
+
if overrides:
|
|
306
|
+
normalized = self._normalize_overrides(overrides)
|
|
307
|
+
deep_merge(config, normalized)
|
|
308
|
+
return config
|
|
309
|
+
|
|
310
|
+
def _normalize_overrides(self, overrides: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
|
|
311
|
+
normalized: dict[str, dict[str, Any]] = {"panel": {}, "content": {}}
|
|
312
|
+
panel_overrides = normalized["panel"]
|
|
313
|
+
content_overrides = normalized["content"]
|
|
314
|
+
direct_panel_keys = {
|
|
315
|
+
"title",
|
|
316
|
+
"title_align",
|
|
317
|
+
"subtitle",
|
|
318
|
+
"subtitle_align",
|
|
319
|
+
"border_style",
|
|
320
|
+
"padding",
|
|
321
|
+
"expand",
|
|
322
|
+
"highlight",
|
|
323
|
+
"box",
|
|
324
|
+
"icon",
|
|
325
|
+
"width",
|
|
326
|
+
"style",
|
|
327
|
+
}
|
|
328
|
+
direct_content_keys = {
|
|
329
|
+
"box",
|
|
330
|
+
"expand",
|
|
331
|
+
"show_header",
|
|
332
|
+
"key_label",
|
|
333
|
+
"value_label",
|
|
334
|
+
"key_style",
|
|
335
|
+
"value_style",
|
|
336
|
+
"header_style",
|
|
337
|
+
"pad_edge",
|
|
338
|
+
"sort_keys",
|
|
339
|
+
"use_markup",
|
|
340
|
+
"use_pretty",
|
|
341
|
+
"pretty_indent",
|
|
342
|
+
}
|
|
343
|
+
for key, value in overrides.items():
|
|
344
|
+
if key in ("panel", "content") and isinstance(value, Mapping):
|
|
345
|
+
deep_merge(normalized[key], value)
|
|
346
|
+
continue
|
|
347
|
+
if key in direct_panel_keys:
|
|
348
|
+
panel_overrides[key] = value
|
|
349
|
+
elif key in direct_content_keys:
|
|
350
|
+
content_overrides[key] = value
|
|
351
|
+
return normalized
|
|
352
|
+
|
|
353
|
+
def _build_renderable(self, payload: PanelPayload, content_config: dict[str, Any]) -> RenderableType:
|
|
354
|
+
if payload is None:
|
|
355
|
+
return Text("")
|
|
356
|
+
if self._is_renderable(payload):
|
|
357
|
+
return payload
|
|
358
|
+
if isinstance(payload, str):
|
|
359
|
+
if content_config.get("use_markup", True):
|
|
360
|
+
return Text.from_markup(payload)
|
|
361
|
+
return Text(payload)
|
|
362
|
+
if isinstance(payload, Mapping):
|
|
363
|
+
return self._mapping_to_table(payload, content_config)
|
|
364
|
+
if isinstance(payload, Sequence):
|
|
365
|
+
pairs = [tuple(item) for item in payload]
|
|
366
|
+
if all(len(pair) == PAIR_ENTRY_LENGTH for pair in pairs):
|
|
367
|
+
return self._pairs_to_table(pairs, content_config)
|
|
368
|
+
raise PanelRenderingError(f"Unsupported payload type: {type(payload)!r}")
|
|
369
|
+
|
|
370
|
+
def _mapping_to_table(self, payload: Mapping[str, Any], content_config: dict[str, Any]) -> Table:
|
|
371
|
+
items: Iterable[tuple[str, Any]] = payload.items()
|
|
372
|
+
if content_config.get("sort_keys", False):
|
|
373
|
+
items = sorted(items, key=lambda item: str(item[0]))
|
|
374
|
+
return self._pairs_to_table(list(items), content_config)
|
|
375
|
+
|
|
376
|
+
def _pairs_to_table(self, pairs: Sequence[tuple[Any, Any]], content_config: dict[str, Any]) -> Table:
|
|
377
|
+
table_box = self._resolve_box(content_config.get("box"), default="SIMPLE")
|
|
378
|
+
table = Table(
|
|
379
|
+
show_header=content_config.get("show_header", False),
|
|
380
|
+
header_style=content_config.get("header_style"),
|
|
381
|
+
box=table_box,
|
|
382
|
+
expand=content_config.get("expand", True),
|
|
383
|
+
pad_edge=content_config.get("pad_edge", False),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
key_label = content_config.get("key_label", "Key")
|
|
387
|
+
value_label = content_config.get("value_label", "Value")
|
|
388
|
+
key_style = cast("str | None", content_config.get("key_style"))
|
|
389
|
+
value_style = cast("str | None", content_config.get("value_style"))
|
|
390
|
+
table.add_column(key_label, style=key_style)
|
|
391
|
+
table.add_column(value_label, style=value_style)
|
|
392
|
+
|
|
393
|
+
for key, value in pairs:
|
|
394
|
+
key_renderable = self._to_text(key, key_style)
|
|
395
|
+
value_renderable = self._render_value(value, content_config)
|
|
396
|
+
table.add_row(key_renderable, value_renderable)
|
|
397
|
+
return table
|
|
398
|
+
|
|
399
|
+
def _render_value(self, value: Any, content_config: dict[str, Any]) -> RenderableType:
|
|
400
|
+
if self._is_renderable(value):
|
|
401
|
+
return value
|
|
402
|
+
value_style = cast("str | None", content_config.get("value_style"))
|
|
403
|
+
if isinstance(value, str):
|
|
404
|
+
return self._render_string_value(value, value_style, content_config)
|
|
405
|
+
if isinstance(value, Number):
|
|
406
|
+
return self._render_numeric_value(value, value_style)
|
|
407
|
+
if content_config.get("use_pretty", True):
|
|
408
|
+
indent = content_config.get("pretty_indent", DEFAULT_PRETTY_INDENT)
|
|
409
|
+
return Pretty(value, indent_guides=indent)
|
|
410
|
+
return self._render_repr_value(value, value_style)
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _render_string_value(value: str, value_style: str | None, content_config: dict[str, Any]) -> Text:
|
|
414
|
+
use_markup = content_config.get("use_markup", True)
|
|
415
|
+
if use_markup:
|
|
416
|
+
if value_style:
|
|
417
|
+
return Text.from_markup(value, style=value_style)
|
|
418
|
+
return Text.from_markup(value)
|
|
419
|
+
if value_style:
|
|
420
|
+
return Text(value, style=value_style)
|
|
421
|
+
return Text(value)
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _render_numeric_value(value: Number, value_style: str | None) -> Text:
|
|
425
|
+
formatted = Text(str(value))
|
|
426
|
+
if value_style:
|
|
427
|
+
formatted.stylize(value_style)
|
|
428
|
+
return formatted
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _render_repr_value(value: Any, value_style: str | None) -> Text:
|
|
432
|
+
representation = repr(value)
|
|
433
|
+
if value_style:
|
|
434
|
+
return Text(representation, style=value_style)
|
|
435
|
+
return Text(representation)
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _to_text(value: Any, style: str | None = None) -> Text:
|
|
439
|
+
text = Text(str(value))
|
|
440
|
+
if style:
|
|
441
|
+
text.stylize(style)
|
|
442
|
+
return text
|
|
443
|
+
|
|
444
|
+
@staticmethod
|
|
445
|
+
def _compose_title(title: str | None, icon: str | None) -> str | None:
|
|
446
|
+
if title and icon:
|
|
447
|
+
return f"{icon} {title}"
|
|
448
|
+
if icon:
|
|
449
|
+
return icon
|
|
450
|
+
return title
|
|
451
|
+
|
|
452
|
+
@staticmethod
|
|
453
|
+
def _is_renderable(candidate: Any) -> TypeGuard[RenderableType]:
|
|
454
|
+
return hasattr(candidate, "__rich_console__") or hasattr(candidate, "__rich__")
|
|
455
|
+
|
|
456
|
+
@staticmethod
|
|
457
|
+
def _coerce_padding(padding: Any) -> tuple[int, ...]:
|
|
458
|
+
if padding is None:
|
|
459
|
+
return DEFAULT_PADDING
|
|
460
|
+
if isinstance(padding, list | tuple):
|
|
461
|
+
coerced = tuple(int(part) for part in padding)
|
|
462
|
+
if len(coerced) in VALID_PADDING_LENGTHS:
|
|
463
|
+
return coerced
|
|
464
|
+
raise PanelRenderingError("Padding must contain 1, 2, or 4 integers.")
|
|
465
|
+
value = int(padding)
|
|
466
|
+
return (value, value)
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def _resolve_box(box_name: str | None, default: str = "ROUNDED") -> rich_box.Box:
|
|
470
|
+
if not box_name:
|
|
471
|
+
box_name = default
|
|
472
|
+
try:
|
|
473
|
+
return cast("rich_box.Box", getattr(rich_box, box_name))
|
|
474
|
+
except AttributeError as exc:
|
|
475
|
+
raise PanelRenderingError(f"Unknown box style '{box_name}'") from exc
|
|
476
|
+
|
|
477
|
+
def _ensure_console(self, console: Console | None) -> Console:
|
|
478
|
+
if console is not None:
|
|
479
|
+
return console
|
|
480
|
+
if self.console is None:
|
|
481
|
+
self.console = Console()
|
|
482
|
+
if self.console is None: # pragma: no cover - defensive guard
|
|
483
|
+
raise PanelRenderingError("Console instance could not be created")
|
|
484
|
+
return self.console
|