pyallel 1.2.7__tar.gz → 1.3.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.
- {pyallel-1.2.7 → pyallel-1.3.0}/PKG-INFO +16 -3
- {pyallel-1.2.7 → pyallel-1.3.0}/README.md +15 -2
- {pyallel-1.2.7 → pyallel-1.3.0}/pyproject.toml +1 -1
- pyallel-1.3.0/src/pyallel/errors.py +2 -0
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/main.py +2 -2
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/parser.py +14 -1
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/printer.py +56 -16
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/process.py +36 -2
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/process_group.py +12 -9
- pyallel-1.2.7/src/pyallel/errors.py +0 -11
- {pyallel-1.2.7 → pyallel-1.3.0}/LICENSE +0 -0
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/__init__.py +0 -0
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/colours.py +0 -0
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/constants.py +0 -0
- {pyallel-1.2.7 → pyallel-1.3.0}/src/pyallel/process_group_manager.py +0 -0
- {pyallel-1.2.7 → pyallel-1.3.0}/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.0
|
|
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
|
|
@@ -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,14 +21,18 @@ 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:
|
|
27
|
+
truncate_num = 0
|
|
26
28
|
prefix = self._prefix if include_prefix else ""
|
|
29
|
+
if prefix:
|
|
30
|
+
truncate_num = 6
|
|
27
31
|
if truncate:
|
|
28
|
-
columns = constants.COLUMNS() -
|
|
32
|
+
columns = constants.COLUMNS() - truncate_num
|
|
29
33
|
if get_num_lines(line, columns) > 1:
|
|
30
34
|
line = truncate_line(line, columns)
|
|
31
|
-
print(f"{prefix}{line}", end=end, flush=
|
|
35
|
+
print(f"{prefix}{line}", end=end, flush=flush)
|
|
32
36
|
|
|
33
37
|
def info(self, msg: str) -> None:
|
|
34
38
|
self.write(
|
|
@@ -67,6 +71,9 @@ class Printer:
|
|
|
67
71
|
out: list[tuple[bool, str, str]] = []
|
|
68
72
|
line_parts: tuple[bool, str, str]
|
|
69
73
|
|
|
74
|
+
if tail_output and output.process.lines == 0:
|
|
75
|
+
return out
|
|
76
|
+
|
|
70
77
|
if include_cmd:
|
|
71
78
|
status = self.generate_process_output_status(
|
|
72
79
|
output, include_progress, include_timer
|
|
@@ -79,9 +86,11 @@ class Printer:
|
|
|
79
86
|
lines = output.data.splitlines(keepends=True)
|
|
80
87
|
|
|
81
88
|
if tail_output:
|
|
82
|
-
|
|
83
|
-
output_lines
|
|
84
|
-
|
|
89
|
+
output_lines = output.process.lines - 1
|
|
90
|
+
if output_lines == 0:
|
|
91
|
+
lines = []
|
|
92
|
+
else:
|
|
93
|
+
lines = lines[-output_lines:]
|
|
85
94
|
|
|
86
95
|
for line in lines:
|
|
87
96
|
prefix = True
|
|
@@ -136,14 +145,23 @@ class Printer:
|
|
|
136
145
|
if not icon:
|
|
137
146
|
msg += "..."
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
timer = ""
|
|
141
149
|
if include_timer:
|
|
142
150
|
end = output.process.end
|
|
143
151
|
if not output.process.end:
|
|
144
152
|
end = time.perf_counter()
|
|
145
153
|
elapsed = end - output.process.start
|
|
146
|
-
|
|
154
|
+
timer = f"({format_time_taken(elapsed)})"
|
|
155
|
+
|
|
156
|
+
command = output.process.command
|
|
157
|
+
if get_num_lines(output.process.command) > 1:
|
|
158
|
+
columns = constants.COLUMNS() - (len(msg) + len(timer) + 9)
|
|
159
|
+
command = truncate_line(command, columns)
|
|
160
|
+
|
|
161
|
+
out = f"{self._colours.white_bold}[{self._colours.reset_colour}{self._colours.blue_bold}{command}{self._colours.reset_colour}{self._colours.white_bold}]{self._colours.reset_colour}{colour} {msg} {icon}{self._colours.reset_colour}"
|
|
162
|
+
|
|
163
|
+
if timer:
|
|
164
|
+
out += f" {self._colours.dim_on}{timer}{self._colours.dim_off}"
|
|
147
165
|
|
|
148
166
|
return out
|
|
149
167
|
|
|
@@ -202,6 +220,9 @@ class Printer:
|
|
|
202
220
|
):
|
|
203
221
|
self.write(line, include_prefix, end)
|
|
204
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
|
+
|
|
205
226
|
def print_progress_group_output(
|
|
206
227
|
self,
|
|
207
228
|
output: ProcessGroupOutput,
|
|
@@ -231,20 +252,39 @@ class Printer:
|
|
|
231
252
|
def set_process_lines(
|
|
232
253
|
output: ProcessGroupOutput,
|
|
233
254
|
interrupt_count: int = 0,
|
|
234
|
-
lines: int
|
|
255
|
+
lines: int = 0,
|
|
235
256
|
) -> None:
|
|
236
257
|
lines = lines or constants.LINES() - 1
|
|
237
258
|
if interrupt_count:
|
|
238
259
|
lines -= 2
|
|
239
260
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
# Allocate lines to processes that have a fixed percentage of lines set
|
|
262
|
+
used_lines = 0
|
|
263
|
+
other_processes: list[ProcessOutput] = []
|
|
244
264
|
for out in output.processes:
|
|
245
|
-
out.
|
|
246
|
-
|
|
247
|
-
|
|
265
|
+
if not out.process.percentage_lines:
|
|
266
|
+
other_processes.append(out)
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
out.process.lines = int(lines * out.process.percentage_lines)
|
|
270
|
+
used_lines += out.process.lines
|
|
271
|
+
|
|
272
|
+
lines -= used_lines
|
|
273
|
+
|
|
274
|
+
# Allocate the rest of the available lines to the other processes that don't have fixed lines set
|
|
275
|
+
num_dynamic_processes = len(other_processes)
|
|
276
|
+
if num_dynamic_processes:
|
|
277
|
+
remainder = lines % num_dynamic_processes
|
|
278
|
+
tail = lines // num_dynamic_processes
|
|
279
|
+
|
|
280
|
+
for out in other_processes:
|
|
281
|
+
out.process.lines = tail
|
|
282
|
+
|
|
283
|
+
# If we have lines left to allocate, we give all of them to the last process
|
|
284
|
+
if remainder:
|
|
285
|
+
for out in other_processes[-1::-1]:
|
|
286
|
+
out.process.lines += remainder
|
|
287
|
+
break
|
|
248
288
|
|
|
249
289
|
|
|
250
290
|
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.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) / 100
|
|
95
|
+
except ValueError:
|
|
96
|
+
raise InvalidLinesModifierError(
|
|
97
|
+
"lines modifier must be a number between 1 and 100"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not 0.0 < percentage_lines <= 1.0:
|
|
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), percentage_lines)
|
|
@@ -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 percentage_lines_sum > 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
|