pyallel 0.12.2__tar.gz → 0.14.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-0.12.2 → pyallel-0.14.0}/PKG-INFO +2 -3
- {pyallel-0.12.2 → pyallel-0.14.0}/README.md +1 -2
- {pyallel-0.12.2 → pyallel-0.14.0}/pyproject.toml +1 -1
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/main.py +2 -8
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/parser.py +0 -9
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/process.py +114 -133
- {pyallel-0.12.2 → pyallel-0.14.0}/LICENSE +0 -0
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/__init__.py +0 -0
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/constants.py +0 -0
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/errors.py +0 -0
- {pyallel-0.12.2 → pyallel-0.14.0}/src/pyallel/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -40,7 +40,7 @@ pip install pyallel
|
|
|
40
40
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
41
41
|
|
|
42
42
|
```
|
|
43
|
-
usage: pyallel [-h] [-t] [-n] [-
|
|
43
|
+
usage: pyallel [-h] [-t] [-n] [-V] [-v] [commands ...]
|
|
44
44
|
|
|
45
45
|
Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
46
46
|
|
|
@@ -56,7 +56,6 @@ options:
|
|
|
56
56
|
-t, --no-timer don't time how long each command is taking
|
|
57
57
|
-n, --non-interactive
|
|
58
58
|
run in non-interactive mode
|
|
59
|
-
-s, --no-stream don't stream output of each command
|
|
60
59
|
-V, --verbose run in verbose mode
|
|
61
60
|
-v, --version print version and exit
|
|
62
61
|
```
|
|
@@ -19,7 +19,7 @@ pip install pyallel
|
|
|
19
19
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
usage: pyallel [-h] [-t] [-n] [-
|
|
22
|
+
usage: pyallel [-h] [-t] [-n] [-V] [-v] [commands ...]
|
|
23
23
|
|
|
24
24
|
Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
25
25
|
|
|
@@ -35,7 +35,6 @@ options:
|
|
|
35
35
|
-t, --no-timer don't time how long each command is taking
|
|
36
36
|
-n, --non-interactive
|
|
37
37
|
run in non-interactive mode
|
|
38
|
-
-s, --no-stream don't stream output of each command
|
|
39
38
|
-V, --verbose run in verbose mode
|
|
40
39
|
-v, --version print version and exit
|
|
41
40
|
```
|
|
@@ -15,13 +15,10 @@ def main_loop(
|
|
|
15
15
|
interactive: bool = False,
|
|
16
16
|
timer: bool = False,
|
|
17
17
|
verbose: bool = False,
|
|
18
|
-
stream: bool = False,
|
|
19
18
|
) -> int:
|
|
20
19
|
process_group = ProcessGroup.from_commands(
|
|
21
20
|
*commands, interactive=interactive, timer=timer, verbose=verbose
|
|
22
21
|
)
|
|
23
|
-
if not stream:
|
|
24
|
-
return process_group.run()
|
|
25
22
|
|
|
26
23
|
return process_group.stream()
|
|
27
24
|
|
|
@@ -46,7 +43,6 @@ def run(*args: str) -> int:
|
|
|
46
43
|
interactive=parsed_args.interactive,
|
|
47
44
|
timer=parsed_args.timer,
|
|
48
45
|
verbose=parsed_args.verbose,
|
|
49
|
-
stream=parsed_args.stream,
|
|
50
46
|
)
|
|
51
47
|
except InvalidExecutableErrors as e:
|
|
52
48
|
exit_code = 1
|
|
@@ -55,14 +51,12 @@ def run(*args: str) -> int:
|
|
|
55
51
|
exit_code = 1
|
|
56
52
|
message = traceback.format_exc()
|
|
57
53
|
|
|
58
|
-
if exit_code ==
|
|
59
|
-
print(f"{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}")
|
|
60
|
-
elif exit_code == 1:
|
|
54
|
+
if exit_code == 1:
|
|
61
55
|
if not message:
|
|
62
56
|
print(f"{constants.RED_BOLD}A command failed!{constants.RESET_COLOUR}")
|
|
63
57
|
else:
|
|
64
58
|
print(f"{constants.RED_BOLD}Error: {message}{constants.RESET_COLOUR}")
|
|
65
|
-
|
|
59
|
+
elif exit_code == 0:
|
|
66
60
|
print(f"{constants.GREEN_BOLD}Success!{constants.RESET_COLOUR}")
|
|
67
61
|
|
|
68
62
|
return exit_code
|
|
@@ -9,7 +9,6 @@ class Arguments:
|
|
|
9
9
|
timer: bool
|
|
10
10
|
verbose: bool
|
|
11
11
|
version: bool
|
|
12
|
-
stream: bool
|
|
13
12
|
|
|
14
13
|
def __repr__(self) -> str:
|
|
15
14
|
msg = "Arguments:\n"
|
|
@@ -54,14 +53,6 @@ def create_parser() -> ArgumentParser:
|
|
|
54
53
|
dest="interactive",
|
|
55
54
|
default=True,
|
|
56
55
|
)
|
|
57
|
-
parser.add_argument(
|
|
58
|
-
"-s",
|
|
59
|
-
"--no-stream",
|
|
60
|
-
help="don't stream output of each command",
|
|
61
|
-
action="store_false",
|
|
62
|
-
default=True,
|
|
63
|
-
dest="stream",
|
|
64
|
-
)
|
|
65
56
|
parser.add_argument(
|
|
66
57
|
"-V",
|
|
67
58
|
"--verbose",
|
|
@@ -9,7 +9,7 @@ import shlex
|
|
|
9
9
|
import shutil
|
|
10
10
|
import os
|
|
11
11
|
from uuid import UUID, uuid4
|
|
12
|
-
from typing import BinaryIO
|
|
12
|
+
from typing import Any, BinaryIO
|
|
13
13
|
from pyallel import constants
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass, field
|
|
@@ -74,26 +74,6 @@ def get_command_status(
|
|
|
74
74
|
return output
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def print_command_output(process: Process) -> None:
|
|
78
|
-
output = process.read()
|
|
79
|
-
if output:
|
|
80
|
-
print(prefix(output.decode(), keepend=False))
|
|
81
|
-
print()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def run_process(process: Process, timer: bool = False, verbose: bool = False) -> bool:
|
|
85
|
-
print(f"{constants.CLEAR_LINE}{constants.CARRIAGE_RETURN}", end="")
|
|
86
|
-
|
|
87
|
-
if process.return_code() != 0:
|
|
88
|
-
print(get_command_status(process, passed=False, verbose=verbose, timer=timer))
|
|
89
|
-
print_command_output(process)
|
|
90
|
-
return False
|
|
91
|
-
else:
|
|
92
|
-
print(get_command_status(process, passed=True, verbose=verbose, timer=timer))
|
|
93
|
-
print_command_output(process)
|
|
94
|
-
return True
|
|
95
|
-
|
|
96
|
-
|
|
97
77
|
@dataclass
|
|
98
78
|
class ProcessGroup:
|
|
99
79
|
processes: list[Process]
|
|
@@ -103,43 +83,11 @@ class ProcessGroup:
|
|
|
103
83
|
output: dict[UUID, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
104
84
|
process_lines: list[int] = field(default_factory=list)
|
|
105
85
|
completed_processes: set[UUID] = field(default_factory=set)
|
|
86
|
+
exit_code: int = 0
|
|
87
|
+
interrupt_count: int = 0
|
|
106
88
|
passed: bool = True
|
|
107
89
|
icon: int = 0
|
|
108
90
|
|
|
109
|
-
def run(self) -> int:
|
|
110
|
-
for process in self.processes:
|
|
111
|
-
process.run()
|
|
112
|
-
|
|
113
|
-
if not self.interactive or not constants.IN_TTY:
|
|
114
|
-
print(
|
|
115
|
-
f"{constants.WHITE_BOLD}Running commands...{constants.RESET_COLOUR}\n"
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
while True:
|
|
119
|
-
if self.interactive and constants.IN_TTY:
|
|
120
|
-
for icon in constants.ICONS:
|
|
121
|
-
print(
|
|
122
|
-
f"{constants.CLEAR_LINE}{constants.CARRIAGE_RETURN}{constants.WHITE_BOLD}Running commands{constants.RESET_COLOUR} {icon}",
|
|
123
|
-
end="",
|
|
124
|
-
)
|
|
125
|
-
time.sleep(0.1)
|
|
126
|
-
|
|
127
|
-
for process in self.processes:
|
|
128
|
-
if process.id in self.completed_processes or process.poll() is None:
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
self.completed_processes.add(process.id)
|
|
132
|
-
process_passed = run_process(
|
|
133
|
-
process, verbose=self.verbose, timer=self.timer
|
|
134
|
-
)
|
|
135
|
-
if not process_passed:
|
|
136
|
-
self.passed = False
|
|
137
|
-
|
|
138
|
-
if len(self.completed_processes) == len(self.processes):
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
return 1 if not self.passed else 0
|
|
142
|
-
|
|
143
91
|
def stream(self) -> int:
|
|
144
92
|
for process in self.processes:
|
|
145
93
|
process.run()
|
|
@@ -147,36 +95,31 @@ class ProcessGroup:
|
|
|
147
95
|
if not self.interactive:
|
|
148
96
|
return self.stream_non_interactive()
|
|
149
97
|
|
|
150
|
-
interrupted = False
|
|
151
|
-
|
|
152
98
|
while True:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
self.icon = 0
|
|
99
|
+
output = self.complete_output()
|
|
100
|
+
self.icon += 1
|
|
101
|
+
if self.icon == len(constants.ICONS):
|
|
102
|
+
self.icon = 0
|
|
158
103
|
|
|
159
|
-
|
|
160
|
-
|
|
104
|
+
# Clear the screen and print the output
|
|
105
|
+
print(f"\033[H\033[0J{output}", end="")
|
|
161
106
|
|
|
162
|
-
|
|
163
|
-
|
|
107
|
+
# Clear the screen again
|
|
108
|
+
print("\033[H\033[0J", end="")
|
|
164
109
|
|
|
165
|
-
|
|
166
|
-
|
|
110
|
+
if len(self.completed_processes) == len(self.processes):
|
|
111
|
+
break
|
|
167
112
|
|
|
168
|
-
|
|
169
|
-
except KeyboardInterrupt:
|
|
170
|
-
interrupted = True
|
|
171
|
-
for process in self.processes:
|
|
172
|
-
process.interrupt()
|
|
173
|
-
process.wait()
|
|
113
|
+
time.sleep(0.1)
|
|
174
114
|
|
|
175
115
|
output = self.complete_output(all=True)
|
|
176
116
|
# Clear the screen one final time before printing the output
|
|
177
117
|
print(f"\033[3J{output}")
|
|
178
118
|
|
|
179
|
-
|
|
119
|
+
if not self.exit_code and not self.passed:
|
|
120
|
+
self.exit_code = 1
|
|
121
|
+
|
|
122
|
+
return self.exit_code
|
|
180
123
|
|
|
181
124
|
def stream_non_interactive(self) -> int:
|
|
182
125
|
running_process = None
|
|
@@ -185,67 +128,88 @@ class ProcessGroup:
|
|
|
185
128
|
print(f"{constants.WHITE_BOLD}Running commands...{constants.RESET_COLOUR}\n")
|
|
186
129
|
|
|
187
130
|
while True:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
continue
|
|
131
|
+
output = ""
|
|
132
|
+
for process in self.processes:
|
|
133
|
+
if (
|
|
134
|
+
running_process is None
|
|
135
|
+
and process.id not in self.completed_processes
|
|
136
|
+
):
|
|
137
|
+
output += get_command_status(process, verbose=self.verbose)
|
|
138
|
+
output += "\n"
|
|
139
|
+
running_process = process
|
|
140
|
+
elif running_process is not process:
|
|
141
|
+
# Need to do this to properly keep track of how long all the other
|
|
142
|
+
# commands are taking
|
|
143
|
+
process.poll()
|
|
144
|
+
continue
|
|
203
145
|
|
|
204
|
-
|
|
146
|
+
process_output = process.readline().decode()
|
|
205
147
|
|
|
206
|
-
|
|
148
|
+
if not self.output[process.id] and process_output:
|
|
149
|
+
process_output = prefix(process_output)
|
|
150
|
+
self.output[process.id].append(process_output)
|
|
151
|
+
output += process_output
|
|
152
|
+
elif process_output:
|
|
153
|
+
if self.output[process.id][-1][-1] != "\n":
|
|
154
|
+
self.output[process.id][-1] += process_output
|
|
155
|
+
else:
|
|
207
156
|
process_output = prefix(process_output)
|
|
208
157
|
self.output[process.id].append(process_output)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
output += process_output
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
158
|
+
output += process_output
|
|
159
|
+
|
|
160
|
+
if process.poll() is not None:
|
|
161
|
+
if process.return_code() != 0:
|
|
162
|
+
self.passed = False
|
|
163
|
+
process_output = process.read().decode()
|
|
164
|
+
if process_output:
|
|
165
|
+
output += prefix(process_output)
|
|
166
|
+
|
|
167
|
+
output += get_command_status(
|
|
168
|
+
process,
|
|
169
|
+
passed=process.return_code() == 0,
|
|
170
|
+
verbose=self.verbose,
|
|
171
|
+
timer=self.timer,
|
|
172
|
+
)
|
|
173
|
+
output += "\n\n"
|
|
174
|
+
self.completed_processes.add(process.id)
|
|
175
|
+
running_process = None
|
|
176
|
+
|
|
177
|
+
if self.interrupt_count == 0:
|
|
178
|
+
pass
|
|
179
|
+
elif not interrupted and self.interrupt_count == 1:
|
|
180
|
+
if (output and output[-1] != "\n") or (
|
|
181
|
+
self.output[process.id]
|
|
182
|
+
and self.output[process.id][-1][-1] != "\n"
|
|
183
|
+
):
|
|
184
|
+
output += "\n"
|
|
185
|
+
output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}\n\n"
|
|
186
|
+
interrupted = True
|
|
187
|
+
|
|
188
|
+
if output:
|
|
189
|
+
print(output, end="")
|
|
190
|
+
|
|
191
|
+
if len(self.completed_processes) == len(self.processes):
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
time.sleep(0.01)
|
|
195
|
+
|
|
196
|
+
if self.interrupt_count == 2:
|
|
197
|
+
print(f"{constants.RED_BOLD}Abort!{constants.RESET_COLOUR}")
|
|
198
|
+
|
|
199
|
+
if not self.exit_code and not self.passed:
|
|
200
|
+
self.exit_code = 1
|
|
201
|
+
|
|
202
|
+
return self.exit_code
|
|
203
|
+
|
|
204
|
+
def _handle_signal(self, signum: int, _frame: Any) -> None:
|
|
205
|
+
for process in self.processes:
|
|
206
|
+
if self.interrupt_count == 0:
|
|
207
|
+
process.interrupt()
|
|
208
|
+
else:
|
|
209
|
+
process.kill()
|
|
210
|
+
|
|
211
|
+
self.exit_code = 128 + signum
|
|
212
|
+
self.interrupt_count += 1
|
|
249
213
|
|
|
250
214
|
@classmethod
|
|
251
215
|
def from_commands(
|
|
@@ -267,13 +231,18 @@ class ProcessGroup:
|
|
|
267
231
|
if errors:
|
|
268
232
|
raise InvalidExecutableErrors(*errors)
|
|
269
233
|
|
|
270
|
-
|
|
234
|
+
process_group = cls(
|
|
271
235
|
processes=processes,
|
|
272
236
|
interactive=interactive,
|
|
273
237
|
timer=timer,
|
|
274
238
|
verbose=verbose,
|
|
275
239
|
)
|
|
276
240
|
|
|
241
|
+
signal.signal(signal.SIGINT, process_group._handle_signal)
|
|
242
|
+
signal.signal(signal.SIGTERM, process_group._handle_signal)
|
|
243
|
+
|
|
244
|
+
return process_group
|
|
245
|
+
|
|
277
246
|
def complete_output(self, tail: int = 20, all: bool = False) -> str:
|
|
278
247
|
num_processes = len(self.processes)
|
|
279
248
|
lines = constants.LINES() - (2 * num_processes)
|
|
@@ -330,6 +299,14 @@ class ProcessGroup:
|
|
|
330
299
|
if i != num_processes:
|
|
331
300
|
output += "\n"
|
|
332
301
|
|
|
302
|
+
if self.interrupt_count == 0:
|
|
303
|
+
return output
|
|
304
|
+
|
|
305
|
+
if self.interrupt_count == 1:
|
|
306
|
+
output += f"\n{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}"
|
|
307
|
+
elif self.interrupt_count == 2:
|
|
308
|
+
output += f"\n{constants.RED_BOLD}Abort!{constants.RESET_COLOUR}"
|
|
309
|
+
|
|
333
310
|
return output
|
|
334
311
|
|
|
335
312
|
|
|
@@ -395,6 +372,10 @@ class Process:
|
|
|
395
372
|
if self._process:
|
|
396
373
|
self._process.send_signal(signal.SIGINT)
|
|
397
374
|
|
|
375
|
+
def kill(self) -> None:
|
|
376
|
+
if self._process:
|
|
377
|
+
self._process.send_signal(signal.SIGKILL)
|
|
378
|
+
|
|
398
379
|
def wait(self) -> int:
|
|
399
380
|
if self._process:
|
|
400
381
|
return self._process.wait()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|