runall 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runall/__init__.py +20 -0
- runall/config.py +28 -0
- runall/processor.py +22 -0
- runall/tui.py +280 -0
- runall-0.1.0.dist-info/METADATA +160 -0
- runall-0.1.0.dist-info/RECORD +8 -0
- runall-0.1.0.dist-info/WHEEL +4 -0
- runall-0.1.0.dist-info/licenses/LICENSE +21 -0
runall/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Run shell commands over a set of input files with a terminal progress display.
|
|
2
|
+
|
|
3
|
+
The ``runall`` package lets a user register Python functions with the
|
|
4
|
+
:func:`runthat` decorator. Each registered function receives the path of an
|
|
5
|
+
input file and returns a shell command. The command is executed for every
|
|
6
|
+
input file that matches a glob pattern, while a Textual-based terminal user
|
|
7
|
+
interface (:class:`runall.tui.RunAllApp`) displays one progress bar per
|
|
8
|
+
registered function and a running list of the processed files.
|
|
9
|
+
|
|
10
|
+
Public names:
|
|
11
|
+
CONFIG: A default configuration dictionary.
|
|
12
|
+
discover_input_files: Resolves the input files described by a configuration.
|
|
13
|
+
runthat: Decorator that registers a function in a configuration.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .config import CONFIG
|
|
17
|
+
from .processor import discover_input_files
|
|
18
|
+
from .tui import runthat
|
|
19
|
+
|
|
20
|
+
__all__ = ["CONFIG", "discover_input_files", "runthat"]
|
runall/config.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Default configuration for the ``runall`` package.
|
|
2
|
+
|
|
3
|
+
A configuration is a plain dictionary. The same structure may be defined by
|
|
4
|
+
the user and passed to :func:`runall.tui.runthat` and
|
|
5
|
+
:class:`runall.tui.RunAllApp`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
CONFIG = {
|
|
9
|
+
"runthat": {},
|
|
10
|
+
"input_dir": "instances/ht0_*.txt",
|
|
11
|
+
"output_dir": "output",
|
|
12
|
+
"output_preffix": "out_",
|
|
13
|
+
"output_header": "execucao,arquivo",
|
|
14
|
+
"err_dir": "err",
|
|
15
|
+
}
|
|
16
|
+
"""Default configuration dictionary.
|
|
17
|
+
|
|
18
|
+
Keys:
|
|
19
|
+
runthat: Registry that maps function names to the wrapped callables. It is
|
|
20
|
+
populated by the :func:`runall.tui.runthat` decorator.
|
|
21
|
+
input_dir: Glob pattern that selects the input files.
|
|
22
|
+
output_dir: Directory that receives the standard output of each command.
|
|
23
|
+
output_preffix: Prefix of every output file name. The key name keeps its
|
|
24
|
+
historical spelling.
|
|
25
|
+
output_header: Header line written once at the top of each new output file.
|
|
26
|
+
err_dir: Directory that receives the executed commands and their standard
|
|
27
|
+
error output.
|
|
28
|
+
"""
|
runall/processor.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Discovery of the input files that ``runall`` processes."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
|
|
5
|
+
from .config import CONFIG
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def discover_input_files(config: dict | None = None) -> list[str]:
|
|
9
|
+
"""Return the input files that match the configured glob pattern.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
config: Configuration dictionary whose ``"input_dir"`` entry holds the
|
|
13
|
+
glob pattern. When ``None``, the default
|
|
14
|
+
:data:`runall.config.CONFIG` is used.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The matching paths in lexicographic order. The list is empty when no
|
|
18
|
+
file matches the pattern.
|
|
19
|
+
"""
|
|
20
|
+
if config is None:
|
|
21
|
+
config = CONFIG
|
|
22
|
+
return sorted(glob.glob(config["input_dir"]))
|
runall/tui.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Command registration and terminal user interface for ``runall``.
|
|
2
|
+
|
|
3
|
+
This module provides the :func:`runthat` decorator, which registers functions
|
|
4
|
+
that build shell commands, and :class:`RunAllApp`, a Textual application that
|
|
5
|
+
runs every registered function over every input file while it displays
|
|
6
|
+
progress.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import functools
|
|
11
|
+
import pathlib
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
from textual.app import App, ComposeResult
|
|
15
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
16
|
+
from textual.widgets import Label, ProgressBar
|
|
17
|
+
|
|
18
|
+
# from .config import CONFIG
|
|
19
|
+
from .processor import discover_input_files
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _output_path(config: dict, label: str) -> pathlib.Path:
|
|
23
|
+
"""Build the path of the output file for a registered function.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Configuration dictionary.
|
|
27
|
+
label: Name of the registered function.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The path ``<output_dir>/<output_preffix><label>.csv``.
|
|
31
|
+
"""
|
|
32
|
+
return pathlib.Path(config["output_dir"]) / f"{config['output_preffix']}{label}.csv"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _err_path(config: dict, label: str) -> pathlib.Path:
|
|
36
|
+
"""Build the path of the error log file for a registered function.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config: Configuration dictionary.
|
|
40
|
+
label: Name of the registered function.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The path ``<err_dir>/<output_preffix><label>.csv``.
|
|
44
|
+
"""
|
|
45
|
+
return pathlib.Path(config["err_dir"]) / f"{config['output_preffix']}{label}.csv"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _redirect_output(
|
|
49
|
+
config: dict, label: str, proc: subprocess.CompletedProcess, cmd: str
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Persist the result of a command to the output and error files.
|
|
52
|
+
|
|
53
|
+
The standard output is appended to the output file. When that file does
|
|
54
|
+
not exist yet, it is created with the configured header line first. The
|
|
55
|
+
executed command and its standard error output are appended to the error
|
|
56
|
+
file. Missing directories are created as needed.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config: Configuration dictionary.
|
|
60
|
+
label: Name of the registered function that produced the command.
|
|
61
|
+
proc: Completed process whose streams are persisted.
|
|
62
|
+
cmd: Shell command that was executed.
|
|
63
|
+
"""
|
|
64
|
+
out_path = _output_path(config, label)
|
|
65
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
if not out_path.exists():
|
|
67
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
68
|
+
f.write(f"{config['output_header']}\n")
|
|
69
|
+
with open(out_path, "a", encoding="utf-8") as f:
|
|
70
|
+
f.write(proc.stdout)
|
|
71
|
+
|
|
72
|
+
err_path = _err_path(config, label)
|
|
73
|
+
err_path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
with open(err_path, "a", encoding="utf-8") as f:
|
|
75
|
+
f.write(f"$ {cmd}\n")
|
|
76
|
+
if proc.stderr:
|
|
77
|
+
f.write(proc.stderr)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def runthat(config: dict):
|
|
81
|
+
"""Create a decorator that registers a command-building function.
|
|
82
|
+
|
|
83
|
+
The decorated function receives the path of an input file and must return
|
|
84
|
+
a shell command as a string. The returned wrapper executes that command
|
|
85
|
+
through the shell, writes its output through :func:`_redirect_output`, and
|
|
86
|
+
returns the completed process. The wrapper is stored in
|
|
87
|
+
``config["runthat"]`` under the name of the decorated function.
|
|
88
|
+
|
|
89
|
+
The command runs with ``shell=True``. Input file names are therefore
|
|
90
|
+
interpolated into a shell command line, and the function must quote them
|
|
91
|
+
when they may contain special characters. The exit status of the command
|
|
92
|
+
is recorded in the returned process but is not treated as an error.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
config: Configuration dictionary. It must contain a ``"runthat"``
|
|
96
|
+
dictionary, which receives the registered wrapper.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A decorator that registers a function and returns its wrapper.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def decorator(func):
|
|
103
|
+
"""Wrap and register a command-building function.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
func: Function that maps an input file path to a shell command.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The wrapper that executes the command for a given input file.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
@functools.wraps(func)
|
|
113
|
+
def wrapper(filename: str) -> subprocess.CompletedProcess:
|
|
114
|
+
"""Execute the command built for an input file.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
filename: Path of the input file.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The completed process with captured text output.
|
|
121
|
+
"""
|
|
122
|
+
cmd = func(filename)
|
|
123
|
+
proc = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
124
|
+
_redirect_output(config, func.__name__, proc, cmd)
|
|
125
|
+
return proc
|
|
126
|
+
|
|
127
|
+
config["runthat"][func.__name__] = wrapper
|
|
128
|
+
return wrapper
|
|
129
|
+
|
|
130
|
+
return decorator
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
CSS = """
|
|
134
|
+
#ls {
|
|
135
|
+
height: 75%;
|
|
136
|
+
width: 90%;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#ls .fn-row {
|
|
140
|
+
height: 1;
|
|
141
|
+
padding: 0;
|
|
142
|
+
align: left middle;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#ls .fn-label {
|
|
146
|
+
width: 12;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#ls ProgressBar {
|
|
150
|
+
width: 1fr;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Bar > .bar--bar {
|
|
154
|
+
color: $primary;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Bar.running > .bar--bar {
|
|
158
|
+
color: #ffa500;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
Bar.done > .bar--bar {
|
|
162
|
+
color: $success;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#li {
|
|
166
|
+
height: 15;
|
|
167
|
+
max-height: 25%;
|
|
168
|
+
margin: 0 1;
|
|
169
|
+
border: round $primary;
|
|
170
|
+
background: $surface;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#li Label {
|
|
174
|
+
padding: 0 1;
|
|
175
|
+
}
|
|
176
|
+
"""
|
|
177
|
+
"""Textual stylesheet of the application.
|
|
178
|
+
|
|
179
|
+
The progress bar of the function that is currently running is drawn in orange.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class RunAllApp(App):
|
|
184
|
+
"""Textual application that runs registered functions over input files.
|
|
185
|
+
|
|
186
|
+
The interface shows one progress bar per registered function and a scrolling
|
|
187
|
+
list of the input files in the order in which they are processed. For each
|
|
188
|
+
input file, every registered function runs in registration order. The
|
|
189
|
+
application exits when all files have been processed.
|
|
190
|
+
|
|
191
|
+
Attributes:
|
|
192
|
+
config: Configuration dictionary shared with the registered functions.
|
|
193
|
+
files: Input files to process. It is assigned when the application is
|
|
194
|
+
mounted.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
TITLE = "RunAll"
|
|
198
|
+
CSS = CSS
|
|
199
|
+
|
|
200
|
+
def __init__(self, config: dict, **kwargs: object) -> None:
|
|
201
|
+
"""Initialize the application.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
config: Configuration dictionary. Its ``"runthat"`` entry holds the
|
|
205
|
+
registered functions and its ``"input_dir"`` entry selects the
|
|
206
|
+
input files.
|
|
207
|
+
**kwargs: Additional keyword arguments forwarded to
|
|
208
|
+
:class:`textual.app.App`.
|
|
209
|
+
"""
|
|
210
|
+
super().__init__(**kwargs)
|
|
211
|
+
self.config = config
|
|
212
|
+
|
|
213
|
+
def compose(self) -> ComposeResult:
|
|
214
|
+
"""Build the widget tree.
|
|
215
|
+
|
|
216
|
+
Yields:
|
|
217
|
+
One labeled progress bar for each registered function, followed by
|
|
218
|
+
a scrollable container that lists the processed files.
|
|
219
|
+
"""
|
|
220
|
+
total = len(discover_input_files(self.config))
|
|
221
|
+
with Vertical(id="ls"):
|
|
222
|
+
for label in self.config["runthat"]:
|
|
223
|
+
with Horizontal(id=f"fn-row-{label}", classes="fn-row"):
|
|
224
|
+
yield Label(label, classes="fn-label")
|
|
225
|
+
yield ProgressBar(
|
|
226
|
+
total=total,
|
|
227
|
+
show_percentage=True,
|
|
228
|
+
show_eta=False,
|
|
229
|
+
id=f"bar-{label}",
|
|
230
|
+
)
|
|
231
|
+
with VerticalScroll(id="li"):
|
|
232
|
+
yield Label("")
|
|
233
|
+
|
|
234
|
+
def on_mount(self) -> None:
|
|
235
|
+
"""Start processing once the interface is mounted.
|
|
236
|
+
|
|
237
|
+
The application exits with a message, without processing anything,
|
|
238
|
+
when no function is registered or no input file matches the configured
|
|
239
|
+
pattern.
|
|
240
|
+
"""
|
|
241
|
+
if not self.config["runthat"]:
|
|
242
|
+
self.exit(message="Nenhuma função registrada com @runthat.")
|
|
243
|
+
return
|
|
244
|
+
self.files = discover_input_files(self.config)
|
|
245
|
+
if not self.files:
|
|
246
|
+
self.exit(message=f"Nenhum arquivo encontrado: {self.config['input_dir']}")
|
|
247
|
+
return
|
|
248
|
+
self.run_worker(self.process_all())
|
|
249
|
+
|
|
250
|
+
async def process_all(self) -> None:
|
|
251
|
+
"""Run every registered function over every input file.
|
|
252
|
+
|
|
253
|
+
Files are processed sequentially. For each file, the registered
|
|
254
|
+
functions run one after another in a worker thread so that the
|
|
255
|
+
interface stays responsive. The progress bar of the running function is
|
|
256
|
+
highlighted and advances when the function finishes. The application
|
|
257
|
+
exits after the last file.
|
|
258
|
+
"""
|
|
259
|
+
total = len(self.files)
|
|
260
|
+
for index, filename in enumerate(self.files, start=1):
|
|
261
|
+
await self._add_file_line(index, total, filename)
|
|
262
|
+
for label in self.config["runthat"]:
|
|
263
|
+
pb = self.query_one(f"#bar-{label}", ProgressBar)
|
|
264
|
+
bar = pb.query_one("#bar")
|
|
265
|
+
bar.set_class(True, "running")
|
|
266
|
+
await asyncio.to_thread(self.config["runthat"][label], filename)
|
|
267
|
+
bar.set_class(False, "running")
|
|
268
|
+
pb.advance(1)
|
|
269
|
+
self.exit(message="Processamento concluído.")
|
|
270
|
+
|
|
271
|
+
async def _add_file_line(self, index: int, total: int, filename: str) -> None:
|
|
272
|
+
"""Append a line for the file being processed to the file list.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
index: One-based position of the file.
|
|
276
|
+
total: Total number of input files.
|
|
277
|
+
filename: Path of the file.
|
|
278
|
+
"""
|
|
279
|
+
line = Label(f"[{index}/{total}] {filename}")
|
|
280
|
+
await self.query_one("#li", VerticalScroll).mount(line)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: runall
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run functions registered by @runthat decorator with a set of progress bars.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tavaresneto/pyrunall
|
|
6
|
+
Project-URL: Repository, https://github.com/tavaresneto/pyrunall
|
|
7
|
+
Project-URL: Issues, https://github.com/tavaresneto/pyrunall/issues
|
|
8
|
+
Author: Roberto Fernandes Tavares Neto
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: textual>=8.2.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# runall
|
|
16
|
+
|
|
17
|
+
`runall` is a Python package that runs shell commands over a set of input files
|
|
18
|
+
and displays the progress in a terminal user interface (TUI). Commands are
|
|
19
|
+
declared as ordinary Python functions registered with the `@runthat` decorator.
|
|
20
|
+
For every function, the interface shows a progress bar (the bar of the function
|
|
21
|
+
that is currently running is highlighted in orange) and a scrolling list of the
|
|
22
|
+
files that have been processed.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Registers any number of command-building functions with a single decorator.
|
|
27
|
+
- Selects input files with a glob pattern.
|
|
28
|
+
- Displays one progress bar per function and a running list of processed files.
|
|
29
|
+
- Writes the standard output of every command to a CSV file per function.
|
|
30
|
+
- Logs each executed command and its standard error output to a separate file.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.10 or later
|
|
35
|
+
- [Textual](https://textual.textualize.io/) 8.2.8 or later (installed automatically)
|
|
36
|
+
- [uv](https://docs.astral.sh/uv/) (recommended for development)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install runall
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv add runall
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
1. Create a configuration dictionary (see [Configuration](#configuration)).
|
|
51
|
+
2. Decorate one or more functions with `@runthat(config=config)`. Each function
|
|
52
|
+
receives the path of an input file and returns the shell command to execute.
|
|
53
|
+
3. Start the interface with `RunAllApp(config).run()`.
|
|
54
|
+
|
|
55
|
+
For every input file, `runall` executes each registered function in the order in
|
|
56
|
+
which it was defined. The commands run sequentially.
|
|
57
|
+
|
|
58
|
+
### Example
|
|
59
|
+
|
|
60
|
+
The complete example below is available in `examples/demo.py`. It registers three
|
|
61
|
+
functions that each wait five seconds and then print one line:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# from runall.config import CONFIG
|
|
65
|
+
from runall.tui import RunAllApp, runthat
|
|
66
|
+
|
|
67
|
+
config = {
|
|
68
|
+
"runthat": {},
|
|
69
|
+
"input_dir": "instances/ht0_*.txt",
|
|
70
|
+
"output_dir": "output",
|
|
71
|
+
"output_preffix": "out_",
|
|
72
|
+
"output_header": "execucao,arquivo",
|
|
73
|
+
"err_dir": "err",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@runthat(config=config)
|
|
77
|
+
def exec1(filename: str) -> str:
|
|
78
|
+
return f'sleep 5;echo "[1] {filename}"'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@runthat(config=config)
|
|
82
|
+
def exec2(filename: str) -> str:
|
|
83
|
+
return f'sleep 5;echo "[2] {filename}"'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@runthat(config=config)
|
|
87
|
+
def exec3(filename: str) -> str:
|
|
88
|
+
return f'sleep 5;echo "[3] {filename}"'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> None:
|
|
92
|
+
RunAllApp(config).run()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Run it from the repository root, so that the relative paths in the configuration
|
|
100
|
+
resolve correctly:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
uv run python examples/demo.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
The configuration is a plain dictionary with the following keys:
|
|
109
|
+
|
|
110
|
+
| Key | Description |
|
|
111
|
+
| ---------------- | -------------------------------------------------------------------------------------------- |
|
|
112
|
+
| `runthat` | Registry of decorated functions. Start it as an empty dictionary; `@runthat` fills it in. |
|
|
113
|
+
| `input_dir` | Glob pattern that selects the input files, for example `instances/ht0_*.txt`. |
|
|
114
|
+
| `output_dir` | Directory that receives the standard output of each function. |
|
|
115
|
+
| `output_preffix` | Prefix of every output file name. The key keeps its original spelling. |
|
|
116
|
+
| `output_header` | Header line written once at the top of each newly created output file. |
|
|
117
|
+
| `err_dir` | Directory that receives the executed commands and their standard error output. |
|
|
118
|
+
|
|
119
|
+
A default configuration is also available as `runall.CONFIG`.
|
|
120
|
+
|
|
121
|
+
## Output files
|
|
122
|
+
|
|
123
|
+
For a function named `exec1` and the configuration above, `runall` writes:
|
|
124
|
+
|
|
125
|
+
- `output/out_exec1.csv`: the header line followed by the standard output of every
|
|
126
|
+
execution of `exec1`.
|
|
127
|
+
- `err/out_exec1.csv`: for every execution, the line `$ <command>` followed by
|
|
128
|
+
the standard error output of the command, if any.
|
|
129
|
+
|
|
130
|
+
Both files are opened in append mode, so results accumulate across runs. Remove
|
|
131
|
+
the output directories before a run to start from a clean state. Commands should
|
|
132
|
+
print lines that match the columns of `output_header`.
|
|
133
|
+
|
|
134
|
+
## Security considerations
|
|
135
|
+
|
|
136
|
+
Commands are executed through the shell (`shell=True`), and the file name is
|
|
137
|
+
inserted into the command string that your function returns. Quote file names
|
|
138
|
+
(for example with `shlex.quote`) if they may contain spaces or shell
|
|
139
|
+
metacharacters, and never register commands built from untrusted input.
|
|
140
|
+
|
|
141
|
+
The exit status of a command is not treated as an error: a failing command is
|
|
142
|
+
recorded in the error log and processing continues with the next command.
|
|
143
|
+
|
|
144
|
+
## Project layout
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
src/runall/
|
|
148
|
+
├── __init__.py Public API: CONFIG, discover_input_files, runthat
|
|
149
|
+
├── config.py Default configuration
|
|
150
|
+
├── processor.py Input file discovery
|
|
151
|
+
└── tui.py The @runthat decorator and the RunAllApp interface
|
|
152
|
+
examples/
|
|
153
|
+
└── demo.py Complete usage example
|
|
154
|
+
instances/ Sample input files used by the example
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
|
|
160
|
+
for details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
runall/__init__.py,sha256=8jbxNNSIvsT0-T-hlXxhAGYlviiqnLLR5SdxHdchqiw,885
|
|
2
|
+
runall/config.py,sha256=mi0h46cIIDtf7TSnmtmOlqe3PGPCG-GU2xIHxUSd3-A,1024
|
|
3
|
+
runall/processor.py,sha256=CwC4DWn_Fl5g1Gk_DhwkW-v1OP28C0fM_JxBB6No0Ck,665
|
|
4
|
+
runall/tui.py,sha256=GjtBhPKwghBbePDRr1it6HCtE8ZHwOKcgpTgPTL3CcA,9242
|
|
5
|
+
runall-0.1.0.dist-info/METADATA,sha256=fJ7iQzIk0YpkiCGeihk-YLWcY1ku25aoftkPfrnqS5Q,5615
|
|
6
|
+
runall-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
runall-0.1.0.dist-info/licenses/LICENSE,sha256=Ty-96cRm7x4TchIZBW3ES6B_1OC8YX-32EfqozKmL5Q,1087
|
|
8
|
+
runall-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roberto Fernandes Tavares Neto
|
|
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.
|