pyallel 1.2.8__tar.gz → 1.3.1__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.
- {pyallel-1.2.8 → pyallel-1.3.1}/PKG-INFO +16 -3
- {pyallel-1.2.8 → pyallel-1.3.1}/README.md +15 -2
- {pyallel-1.2.8 → pyallel-1.3.1}/pyproject.toml +1 -1
- pyallel-1.3.1/src/pyallel/errors.py +2 -0
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/main.py +3 -3
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/parser.py +14 -1
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/printer.py +56 -13
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/process.py +36 -2
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/process_group.py +12 -9
- pyallel-1.2.8/src/pyallel/errors.py +0 -11
- {pyallel-1.2.8 → pyallel-1.3.1}/LICENSE +0 -0
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/__init__.py +0 -0
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/colours.py +0 -0
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/constants.py +0 -0
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/process_group_manager.py +0 -0
- {pyallel-1.2.8 → pyallel-1.3.1}/src/pyallel/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
5
5
|
Home-page: https://github.com/Danthewaann/pyallel
|
|
6
6
|
License: MIT
|
|
@@ -62,15 +62,28 @@ positional arguments:
|
|
|
62
62
|
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
63
63
|
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
64
64
|
|
|
65
|
+
PROCESS GROUPS
|
|
66
|
+
--------------
|
|
65
67
|
commands can be grouped using the group separator symbol (:::)
|
|
66
68
|
|
|
67
|
-
"echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
69
|
+
%(prog)s "echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
68
70
|
|
|
69
71
|
the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
|
|
70
72
|
|
|
71
73
|
command groups are ran in the sequence you provide them, and if a command group fails
|
|
72
74
|
(if a command fails inside the command group) the rest of the command groups in the sequence are not run
|
|
73
75
|
|
|
76
|
+
COMMAND MODIFIERS
|
|
77
|
+
-----------------
|
|
78
|
+
modifiers can be set for commands to augment their behaviour using the command modifier symbol (::)
|
|
79
|
+
|
|
80
|
+
lines (only used in interactive mode):
|
|
81
|
+
the lines modifier allows you to specify how many lines the command output can take up on the screen
|
|
82
|
+
|
|
83
|
+
%(prog)s "lines=90 :: echo running long command..." "echo running other command..."
|
|
84
|
+
|
|
85
|
+
90 is expressed as a percentage value, which must be between 1 and 100 inclusive
|
|
86
|
+
|
|
74
87
|
options:
|
|
75
88
|
-h, --help show this help message and exit
|
|
76
89
|
-t, --no-timer don't time how long each command is taking
|
|
@@ -142,8 +155,8 @@ python -m venv .venv && \
|
|
|
142
155
|
|
|
143
156
|
- [x] Add support to have commands depend on other commands (some commands must complete
|
|
144
157
|
before a given command can start)
|
|
158
|
+
- [x] Add support to state how many lines a command can use for it's output in interactive mode
|
|
145
159
|
- [ ] Add a debug mode that logs debug information to a log file
|
|
146
|
-
- [ ] Add support to state how many lines a command can use for it's output in interactive mode
|
|
147
160
|
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
148
161
|
(such as a REPL)
|
|
149
162
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -38,15 +38,28 @@ positional arguments:
|
|
|
38
38
|
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
39
39
|
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
40
40
|
|
|
41
|
+
PROCESS GROUPS
|
|
42
|
+
--------------
|
|
41
43
|
commands can be grouped using the group separator symbol (:::)
|
|
42
44
|
|
|
43
|
-
"echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
45
|
+
%(prog)s "echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
44
46
|
|
|
45
47
|
the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
|
|
46
48
|
|
|
47
49
|
command groups are ran in the sequence you provide them, and if a command group fails
|
|
48
50
|
(if a command fails inside the command group) the rest of the command groups in the sequence are not run
|
|
49
51
|
|
|
52
|
+
COMMAND MODIFIERS
|
|
53
|
+
-----------------
|
|
54
|
+
modifiers can be set for commands to augment their behaviour using the command modifier symbol (::)
|
|
55
|
+
|
|
56
|
+
lines (only used in interactive mode):
|
|
57
|
+
the lines modifier allows you to specify how many lines the command output can take up on the screen
|
|
58
|
+
|
|
59
|
+
%(prog)s "lines=90 :: echo running long command..." "echo running other command..."
|
|
60
|
+
|
|
61
|
+
90 is expressed as a percentage value, which must be between 1 and 100 inclusive
|
|
62
|
+
|
|
50
63
|
options:
|
|
51
64
|
-h, --help show this help message and exit
|
|
52
65
|
-t, --no-timer don't time how long each command is taking
|
|
@@ -118,8 +131,8 @@ python -m venv .venv && \
|
|
|
118
131
|
|
|
119
132
|
- [x] Add support to have commands depend on other commands (some commands must complete
|
|
120
133
|
before a given command can start)
|
|
134
|
+
- [x] Add support to state how many lines a command can use for it's output in interactive mode
|
|
121
135
|
- [ ] Add a debug mode that logs debug information to a log file
|
|
122
|
-
- [ ] Add support to state how many lines a command can use for it's output in interactive mode
|
|
123
136
|
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
124
137
|
(such as a REPL)
|
|
125
138
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
|
|
8
8
|
from pyallel import constants
|
|
9
9
|
from pyallel.colours import Colours
|
|
10
|
-
from pyallel.errors import
|
|
10
|
+
from pyallel.errors import InvalidLinesModifierError
|
|
11
11
|
from pyallel.parser import Arguments, create_parser
|
|
12
12
|
from pyallel.printer import Printer
|
|
13
13
|
from pyallel.process_group_manager import ProcessGroupManager
|
|
@@ -35,7 +35,7 @@ def run_interactive(
|
|
|
35
35
|
if poll > 0:
|
|
36
36
|
return poll
|
|
37
37
|
|
|
38
|
-
printer.
|
|
38
|
+
printer.reset()
|
|
39
39
|
process_group_manager.run()
|
|
40
40
|
if not process_group_manager.next():
|
|
41
41
|
return 0
|
|
@@ -111,7 +111,7 @@ def run(*args: str) -> int:
|
|
|
111
111
|
exit_code = run_interactive(process_group_manager, printer)
|
|
112
112
|
else:
|
|
113
113
|
exit_code = run_non_interactive(process_group_manager, printer)
|
|
114
|
-
except
|
|
114
|
+
except InvalidLinesModifierError as e:
|
|
115
115
|
exit_code = 1
|
|
116
116
|
message = str(e)
|
|
117
117
|
except Exception:
|
|
@@ -31,14 +31,27 @@ if you were running the command directly in a shell, some examples are below
|
|
|
31
31
|
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
32
32
|
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
33
33
|
|
|
34
|
+
PROCESS GROUPS
|
|
35
|
+
--------------
|
|
34
36
|
commands can be grouped using the group separator symbol (:::)
|
|
35
37
|
|
|
36
|
-
"echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
38
|
+
%(prog)s "echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
37
39
|
|
|
38
40
|
the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
|
|
39
41
|
|
|
40
42
|
command groups are ran in the sequence you provide them, and if a command group fails
|
|
41
43
|
(if a command fails inside the command group) the rest of the command groups in the sequence are not run
|
|
44
|
+
|
|
45
|
+
COMMAND MODIFIERS
|
|
46
|
+
-----------------
|
|
47
|
+
modifiers can be set for commands to augment their behaviour using the command modifier symbol (::)
|
|
48
|
+
|
|
49
|
+
lines (only used in interactive mode):
|
|
50
|
+
the lines modifier allows you to specify how many lines the command output can take up on the screen
|
|
51
|
+
|
|
52
|
+
%(prog)s "lines=90 :: echo running long command..." "echo running other command..."
|
|
53
|
+
|
|
54
|
+
90 is expressed as a percentage value, which must be between 1 and 100 inclusive
|
|
42
55
|
"""
|
|
43
56
|
|
|
44
57
|
|
|
@@ -21,6 +21,7 @@ class Printer:
|
|
|
21
21
|
line: str,
|
|
22
22
|
include_prefix: bool = False,
|
|
23
23
|
end: str = "\n",
|
|
24
|
+
flush: bool = False,
|
|
24
25
|
truncate: bool = False,
|
|
25
26
|
) -> None:
|
|
26
27
|
truncate_num = 0
|
|
@@ -31,7 +32,7 @@ class Printer:
|
|
|
31
32
|
columns = constants.COLUMNS() - truncate_num
|
|
32
33
|
if get_num_lines(line, columns) > 1:
|
|
33
34
|
line = truncate_line(line, columns)
|
|
34
|
-
print(f"{prefix}{line}", end=end, flush=
|
|
35
|
+
print(f"{prefix}{line}", end=end, flush=flush)
|
|
35
36
|
|
|
36
37
|
def info(self, msg: str) -> None:
|
|
37
38
|
self.write(
|
|
@@ -70,6 +71,9 @@ class Printer:
|
|
|
70
71
|
out: list[tuple[bool, str, str]] = []
|
|
71
72
|
line_parts: tuple[bool, str, str]
|
|
72
73
|
|
|
74
|
+
if tail_output and output.process.lines == 0:
|
|
75
|
+
return out
|
|
76
|
+
|
|
73
77
|
if include_cmd:
|
|
74
78
|
status = self.generate_process_output_status(
|
|
75
79
|
output, include_progress, include_timer
|
|
@@ -82,8 +86,11 @@ class Printer:
|
|
|
82
86
|
lines = output.data.splitlines(keepends=True)
|
|
83
87
|
|
|
84
88
|
if tail_output:
|
|
85
|
-
output_lines = output.lines - 1
|
|
86
|
-
|
|
89
|
+
output_lines = output.process.lines - 1
|
|
90
|
+
if output_lines == 0:
|
|
91
|
+
lines = []
|
|
92
|
+
else:
|
|
93
|
+
lines = lines[-output_lines:]
|
|
87
94
|
|
|
88
95
|
for line in lines:
|
|
89
96
|
prefix = True
|
|
@@ -213,6 +220,9 @@ class Printer:
|
|
|
213
220
|
):
|
|
214
221
|
self.write(line, include_prefix, end)
|
|
215
222
|
|
|
223
|
+
# Force a flush otherwise lines that don't end in a newline character will not get printed as they are read
|
|
224
|
+
self.write("", include_prefix=False, end="", flush=True)
|
|
225
|
+
|
|
216
226
|
def print_progress_group_output(
|
|
217
227
|
self,
|
|
218
228
|
output: ProcessGroupOutput,
|
|
@@ -233,29 +243,62 @@ class Printer:
|
|
|
233
243
|
end="",
|
|
234
244
|
)
|
|
235
245
|
|
|
236
|
-
self.
|
|
246
|
+
self.reset()
|
|
237
247
|
|
|
238
|
-
def
|
|
248
|
+
def reset(self) -> None:
|
|
239
249
|
self._printed.clear()
|
|
240
250
|
|
|
241
251
|
|
|
242
252
|
def set_process_lines(
|
|
243
253
|
output: ProcessGroupOutput,
|
|
244
254
|
interrupt_count: int = 0,
|
|
245
|
-
lines: int
|
|
255
|
+
lines: int = 0,
|
|
246
256
|
) -> None:
|
|
247
257
|
lines = lines or constants.LINES() - 1
|
|
248
258
|
if interrupt_count:
|
|
249
259
|
lines -= 2
|
|
250
260
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
# Allocate lines to processes that have a fixed percentage of lines set
|
|
262
|
+
used_lines = 0
|
|
263
|
+
process_with_most_lines: ProcessOutput | None = None
|
|
264
|
+
other_processes: list[ProcessOutput] = []
|
|
255
265
|
for out in output.processes:
|
|
256
|
-
out.
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
if not out.process.percentage_lines:
|
|
267
|
+
other_processes.append(out)
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
out.process.lines = int(lines * out.process.percentage_lines)
|
|
271
|
+
used_lines += out.process.lines
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
process_with_most_lines is None
|
|
275
|
+
or process_with_most_lines.process.lines < out.process.lines
|
|
276
|
+
):
|
|
277
|
+
process_with_most_lines = out
|
|
278
|
+
|
|
279
|
+
lines -= used_lines
|
|
280
|
+
|
|
281
|
+
# Allocate the rest of the available lines to the other processes that don't have fixed lines set
|
|
282
|
+
num_dynamic_processes = len(other_processes)
|
|
283
|
+
if num_dynamic_processes:
|
|
284
|
+
remainder = lines % num_dynamic_processes
|
|
285
|
+
tail = lines // num_dynamic_processes
|
|
286
|
+
|
|
287
|
+
for out in other_processes:
|
|
288
|
+
out.process.lines = tail
|
|
289
|
+
|
|
290
|
+
# If we have lines left to allocate, we give all of them to the process with the highest
|
|
291
|
+
# allocated lines or to the last process
|
|
292
|
+
if remainder:
|
|
293
|
+
if process_with_most_lines:
|
|
294
|
+
process_with_most_lines.process.lines += remainder
|
|
295
|
+
else:
|
|
296
|
+
other_processes[-1].process.lines += remainder
|
|
297
|
+
else:
|
|
298
|
+
# Otherwise allocate remaining lines to the process with the highest allocated lines
|
|
299
|
+
remainder = lines
|
|
300
|
+
if remainder and process_with_most_lines:
|
|
301
|
+
process_with_most_lines.process.lines += remainder
|
|
259
302
|
|
|
260
303
|
|
|
261
304
|
def get_num_lines(line: str, columns: int | None = None) -> int:
|
|
@@ -6,24 +6,27 @@ import tempfile
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import BinaryIO
|
|
8
8
|
|
|
9
|
+
from pyallel.errors import InvalidLinesModifierError
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class ProcessOutput:
|
|
11
13
|
def __init__(self, id: int, process: Process, data: str = "") -> None:
|
|
12
14
|
self.id = id
|
|
13
15
|
self.data = data
|
|
14
16
|
self.process = process
|
|
15
|
-
self.lines = -1
|
|
16
17
|
|
|
17
18
|
def merge(self, other: ProcessOutput) -> None:
|
|
18
19
|
self.data += other.data
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Process:
|
|
22
|
-
def __init__(self, id: int, command: str) -> None:
|
|
23
|
+
def __init__(self, id: int, command: str, percentage_lines: float = 0.0) -> None:
|
|
23
24
|
self.id = id
|
|
24
25
|
self.command = command
|
|
25
26
|
self.start = 0.0
|
|
26
27
|
self.end = 0.0
|
|
28
|
+
self.lines = 0
|
|
29
|
+
self.percentage_lines = percentage_lines
|
|
27
30
|
self._fd: BinaryIO
|
|
28
31
|
self._process: subprocess.Popen[bytes]
|
|
29
32
|
|
|
@@ -71,3 +74,34 @@ class Process:
|
|
|
71
74
|
def wait(self) -> int:
|
|
72
75
|
return self._process.wait()
|
|
73
76
|
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_command(cls, id: int, command: str) -> Process:
|
|
79
|
+
cmd = command.split(" :: ", maxsplit=1)
|
|
80
|
+
if len(cmd) == 1:
|
|
81
|
+
return cls(id, cmd[0])
|
|
82
|
+
|
|
83
|
+
args, *parts = cmd
|
|
84
|
+
|
|
85
|
+
percentage_lines = 0
|
|
86
|
+
for arg in args.split(" "):
|
|
87
|
+
try:
|
|
88
|
+
arg, value = args.split("=")
|
|
89
|
+
except ValueError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if arg == "lines":
|
|
93
|
+
try:
|
|
94
|
+
percentage_lines = int(value)
|
|
95
|
+
except ValueError:
|
|
96
|
+
raise InvalidLinesModifierError(
|
|
97
|
+
"lines modifier must be a number between 1 and 100"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not 0 < percentage_lines <= 100:
|
|
101
|
+
raise InvalidLinesModifierError(
|
|
102
|
+
"lines modifier must be a number between 1 and 100"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
return cls(id, " ".join(parts), round(percentage_lines / 100, 2))
|
|
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Sequence
|
|
4
4
|
|
|
5
|
-
from pyallel.errors import
|
|
5
|
+
from pyallel.errors import (
|
|
6
|
+
InvalidLinesModifierError,
|
|
7
|
+
)
|
|
6
8
|
from pyallel.process import Process, ProcessOutput
|
|
7
9
|
|
|
8
10
|
|
|
@@ -63,16 +65,17 @@ class ProcessGroup:
|
|
|
63
65
|
@classmethod
|
|
64
66
|
def from_commands(cls, id: int, process_id: int, *commands: str) -> ProcessGroup:
|
|
65
67
|
processes: list[Process] = []
|
|
66
|
-
errors: list[InvalidExecutableError] = []
|
|
67
68
|
|
|
69
|
+
percentage_lines_sum = 0.0
|
|
68
70
|
for i, command in enumerate(commands):
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
process = Process.from_command(i + process_id, command)
|
|
72
|
+
percentage_lines_sum += process.percentage_lines
|
|
73
|
+
processes.append(process)
|
|
74
|
+
|
|
75
|
+
if round(percentage_lines_sum, 2) > 1.0:
|
|
76
|
+
raise InvalidLinesModifierError(
|
|
77
|
+
"lines modifier must not exceed 100 across all processes within each process group"
|
|
78
|
+
)
|
|
76
79
|
|
|
77
80
|
process_group = cls(id=id, processes=processes)
|
|
78
81
|
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
class InvalidExecutableError(Exception):
|
|
2
|
-
def __init__(self, exe: str) -> None:
|
|
3
|
-
super().__init__(exe)
|
|
4
|
-
self.exe = exe
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class InvalidExecutableErrors(Exception):
|
|
8
|
-
def __init__(self, *errors: InvalidExecutableError) -> None:
|
|
9
|
-
super().__init__(
|
|
10
|
-
f"executables [{', '.join(error.exe for error in errors)}] were not found"
|
|
11
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|