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.
- lewishowles_cli_style-0.15.0/.gitignore +6 -0
- lewishowles_cli_style-0.15.0/LICENSE +21 -0
- lewishowles_cli_style-0.15.0/PKG-INFO +50 -0
- lewishowles_cli_style-0.15.0/README.md +29 -0
- lewishowles_cli_style-0.15.0/cli_style/__init__.py +73 -0
- lewishowles_cli_style-0.15.0/cli_style/_wrappers.py +503 -0
- lewishowles_cli_style-0.15.0/cli_style/core.py +136 -0
- lewishowles_cli_style-0.15.0/pyproject.toml +32 -0
|
@@ -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"]
|