lewishowles-cli-style 0.15.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ .npmrc
3
+ dist/
4
+ build/
5
+ coverage/
6
+ adapters/swift/.build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lewis Howles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.5
2
+ Name: lewishowles-cli-style
3
+ Version: 0.15.0
4
+ Summary: Python adapter for the shared cli-style binary.
5
+ Project-URL: Repository, https://github.com/lewishowles/cli-style
6
+ Project-URL: Issues, https://github.com/lewishowles/cli-style/issues
7
+ Author: Lewis Howles
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # lewishowles-cli-style
23
+
24
+ `lewishowles-cli-style` is a Python adapter for the shared `cli-style` command-line renderer. It lets Python programs use the same terminal output styles as the JavaScript package.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ python -m pip install lewishowles-cli-style
30
+ ```
31
+
32
+ The package supports Python 3.9 and later. The installed distribution is named `lewishowles-cli-style`, and you import it as `cli_style`.
33
+
34
+ Rendering happens in the separate `cli-style` binary, so that needs to be on your `PATH`. The [main readme](https://github.com/lewishowles/cli-style#install) covers the ways to install it.
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from cli_style import render
40
+
41
+ print(render("status", {"type": "success", "label": "Build passed"}))
42
+ ```
43
+
44
+ If the `cli-style` binary is not available, `render()` returns sorted plain `key: value` text by default. Pass `raise_on_missing=True` to raise `CliStyleNotFoundError` instead.
45
+
46
+ ## More information
47
+
48
+ - [Repository](https://github.com/lewishowles/cli-style)
49
+ - [Issue tracker](https://github.com/lewishowles/cli-style/issues)
50
+ - [MIT licence](https://github.com/lewishowles/cli-style/blob/main/LICENSE)
@@ -0,0 +1,29 @@
1
+ # lewishowles-cli-style
2
+
3
+ `lewishowles-cli-style` is a Python adapter for the shared `cli-style` command-line renderer. It lets Python programs use the same terminal output styles as the JavaScript package.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ python -m pip install lewishowles-cli-style
9
+ ```
10
+
11
+ The package supports Python 3.9 and later. The installed distribution is named `lewishowles-cli-style`, and you import it as `cli_style`.
12
+
13
+ Rendering happens in the separate `cli-style` binary, so that needs to be on your `PATH`. The [main readme](https://github.com/lewishowles/cli-style#install) covers the ways to install it.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from cli_style import render
19
+
20
+ print(render("status", {"type": "success", "label": "Build passed"}))
21
+ ```
22
+
23
+ If the `cli-style` binary is not available, `render()` returns sorted plain `key: value` text by default. Pass `raise_on_missing=True` to raise `CliStyleNotFoundError` instead.
24
+
25
+ ## More information
26
+
27
+ - [Repository](https://github.com/lewishowles/cli-style)
28
+ - [Issue tracker](https://github.com/lewishowles/cli-style/issues)
29
+ - [MIT licence](https://github.com/lewishowles/cli-style/blob/main/LICENSE)
@@ -0,0 +1,73 @@
1
+ """Public Python API for the cli-style adapter."""
2
+
3
+ from ._wrappers import (
4
+ agent_transcript,
5
+ audit_finding,
6
+ bar_chart,
7
+ bullet_list,
8
+ chip,
9
+ command_result,
10
+ compact_data_table,
11
+ confirmation_result,
12
+ diagnostic_report,
13
+ diff_block,
14
+ divider,
15
+ empty_state,
16
+ error_block,
17
+ hint,
18
+ labelled_line,
19
+ next_step_block,
20
+ panel,
21
+ progress_bar,
22
+ row,
23
+ row_group,
24
+ sparkline,
25
+ span,
26
+ status,
27
+ step,
28
+ step_progress,
29
+ table,
30
+ task_summary,
31
+ )
32
+ from .core import (
33
+ CliStyleError,
34
+ CliStyleNotFoundError,
35
+ CliStyleRenderError,
36
+ render,
37
+ resolve_binary,
38
+ )
39
+
40
+ __all__ = [
41
+ "CliStyleError",
42
+ "CliStyleNotFoundError",
43
+ "CliStyleRenderError",
44
+ "agent_transcript",
45
+ "audit_finding",
46
+ "bar_chart",
47
+ "bullet_list",
48
+ "chip",
49
+ "command_result",
50
+ "compact_data_table",
51
+ "confirmation_result",
52
+ "diagnostic_report",
53
+ "diff_block",
54
+ "divider",
55
+ "empty_state",
56
+ "error_block",
57
+ "hint",
58
+ "labelled_line",
59
+ "next_step_block",
60
+ "panel",
61
+ "progress_bar",
62
+ "render",
63
+ "resolve_binary",
64
+ "row",
65
+ "row_group",
66
+ "sparkline",
67
+ "span",
68
+ "status",
69
+ "step",
70
+ "step_progress",
71
+ "table",
72
+ "task_summary",
73
+ ]
@@ -0,0 +1,503 @@
1
+ # Generated by scripts/generate-adapters.js. Do not edit directly.
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .core import render
8
+
9
+ def agent_transcript(
10
+ entries: list[dict[str, Any]] | None = None,
11
+ title: str | None = None,
12
+ **kwargs: Any,
13
+ ) -> str:
14
+ """Return agent-transcript output; extra keyword arguments go to render()."""
15
+ data = {
16
+ "entries": [] if entries is None else entries,
17
+ }
18
+
19
+ if title is not None:
20
+ data["title"] = title
21
+
22
+ return render("agent-transcript", data, **kwargs)
23
+
24
+
25
+ def audit_finding(
26
+ result: str,
27
+ finding: str,
28
+ location: str = "",
29
+ recommendation: str = "",
30
+ evidence: str = "",
31
+ reference: str = "",
32
+ **kwargs: Any,
33
+ ) -> str:
34
+ """Return audit-finding output; extra keyword arguments go to render()."""
35
+ data = {
36
+ "result": result,
37
+ "finding": finding,
38
+ "location": location,
39
+ "recommendation": recommendation,
40
+ "evidence": [evidence] if evidence else [],
41
+ "references": [reference] if reference else [],
42
+ }
43
+
44
+ return render("audit-finding", data, **kwargs)
45
+
46
+
47
+ def bar_chart(
48
+ rows: list[dict[str, Any]] | None = None,
49
+ bar_width: int | float | None = None,
50
+ **kwargs: Any,
51
+ ) -> str:
52
+ """Return bar-chart output; extra keyword arguments go to render()."""
53
+ data = {
54
+ "rows": [] if rows is None else rows,
55
+ }
56
+
57
+ if bar_width is not None:
58
+ data["barWidth"] = bar_width
59
+
60
+ return render("bar-chart", data, **kwargs)
61
+
62
+
63
+ def bullet_list(
64
+ items: list[dict[str, Any]],
65
+ **kwargs: Any,
66
+ ) -> str:
67
+ """Return bullet-list output; extra keyword arguments go to render()."""
68
+ data = {
69
+ "items": items,
70
+ }
71
+
72
+ return render("bullet-list", data, **kwargs)
73
+
74
+
75
+ def chip(
76
+ label: str,
77
+ tone: str = "neutral",
78
+ **kwargs: Any,
79
+ ) -> str:
80
+ """Return chip output; extra keyword arguments go to render()."""
81
+ data = {
82
+ "label": label,
83
+ "tone": tone,
84
+ }
85
+
86
+ return render("chip", data, **kwargs)
87
+
88
+
89
+ def command_result(
90
+ result: str,
91
+ summary: str,
92
+ command: str = "",
93
+ exit_code: int | float | None = None,
94
+ duration: str = "",
95
+ detail: str = "",
96
+ **kwargs: Any,
97
+ ) -> str:
98
+ """Return command-result output; extra keyword arguments go to render()."""
99
+ data = {
100
+ "result": result,
101
+ "summary": summary,
102
+ "command": command,
103
+ "duration": duration,
104
+ "details": [detail] if detail else [],
105
+ }
106
+
107
+ if exit_code is not None:
108
+ data["exitCode"] = exit_code
109
+
110
+ return render("command-result", data, **kwargs)
111
+
112
+
113
+ def compact_data_table(
114
+ columns: list[dict[str, Any]],
115
+ rows: list[dict[str, Any]],
116
+ summary: str = "",
117
+ title: str | None = None,
118
+ **kwargs: Any,
119
+ ) -> str:
120
+ """Return compact-data-table output; extra keyword arguments go to render()."""
121
+ data = {
122
+ "columns": columns,
123
+ "rows": rows,
124
+ "summary": summary,
125
+ }
126
+
127
+ if title is not None:
128
+ data["title"] = title
129
+
130
+ return render("compact-data-table", data, **kwargs)
131
+
132
+
133
+ def confirmation_result(
134
+ state: str,
135
+ action: str,
136
+ item: str = "",
137
+ detail: str = "",
138
+ **kwargs: Any,
139
+ ) -> str:
140
+ """Return confirmation-result output; extra keyword arguments go to render()."""
141
+ data = {
142
+ "state": state,
143
+ "action": action,
144
+ "item": item,
145
+ "detail": detail,
146
+ }
147
+
148
+ return render("confirmation-result", data, **kwargs)
149
+
150
+
151
+ def diagnostic_report(
152
+ title: str | None = None,
153
+ checks: list[dict[str, Any]] | None = None,
154
+ findings: list[dict[str, Any]] | None = None,
155
+ skipped_checks: list[dict[str, Any]] | None = None,
156
+ next_actions: list[str] | None = None,
157
+ **kwargs: Any,
158
+ ) -> str:
159
+ """Return diagnostic-report output; extra keyword arguments go to render()."""
160
+ data = {
161
+ "checks": [] if checks is None else checks,
162
+ "findings": [] if findings is None else findings,
163
+ "skippedChecks": [] if skipped_checks is None else skipped_checks,
164
+ "nextActions": [] if next_actions is None else next_actions,
165
+ }
166
+
167
+ if title is not None:
168
+ data["title"] = title
169
+
170
+ return render("diagnostic-report", data, **kwargs)
171
+
172
+
173
+ def diff_block(
174
+ lines: list[dict[str, Any]] | None = None,
175
+ path: str = "",
176
+ title: str | None = None,
177
+ **kwargs: Any,
178
+ ) -> str:
179
+ """Return diff-block output; extra keyword arguments go to render()."""
180
+ data = {
181
+ "path": path,
182
+ }
183
+
184
+ if lines is not None:
185
+ data["lines"] = lines
186
+ if title is not None:
187
+ data["title"] = title
188
+
189
+ return render("diff-block", data, **kwargs)
190
+
191
+
192
+ def divider(
193
+ label: str = "",
194
+ divider_width: int | float | None = None,
195
+ divider_colour: str | None = None,
196
+ label_colour: str | None = None,
197
+ **kwargs: Any,
198
+ ) -> str:
199
+ """Return divider output; extra keyword arguments go to render()."""
200
+ data = {
201
+ "label": label,
202
+ }
203
+
204
+ if divider_width is not None:
205
+ data["dividerWidth"] = divider_width
206
+ if divider_colour is not None:
207
+ data["dividerColour"] = divider_colour
208
+ if label_colour is not None:
209
+ data["labelColour"] = label_colour
210
+
211
+ return render("divider", data, **kwargs)
212
+
213
+
214
+ def empty_state(
215
+ title: str = "No results",
216
+ detail: str = "",
217
+ **kwargs: Any,
218
+ ) -> str:
219
+ """Return empty-state output; extra keyword arguments go to render()."""
220
+ data = {
221
+ "title": title,
222
+ "detail": detail,
223
+ }
224
+
225
+ return render("empty-state", data, **kwargs)
226
+
227
+
228
+ def error_block(
229
+ title: str = "Failed",
230
+ lines: list[str] | None = None,
231
+ **kwargs: Any,
232
+ ) -> str:
233
+ """Return error-block output; extra keyword arguments go to render()."""
234
+ data = {
235
+ "title": title,
236
+ "lines": [] if lines is None else lines,
237
+ }
238
+
239
+ return render("error-block", data, **kwargs)
240
+
241
+
242
+ def hint(
243
+ message: str,
244
+ **kwargs: Any,
245
+ ) -> str:
246
+ """Return hint output; extra keyword arguments go to render()."""
247
+ data = {
248
+ "message": message,
249
+ }
250
+
251
+ return render("hint", data, **kwargs)
252
+
253
+
254
+ def labelled_line(
255
+ label: str = "",
256
+ message: str = "",
257
+ icon: str = "",
258
+ tone: str = "info",
259
+ wrap: bool = True,
260
+ wrap_width: int | float | None = None,
261
+ **kwargs: Any,
262
+ ) -> str:
263
+ """Return labelled-line output; extra keyword arguments go to render()."""
264
+ data = {
265
+ "label": label,
266
+ "message": message,
267
+ "icon": icon,
268
+ "tone": tone,
269
+ "wrap": wrap,
270
+ }
271
+
272
+ if wrap_width is not None:
273
+ data["wrapWidth"] = wrap_width
274
+
275
+ return render("labelled-line", data, **kwargs)
276
+
277
+
278
+ def next_step_block(
279
+ next_step: str,
280
+ reason: str = "",
281
+ command: str = "",
282
+ alternative: str = "",
283
+ **kwargs: Any,
284
+ ) -> str:
285
+ """Return next-step-block output; extra keyword arguments go to render()."""
286
+ data = {
287
+ "next": next_step,
288
+ "reason": reason,
289
+ "commands": [command] if command else [],
290
+ "alternatives": [alternative] if alternative else [],
291
+ }
292
+
293
+ return render("next-step-block", data, **kwargs)
294
+
295
+
296
+ def panel(
297
+ lines: list[str] | None = None,
298
+ title: str | None = None,
299
+ tone: str = "info",
300
+ panel_width: int | float | None = None,
301
+ **kwargs: Any,
302
+ ) -> str:
303
+ """Return panel output; extra keyword arguments go to render()."""
304
+ data = {
305
+ "lines": [] if lines is None else lines,
306
+ "tone": tone,
307
+ }
308
+
309
+ if title is not None:
310
+ data["title"] = title
311
+ if panel_width is not None:
312
+ data["panelWidth"] = panel_width
313
+
314
+ return render("panel", data, **kwargs)
315
+
316
+
317
+ def progress_bar(
318
+ max: int | float = 100,
319
+ value: int | float = 0,
320
+ bar_width: int | float | None = None,
321
+ tone: str = "success",
322
+ **kwargs: Any,
323
+ ) -> str:
324
+ """Return progress-bar output; extra keyword arguments go to render()."""
325
+ data = {
326
+ "max": max,
327
+ "value": value,
328
+ "tone": tone,
329
+ }
330
+
331
+ if bar_width is not None:
332
+ data["barWidth"] = bar_width
333
+
334
+ return render("progress-bar", data, **kwargs)
335
+
336
+
337
+ def row(
338
+ label: str,
339
+ value: str,
340
+ result: str = "",
341
+ label_width: int | float | None = None,
342
+ label_colour: str | None = None,
343
+ value_colour: str | None = None,
344
+ separator: str | None = None,
345
+ **kwargs: Any,
346
+ ) -> str:
347
+ """Return row output; extra keyword arguments go to render()."""
348
+ data = {
349
+ "label": label,
350
+ "value": value,
351
+ "result": result,
352
+ }
353
+
354
+ if label_width is not None:
355
+ data["labelWidth"] = label_width
356
+ if label_colour is not None:
357
+ data["labelColour"] = label_colour
358
+ if value_colour is not None:
359
+ data["valueColour"] = value_colour
360
+ if separator is not None:
361
+ data["separator"] = separator
362
+
363
+ return render("row", data, **kwargs)
364
+
365
+
366
+ def row_group(
367
+ rows: list[dict[str, Any]],
368
+ label_width: int | float | None = None,
369
+ **kwargs: Any,
370
+ ) -> str:
371
+ """Return row-group output; extra keyword arguments go to render()."""
372
+ data = {
373
+ "rows": rows,
374
+ }
375
+
376
+ if label_width is not None:
377
+ data["labelWidth"] = label_width
378
+
379
+ return render("row-group", data, **kwargs)
380
+
381
+
382
+ def sparkline(
383
+ values: list[int | float] | None = None,
384
+ width: int | float | None = None,
385
+ label: str | None = None,
386
+ tone: str = "success",
387
+ **kwargs: Any,
388
+ ) -> str:
389
+ """Return sparkline output; extra keyword arguments go to render()."""
390
+ data = {
391
+ "values": [] if values is None else values,
392
+ "tone": tone,
393
+ }
394
+
395
+ if width is not None:
396
+ data["width"] = width
397
+ if label is not None:
398
+ data["label"] = label
399
+
400
+ return render("sparkline", data, **kwargs)
401
+
402
+
403
+ def span(
404
+ value: str,
405
+ tone: str = "info",
406
+ weight: str | None = None,
407
+ **kwargs: Any,
408
+ ) -> str:
409
+ """Return span output; extra keyword arguments go to render()."""
410
+ data = {
411
+ "value": value,
412
+ "tone": tone,
413
+ }
414
+
415
+ if weight is not None:
416
+ data["weight"] = weight
417
+
418
+ return render("span", data, **kwargs)
419
+
420
+
421
+ def status(
422
+ type: str,
423
+ label: str = "",
424
+ detail: str = "",
425
+ **kwargs: Any,
426
+ ) -> str:
427
+ """Return status output; extra keyword arguments go to render()."""
428
+ data = {
429
+ "type": type,
430
+ "label": label,
431
+ "detail": detail,
432
+ }
433
+
434
+ return render("status", data, **kwargs)
435
+
436
+
437
+ def step(
438
+ label: str = "",
439
+ state: str = "pending",
440
+ **kwargs: Any,
441
+ ) -> str:
442
+ """Return step output; extra keyword arguments go to render()."""
443
+ data = {
444
+ "label": label,
445
+ "state": state,
446
+ }
447
+
448
+ return render("step", data, **kwargs)
449
+
450
+
451
+ def step_progress(
452
+ current: int | float = 0,
453
+ steps: list[str] | None = None,
454
+ **kwargs: Any,
455
+ ) -> str:
456
+ """Return step-progress output; extra keyword arguments go to render()."""
457
+ data = {
458
+ "current": current,
459
+ "steps": [] if steps is None else steps,
460
+ }
461
+
462
+ return render("step-progress", data, **kwargs)
463
+
464
+
465
+ def table(
466
+ columns: list[dict[str, Any]],
467
+ rows: list[dict[str, Any]],
468
+ unicode: bool | None = None,
469
+ width: int | float | None = None,
470
+ **kwargs: Any,
471
+ ) -> str:
472
+ """Return table output; extra keyword arguments go to render()."""
473
+ data = {
474
+ "columns": columns,
475
+ "rows": rows,
476
+ }
477
+
478
+ if unicode is not None:
479
+ data["unicode"] = unicode
480
+ if width is not None:
481
+ data["width"] = width
482
+
483
+ return render("table", data, **kwargs)
484
+
485
+
486
+ def task_summary(
487
+ result: str,
488
+ task: str,
489
+ summary: str = "",
490
+ completed: str = "",
491
+ remaining: str = "",
492
+ **kwargs: Any,
493
+ ) -> str:
494
+ """Return task-summary output; extra keyword arguments go to render()."""
495
+ data = {
496
+ "result": result,
497
+ "task": task,
498
+ "summary": summary,
499
+ "completed": [completed] if completed else [],
500
+ "remaining": [remaining] if remaining else [],
501
+ }
502
+
503
+ return render("task-summary", data, **kwargs)
@@ -0,0 +1,136 @@
1
+ """Run the shared cli-style binary from Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any, Sequence
12
+
13
+
14
+ class CliStyleError(RuntimeError):
15
+ """Base error for cli-style adapter failures."""
16
+
17
+
18
+ class CliStyleNotFoundError(CliStyleError):
19
+ """Raised when the cli-style binary cannot be found."""
20
+
21
+
22
+ class CliStyleRenderError(CliStyleError):
23
+ """Raised when cli-style rejects a render request."""
24
+
25
+
26
+ def resolve_subprocess_environment() -> dict[str, str]:
27
+ """Resolve the child environment while preserving caller controls."""
28
+ environment = os.environ.copy()
29
+
30
+ if (
31
+ sys.stdout.isatty()
32
+ and "FORCE_COLOR" not in environment
33
+ and "NO_COLOR" not in environment
34
+ ):
35
+ environment["FORCE_COLOR"] = "1"
36
+
37
+ return environment
38
+
39
+
40
+ def render(
41
+ renderer: str,
42
+ data: dict[str, Any],
43
+ *,
44
+ binary: str = "cli-style",
45
+ profile: str | None = None,
46
+ width: int | None = None,
47
+ plain: bool = False,
48
+ no_colour: bool = False,
49
+ no_unicode: bool = False,
50
+ extra_args: Sequence[str] | None = None,
51
+ raise_on_missing: bool = False,
52
+ ) -> str:
53
+ """Render JSON-serialisable data through ``cli-style render``.
54
+
55
+ When the binary cannot be found, return a plain-text rendering of the data
56
+ instead, unless ``raise_on_missing`` is set.
57
+
58
+ :param renderer: Stable renderer name accepted by the cli-style binary.
59
+ :param data: JSON-serialisable renderer input.
60
+ :param binary: Binary path or command name.
61
+ :param profile: Optional output profile.
62
+ :param width: Optional terminal width.
63
+ :param plain: Disable terminal colour and Unicode output.
64
+ :param no_colour: Disable terminal colour output.
65
+ :param no_unicode: Disable Unicode output.
66
+ :param extra_args: Additional flags passed to the binary.
67
+ :param raise_on_missing: Raise ``CliStyleNotFoundError`` when the binary
68
+ cannot be found, instead of returning plain text.
69
+ """
70
+ if not isinstance(renderer, str) or renderer == "":
71
+ raise ValueError("renderer must be a non-empty string")
72
+
73
+ if not isinstance(data, dict):
74
+ raise TypeError("data must be a dict")
75
+
76
+ try:
77
+ resolved_binary = resolve_binary(binary)
78
+ except CliStyleNotFoundError:
79
+ if raise_on_missing:
80
+ raise
81
+
82
+ # Sort the keys so the lines match the Swift adapter, which has no key order to keep.
83
+ return "\n".join(f"{key}: {value}" for key, value in sorted(data.items(), key=lambda item: str(item[0])))
84
+
85
+ command = [resolved_binary, "render", renderer]
86
+
87
+ if profile is not None:
88
+ command.extend(["--profile", profile])
89
+
90
+ if width is not None:
91
+ command.extend(["--width", str(width)])
92
+
93
+ if plain:
94
+ command.append("--plain")
95
+
96
+ if no_colour:
97
+ command.append("--no-colour")
98
+
99
+ if no_unicode:
100
+ command.append("--no-unicode")
101
+
102
+ if extra_args is not None:
103
+ command.extend(extra_args)
104
+
105
+ result = subprocess.run(
106
+ command,
107
+ capture_output=True,
108
+ check=False,
109
+ env=resolve_subprocess_environment(),
110
+ input=json.dumps(data),
111
+ text=True,
112
+ )
113
+
114
+ if result.returncode != 0:
115
+ message = result.stderr.strip() or f"cli-style render failed with exit code {result.returncode}"
116
+ raise CliStyleRenderError(message)
117
+
118
+ return result.stdout.rstrip("\n")
119
+
120
+
121
+ def resolve_binary(binary: str) -> str:
122
+ """Resolve a cli-style command name or executable path."""
123
+ if "/" in binary:
124
+ path = Path(binary)
125
+
126
+ if path.is_file() and os.access(path, os.X_OK):
127
+ return str(path)
128
+
129
+ raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
130
+
131
+ resolved = shutil.which(binary)
132
+
133
+ if resolved is None:
134
+ raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
135
+
136
+ return resolved
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "lewishowles-cli-style"
3
+ version = "0.15.0"
4
+ description = "Python adapter for the shared cli-style binary."
5
+ authors = [{ name = "Lewis Howles" }]
6
+ license = { text = "MIT" }
7
+ license-files = ["LICENSE"]
8
+ readme = "README.md"
9
+ requires-python = ">=3.9"
10
+ dependencies = []
11
+ classifiers = [
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/lewishowles/cli-style"
25
+ Issues = "https://github.com/lewishowles/cli-style/issues"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["cli_style"]