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.
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.1.dist-info/METADATA +201 -0
  159. kstlib-1.0.1.dist-info/RECORD +163 -0
  160. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
  161. kstlib-1.0.1.dist-info/entry_points.txt +2 -0
  162. kstlib-1.0.1.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.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