runem 0.5.0__py3-none-any.whl → 0.7.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.
- runem/VERSION +1 -1
- runem/blocking_print.py +10 -2
- runem/cli/initialise_options.py +0 -1
- runem/command_line.py +4 -7
- runem/config.py +8 -4
- runem/config_parse.py +2 -1
- runem/config_validate.py +47 -0
- runem/informative_dict.py +7 -2
- runem/job.py +1 -3
- runem/job_execute.py +12 -9
- runem/job_filter.py +5 -5
- runem/job_wrapper_python.py +3 -4
- runem/log.py +27 -5
- runem/report.py +8 -4
- runem/run_command.py +62 -28
- runem/runem.py +46 -64
- runem/schema.yml +137 -0
- runem/types/__init__.py +2 -1
- runem/types/errors.py +10 -0
- runem/types/hooks.py +1 -1
- runem/types/types_jobs.py +21 -24
- runem/utils.py +12 -0
- runem/yaml_utils.py +19 -0
- runem/yaml_validation.py +28 -0
- runem-0.7.0.dist-info/METADATA +162 -0
- runem-0.7.0.dist-info/RECORD +56 -0
- {runem-0.5.0.dist-info → runem-0.7.0.dist-info}/WHEEL +1 -1
- scripts/test_hooks/py.py +63 -1
- runem-0.5.0.dist-info/METADATA +0 -164
- runem-0.5.0.dist-info/RECORD +0 -52
- {runem-0.5.0.dist-info → runem-0.7.0.dist-info}/entry_points.txt +0 -0
- {runem-0.5.0.dist-info → runem-0.7.0.dist-info/licenses}/LICENSE +0 -0
- {runem-0.5.0.dist-info → runem-0.7.0.dist-info}/top_level.txt +0 -0
runem/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.7.0
|
runem/blocking_print.py
CHANGED
@@ -13,7 +13,14 @@ def _reset_console() -> Console:
|
|
13
13
|
|
14
14
|
RICH_CONSOLE = Console(
|
15
15
|
log_path=False, # Do NOT print the source path.
|
16
|
-
markup
|
16
|
+
# We allow markup here, BUT stdout/stderr from other procs should have
|
17
|
+
# `escape()` called on them so they don't error here.
|
18
|
+
# This means 'rich' effects/colors can be judiciously applied:
|
19
|
+
# e.g. `[blink]Don't Panic![/blink]`.
|
20
|
+
markup=True,
|
21
|
+
# `highlight` is what colourises string and number in print() calls.
|
22
|
+
# We do not want this to be auto-magic.
|
23
|
+
highlight=False,
|
17
24
|
)
|
18
25
|
return RICH_CONSOLE
|
19
26
|
|
@@ -31,7 +38,8 @@ def _reset_console_for_tests() -> None:
|
|
31
38
|
RICH_CONSOLE = Console(
|
32
39
|
log_path=False, # Do NOT print the source path.
|
33
40
|
log_time=False, # Do not prefix with log time e.g. `[time] log message`.
|
34
|
-
markup=
|
41
|
+
markup=True, # Allow some markup e.g. `[blink]Don't Panic![/blink]`.
|
42
|
+
highlight=False,
|
35
43
|
width=999, # A very wide width.
|
36
44
|
)
|
37
45
|
|
runem/cli/initialise_options.py
CHANGED
runem/command_line.py
CHANGED
@@ -43,10 +43,8 @@ def _get_argparse_help_formatter() -> typing.Any:
|
|
43
43
|
|
44
44
|
if use_fixed_width:
|
45
45
|
# Use custom formatter with the width specified in the environment variable
|
46
|
-
return (
|
47
|
-
|
48
|
-
prog
|
49
|
-
)
|
46
|
+
return lambda prog: HelpFormatterFixedWidth( # pylint: disable=unnecessary-lambda
|
47
|
+
prog
|
50
48
|
)
|
51
49
|
|
52
50
|
# Use default formatter
|
@@ -294,12 +292,12 @@ def parse_args(
|
|
294
292
|
error_on_log_logic(args.verbose, args.silent)
|
295
293
|
|
296
294
|
if args.show_root_path_and_exit:
|
297
|
-
log(str(config_metadata.cfg_filepath.parent),
|
295
|
+
log(str(config_metadata.cfg_filepath.parent), prefix=False)
|
298
296
|
# cleanly exit
|
299
297
|
sys.exit(0)
|
300
298
|
|
301
299
|
if args.show_version_and_exit:
|
302
|
-
log(str(get_runem_version()),
|
300
|
+
log(str(get_runem_version()), prefix=False)
|
303
301
|
# cleanly exit
|
304
302
|
sys.exit(0)
|
305
303
|
|
@@ -383,7 +381,6 @@ def initialise_options(
|
|
383
381
|
|
384
382
|
Returns the options dictionary
|
385
383
|
"""
|
386
|
-
|
387
384
|
options: OptionsWritable = InformativeDict(
|
388
385
|
{option["name"]: option["default"] for option in config_metadata.options_config}
|
389
386
|
)
|
runem/config.py
CHANGED
@@ -2,9 +2,9 @@ import pathlib
|
|
2
2
|
import sys
|
3
3
|
import typing
|
4
4
|
|
5
|
-
import yaml
|
6
5
|
from packaging.version import Version
|
7
6
|
|
7
|
+
from runem.config_validate import validate_runem_file
|
8
8
|
from runem.log import error, log
|
9
9
|
from runem.runem_version import get_runem_version
|
10
10
|
from runem.types.runem_config import (
|
@@ -13,6 +13,7 @@ from runem.types.runem_config import (
|
|
13
13
|
GlobalSerialisedConfig,
|
14
14
|
UserConfigMetadata,
|
15
15
|
)
|
16
|
+
from runem.yaml_utils import load_yaml_object
|
16
17
|
|
17
18
|
CFG_FILE_YAML = pathlib.Path(".runem.yml")
|
18
19
|
|
@@ -46,7 +47,7 @@ def _search_up_multiple_dirs_for_file(
|
|
46
47
|
|
47
48
|
|
48
49
|
def _find_config_file(
|
49
|
-
config_filename: typing.Union[str, pathlib.Path]
|
50
|
+
config_filename: typing.Union[str, pathlib.Path],
|
50
51
|
) -> typing.Tuple[typing.Optional[pathlib.Path], typing.Tuple[pathlib.Path, ...]]:
|
51
52
|
"""Searches up from the cwd for the given config file-name."""
|
52
53
|
start_dirs = (pathlib.Path(".").absolute(),)
|
@@ -117,8 +118,11 @@ def _conform_global_config_types(
|
|
117
118
|
|
118
119
|
def load_and_parse_config(cfg_filepath: pathlib.Path) -> Config:
|
119
120
|
"""For the given config file pass, project or user, load it & parse/conform it."""
|
120
|
-
|
121
|
-
|
121
|
+
all_config = load_yaml_object(cfg_filepath)
|
122
|
+
validate_runem_file(
|
123
|
+
cfg_filepath,
|
124
|
+
all_config,
|
125
|
+
)
|
122
126
|
|
123
127
|
conformed_config: Config
|
124
128
|
global_config: typing.Optional[GlobalConfig]
|
runem/config_parse.py
CHANGED
@@ -162,7 +162,8 @@ def parse_job_config(
|
|
162
162
|
("cwd" in job["ctx"]) and (job["ctx"]["cwd"] is not None)
|
163
163
|
)
|
164
164
|
if (not have_ctw_cwd) or isinstance(
|
165
|
-
job["ctx"]["cwd"],
|
165
|
+
job["ctx"]["cwd"], # type: ignore # handled above
|
166
|
+
str,
|
166
167
|
):
|
167
168
|
# if
|
168
169
|
# - we don't have a cwd, ctx
|
runem/config_validate.py
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
import pathlib
|
2
|
+
import typing
|
3
|
+
|
4
|
+
from runem.log import error, log
|
5
|
+
from runem.types.errors import SystemExitBad
|
6
|
+
from runem.yaml_utils import load_yaml_object
|
7
|
+
from runem.yaml_validation import ValidationErrors, validate_yaml
|
8
|
+
|
9
|
+
|
10
|
+
def _load_runem_schema() -> typing.Any:
|
11
|
+
"""Loads and returns the yaml schema for runem.
|
12
|
+
|
13
|
+
Returns:
|
14
|
+
Any: the Draft202012Validator conformant schema.
|
15
|
+
"""
|
16
|
+
schema_path: pathlib.Path = pathlib.Path(__file__).with_name("schema.yml")
|
17
|
+
if not schema_path.exists():
|
18
|
+
error(
|
19
|
+
(
|
20
|
+
"runem schema file not found, cannot continue! "
|
21
|
+
f"Is the install corrupt? {schema_path}"
|
22
|
+
)
|
23
|
+
)
|
24
|
+
raise SystemExitBad(1)
|
25
|
+
schema: typing.Any = load_yaml_object(schema_path)
|
26
|
+
return schema
|
27
|
+
|
28
|
+
|
29
|
+
def validate_runem_file(
|
30
|
+
cfg_filepath: pathlib.Path,
|
31
|
+
all_config: typing.Any,
|
32
|
+
) -> None:
|
33
|
+
"""Validates the config Loader object against the runem schema.
|
34
|
+
|
35
|
+
Exits if the files does not validate.
|
36
|
+
"""
|
37
|
+
schema: typing.Any = _load_runem_schema()
|
38
|
+
errors: ValidationErrors = validate_yaml(all_config, schema)
|
39
|
+
if not errors:
|
40
|
+
# aok
|
41
|
+
return
|
42
|
+
|
43
|
+
error(f"failed to validate runem config [yellow]{cfg_filepath}[/yellow]")
|
44
|
+
for err in errors:
|
45
|
+
path = ".".join(map(str, err.path)) or "<root>"
|
46
|
+
log(f" [yellow]{path}[/yellow]: {err.message}")
|
47
|
+
raise SystemExit("Config validation failed.")
|
runem/informative_dict.py
CHANGED
@@ -9,8 +9,7 @@ class InformativeDict(typing.Dict[K, V], typing.Generic[K, V]):
|
|
9
9
|
"""A dictionary type that prints out the available keys."""
|
10
10
|
|
11
11
|
def __getitem__(self, key: K) -> V:
|
12
|
-
"""Attempt to
|
13
|
-
found."""
|
12
|
+
"""Attempt to get item, raising a detailed exception if the key is not found."""
|
14
13
|
try:
|
15
14
|
return super().__getitem__(key)
|
16
15
|
except KeyError:
|
@@ -24,19 +23,25 @@ class ReadOnlyInformativeDict(InformativeDict[K, V], typing.Generic[K, V]):
|
|
24
23
|
"""A read-only variant of the above."""
|
25
24
|
|
26
25
|
def __setitem__(self, key: K, value: V) -> None:
|
26
|
+
"""Readonly object, setitem disallowed."""
|
27
27
|
raise NotImplementedError("This dictionary is read-only")
|
28
28
|
|
29
29
|
def __delitem__(self, key: K) -> None:
|
30
|
+
"""Readonly object, delitem disallowed."""
|
30
31
|
raise NotImplementedError("This dictionary is read-only")
|
31
32
|
|
32
33
|
def pop(self, *args: typing.Any, **kwargs: typing.Any) -> V:
|
34
|
+
"""Readonly object, pop disallowed."""
|
33
35
|
raise NotImplementedError("This dictionary is read-only")
|
34
36
|
|
35
37
|
def popitem(self) -> typing.Tuple[K, V]:
|
38
|
+
"""Readonly object, popitem disallowed."""
|
36
39
|
raise NotImplementedError("This dictionary is read-only")
|
37
40
|
|
38
41
|
def clear(self) -> None:
|
42
|
+
"""Readonly object, clear disallowed."""
|
39
43
|
raise NotImplementedError("This dictionary is read-only")
|
40
44
|
|
41
45
|
def update(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
46
|
+
"""Readonly object, update disallowed."""
|
42
47
|
raise NotImplementedError("This dictionary is read-only")
|
runem/job.py
CHANGED
@@ -71,7 +71,6 @@ class Job:
|
|
71
71
|
|
72
72
|
TODO: make a non-static member function
|
73
73
|
"""
|
74
|
-
|
75
74
|
# default to all file-tags
|
76
75
|
tags_for_files: typing.Iterable[str] = file_lists.keys()
|
77
76
|
use_default_tags: bool = job_tags is None
|
@@ -91,7 +90,6 @@ class Job:
|
|
91
90
|
|
92
91
|
TODO: make a non-static member function
|
93
92
|
"""
|
94
|
-
|
95
93
|
# First try one of the following keys.
|
96
94
|
valid_name_keys = ("label", "command")
|
97
95
|
for candidate in valid_name_keys:
|
@@ -101,6 +99,6 @@ class Job:
|
|
101
99
|
|
102
100
|
# The try the python-wrapper address
|
103
101
|
try:
|
104
|
-
return f
|
102
|
+
return f"{job['addr']['file']}.{job['addr']['function']}"
|
105
103
|
except KeyError:
|
106
104
|
raise NoJobName() # pylint: disable=raise-missing-from
|
runem/job_execute.py
CHANGED
@@ -105,7 +105,7 @@ def job_execute_inner(
|
|
105
105
|
reports = function(**all_k_args)
|
106
106
|
except BaseException: # pylint: disable=broad-exception-caught
|
107
107
|
# log that we hit an error on this job and re-raise
|
108
|
-
log(
|
108
|
+
log(prefix=False)
|
109
109
|
error(f"job: job '{Job.get_job_name(job_config)}' failed to complete!")
|
110
110
|
# re-raise
|
111
111
|
raise
|
@@ -132,12 +132,15 @@ def job_execute(
|
|
132
132
|
"""
|
133
133
|
this_id: str = str(uuid.uuid4())
|
134
134
|
running_jobs[this_id] = Job.get_job_name(job_config)
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
135
|
+
try:
|
136
|
+
results = job_execute_inner(
|
137
|
+
job_config,
|
138
|
+
config_metadata,
|
139
|
+
file_lists,
|
140
|
+
**kwargs,
|
141
|
+
)
|
142
|
+
finally:
|
143
|
+
# Always tidy-up job statuses
|
144
|
+
completed_jobs[this_id] = running_jobs[this_id]
|
145
|
+
del running_jobs[this_id]
|
143
146
|
return results
|
runem/job_filter.py
CHANGED
@@ -101,21 +101,21 @@ def filter_jobs( # noqa: C901
|
|
101
101
|
if tags_to_run:
|
102
102
|
log(
|
103
103
|
f"filtering for tags {printable_set(tags_to_run)}",
|
104
|
-
|
104
|
+
prefix=True,
|
105
105
|
end="",
|
106
106
|
)
|
107
107
|
if tags_to_avoid:
|
108
108
|
if tags_to_run:
|
109
|
-
log(", ",
|
109
|
+
log(", ", prefix=False, end="")
|
110
110
|
else:
|
111
|
-
log(
|
111
|
+
log(prefix=True, end="")
|
112
112
|
log(
|
113
113
|
f"excluding jobs with tags {printable_set(tags_to_avoid)}",
|
114
|
-
|
114
|
+
prefix=False,
|
115
115
|
end="",
|
116
116
|
)
|
117
117
|
if tags_to_run or tags_to_avoid:
|
118
|
-
log(
|
118
|
+
log(prefix=False)
|
119
119
|
filtered_jobs: PhaseGroupedJobs = defaultdict(list)
|
120
120
|
for phase in config_metadata.phases:
|
121
121
|
if phase not in phases_to_run:
|
runem/job_wrapper_python.py
CHANGED
@@ -15,7 +15,6 @@ def _load_python_function_from_module(
|
|
15
15
|
function_to_load: str,
|
16
16
|
) -> JobFunction:
|
17
17
|
"""Given a job-description dynamically loads the test-function so we can call it."""
|
18
|
-
|
19
18
|
# first locate the module relative to the config file
|
20
19
|
abs_module_file_path: pathlib.Path = (
|
21
20
|
cfg_filepath.parent / module_file_path
|
@@ -109,9 +108,9 @@ def get_job_wrapper_py_func(
|
|
109
108
|
) from err
|
110
109
|
|
111
110
|
anchored_file_path = cfg_filepath.parent / module_file_path
|
112
|
-
assert (
|
113
|
-
anchored_file_path
|
114
|
-
)
|
111
|
+
assert anchored_file_path.exists(), (
|
112
|
+
f"{module_file_path} not found at {anchored_file_path}!"
|
113
|
+
)
|
115
114
|
|
116
115
|
module_name = module_file_path.stem.replace(" ", "_").replace("-", "_")
|
117
116
|
|
runem/log.py
CHANGED
@@ -3,13 +3,35 @@ import typing
|
|
3
3
|
from runem.blocking_print import blocking_print
|
4
4
|
|
5
5
|
|
6
|
-
def log(
|
6
|
+
def log(
|
7
|
+
msg: str = "",
|
8
|
+
prefix: typing.Optional[bool] = None,
|
9
|
+
end: typing.Optional[str] = None,
|
10
|
+
) -> None:
|
7
11
|
"""Thin wrapper around 'print', change the 'msg' & handles system-errors.
|
8
12
|
|
9
13
|
One way we change it is to decorate the output with 'runem'
|
14
|
+
|
15
|
+
Parameters:
|
16
|
+
msg: str - the message to log out. Any `rich` markup will be escaped
|
17
|
+
and not applied.
|
18
|
+
decorate: str - whether to add runem-specific prefix text. We do this
|
19
|
+
to identify text that comes from the app vs text that
|
20
|
+
comes from hooks or other third-parties.
|
21
|
+
end: Optional[str] - same as the end option used by `print()` and
|
22
|
+
`rich`
|
23
|
+
Returns:
|
24
|
+
Nothing
|
10
25
|
"""
|
11
|
-
if
|
12
|
-
|
26
|
+
# Remove any markup as it will probably error, if unsanitised.
|
27
|
+
# msg = escape(msg)
|
28
|
+
|
29
|
+
if prefix is None:
|
30
|
+
prefix = True
|
31
|
+
|
32
|
+
if prefix:
|
33
|
+
# Make it clear that the message comes from `runem` internals.
|
34
|
+
msg = f"[light_slate_grey]runem[/light_slate_grey]: {msg}"
|
13
35
|
|
14
36
|
# print in a blocking manner, waiting for system resources to free up if a
|
15
37
|
# runem job is contending on stdout or similar.
|
@@ -17,8 +39,8 @@ def log(msg: str = "", decorate: bool = True, end: typing.Optional[str] = None)
|
|
17
39
|
|
18
40
|
|
19
41
|
def warn(msg: str) -> None:
|
20
|
-
log(f"WARNING: {msg}")
|
42
|
+
log(f"[yellow]WARNING[/yellow]: {msg}")
|
21
43
|
|
22
44
|
|
23
45
|
def error(msg: str) -> None:
|
24
|
-
log(f"ERROR: {msg}")
|
46
|
+
log(f"[red]ERROR[/red]: {msg}")
|
runem/report.py
CHANGED
@@ -46,12 +46,13 @@ def replace_bar_graph_characters(text: str, end_str: str, replace_char: str) ->
|
|
46
46
|
"""Replaces block characters in lines containing `end_str` with give char.
|
47
47
|
|
48
48
|
Args:
|
49
|
-
|
49
|
+
text (str): Text containing lines of bar-graphs (perhaps)
|
50
|
+
end_str (str): If contained by a line, the bar-graph shapes are replaced.
|
50
51
|
replace_char (str): The character to replace all bocks with
|
51
52
|
|
52
53
|
Returns:
|
53
|
-
|
54
|
-
|
54
|
+
str: The modified `text` with block characters replaced on specific
|
55
|
+
lines.
|
55
56
|
"""
|
56
57
|
# Define the block character and its light shade replacement
|
57
58
|
block_chars = (
|
@@ -211,7 +212,10 @@ def _print_reports_by_phase(
|
|
211
212
|
for job_report_url_info in report_urls:
|
212
213
|
if not job_report_url_info:
|
213
214
|
continue
|
214
|
-
log(
|
215
|
+
log(
|
216
|
+
f"report: [blue]{str(job_report_url_info[0])}[/blue]: "
|
217
|
+
f"{str(job_report_url_info[1])}"
|
218
|
+
)
|
215
219
|
|
216
220
|
|
217
221
|
def report_on_run(
|
runem/run_command.py
CHANGED
@@ -7,17 +7,33 @@ from subprocess import STDOUT as SUBPROCESS_STDOUT
|
|
7
7
|
from subprocess import Popen
|
8
8
|
from timeit import default_timer as timer
|
9
9
|
|
10
|
+
from rich.markup import escape
|
11
|
+
|
10
12
|
from runem.log import log
|
11
13
|
|
12
14
|
TERMINAL_WIDTH = 86
|
13
15
|
|
14
16
|
|
15
|
-
class
|
16
|
-
|
17
|
+
class RunemJobError(RuntimeError):
|
18
|
+
"""An exception type that stores the stdout/stderr.
|
19
|
+
|
20
|
+
Designed so that we do not print the full stdout via the exception stack, instead,
|
21
|
+
allows an opportunity to parse the markup in it.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, friendly_message: str, stdout: str):
|
25
|
+
self.stdout = stdout
|
26
|
+
super().__init__(friendly_message)
|
17
27
|
|
18
28
|
|
19
|
-
class
|
20
|
-
|
29
|
+
class RunCommandBadExitCode(RunemJobError):
|
30
|
+
def __init__(self, stdout: str):
|
31
|
+
super().__init__(friendly_message="Bad exit-code", stdout=stdout)
|
32
|
+
|
33
|
+
|
34
|
+
class RunCommandUnhandledError(RunemJobError):
|
35
|
+
def __init__(self, stdout: str):
|
36
|
+
super().__init__(friendly_message="Unhandled job error", stdout=stdout)
|
21
37
|
|
22
38
|
|
23
39
|
# A function type for recording timing information.
|
@@ -25,27 +41,25 @@ RecordSubJobTimeType = typing.Callable[[str, timedelta], None]
|
|
25
41
|
|
26
42
|
|
27
43
|
def parse_stdout(stdout: str, prefix: str) -> str:
|
28
|
-
"""Prefixes each line of
|
29
|
-
lines."""
|
44
|
+
"""Prefixes each line of output with a given label, except trailing new lines."""
|
30
45
|
# Edge case: Return the prefix immediately for an empty string
|
31
46
|
if not stdout:
|
32
47
|
return prefix
|
33
48
|
|
34
|
-
#
|
35
|
-
|
49
|
+
# Stop errors in `rich` by parsing out anything that might look like
|
50
|
+
# rich-markup.
|
51
|
+
stdout = escape(stdout)
|
52
|
+
|
53
|
+
# Split stdout into lines
|
36
54
|
lines = stdout.split("\n")
|
37
55
|
|
38
56
|
# Apply prefix to all lines except the last if it's empty (due to a trailing newline)
|
39
|
-
modified_lines = [f"{prefix}{line}" for line in lines[:-1]] + (
|
40
|
-
[lines[-1]]
|
41
|
-
if lines[-1] == "" and ends_with_newline
|
42
|
-
else [f"{prefix}{lines[-1]}"]
|
57
|
+
modified_lines = [f"{prefix}{escape(line)}" for line in lines[:-1]] + (
|
58
|
+
[f"{prefix}{escape(lines[-1])}"]
|
43
59
|
)
|
44
60
|
|
45
61
|
# Join the lines back together, appropriately handling the final newline
|
46
|
-
modified_stdout = "\n".join(modified_lines)
|
47
|
-
# if ends_with_newline:
|
48
|
-
# modified_stdout += "\n"
|
62
|
+
modified_stdout: str = "\n".join(modified_lines)
|
49
63
|
|
50
64
|
return modified_stdout
|
51
65
|
|
@@ -69,24 +83,34 @@ def _log_command_execution(
|
|
69
83
|
label: str,
|
70
84
|
env_overrides: typing.Optional[typing.Dict[str, str]],
|
71
85
|
valid_exit_ids: typing.Optional[typing.Tuple[int, ...]],
|
86
|
+
decorate_logs: bool,
|
72
87
|
verbose: bool,
|
73
88
|
cwd: typing.Optional[pathlib.Path] = None,
|
74
89
|
) -> None:
|
75
90
|
"""Logs out useful debug information on '--verbose'."""
|
76
91
|
if verbose:
|
77
|
-
log(
|
92
|
+
log(
|
93
|
+
f"running: start: [blue]{label}[/blue]: [yellow]{cmd_string}[yellow]",
|
94
|
+
prefix=decorate_logs,
|
95
|
+
)
|
78
96
|
if valid_exit_ids is not None:
|
79
97
|
valid_exit_strs = ",".join(str(exit_code) for exit_code in valid_exit_ids)
|
80
|
-
log(
|
98
|
+
log(
|
99
|
+
f"\tallowed return ids are: [green]{valid_exit_strs}[/green]",
|
100
|
+
prefix=decorate_logs,
|
101
|
+
)
|
81
102
|
|
82
103
|
if env_overrides:
|
83
104
|
env_overrides_as_string = " ".join(
|
84
105
|
[f"{key}='{value}'" for key, value in env_overrides.items()]
|
85
106
|
)
|
86
|
-
log(
|
107
|
+
log(
|
108
|
+
f"ENV OVERRIDES: [yellow]{env_overrides_as_string} {cmd_string}[/yellow]",
|
109
|
+
prefix=decorate_logs,
|
110
|
+
)
|
87
111
|
|
88
112
|
if cwd:
|
89
|
-
log(f"cwd: {str(cwd)}")
|
113
|
+
log(f"cwd: {str(cwd)}", prefix=decorate_logs)
|
90
114
|
|
91
115
|
|
92
116
|
def run_command( # noqa: C901
|
@@ -98,6 +122,7 @@ def run_command( # noqa: C901
|
|
98
122
|
valid_exit_ids: typing.Optional[typing.Tuple[int, ...]] = None,
|
99
123
|
cwd: typing.Optional[pathlib.Path] = None,
|
100
124
|
record_sub_job_time: typing.Optional[RecordSubJobTimeType] = None,
|
125
|
+
decorate_logs: bool = True,
|
101
126
|
**kwargs: typing.Any,
|
102
127
|
) -> str:
|
103
128
|
"""Runs the given command, returning stdout or throwing on any error."""
|
@@ -115,6 +140,7 @@ def run_command( # noqa: C901
|
|
115
140
|
label,
|
116
141
|
env_overrides,
|
117
142
|
valid_exit_ids,
|
143
|
+
decorate_logs,
|
118
144
|
verbose,
|
119
145
|
cwd,
|
120
146
|
)
|
@@ -143,7 +169,12 @@ def run_command( # noqa: C901
|
|
143
169
|
stdout += line
|
144
170
|
if verbose:
|
145
171
|
# print each line of output, assuming that each has a newline
|
146
|
-
log(
|
172
|
+
log(
|
173
|
+
parse_stdout(
|
174
|
+
line, prefix=f"[green]| [/green][blue]{label}[/blue]: "
|
175
|
+
),
|
176
|
+
prefix=False,
|
177
|
+
)
|
147
178
|
|
148
179
|
# Wait for the subprocess to finish and get the exit code
|
149
180
|
process.wait()
|
@@ -154,15 +185,15 @@ def run_command( # noqa: C901
|
|
154
185
|
)
|
155
186
|
raise RunCommandBadExitCode(
|
156
187
|
(
|
157
|
-
f"non-zero exit {process.returncode} (allowed are "
|
158
|
-
f"{valid_exit_strs}) from {cmd_string}"
|
188
|
+
f"non-zero exit [red]{process.returncode}[/red] (allowed are "
|
189
|
+
f"[green]{valid_exit_strs}[/green]) from {cmd_string}"
|
159
190
|
)
|
160
191
|
)
|
161
192
|
except BaseException as err:
|
162
193
|
if ignore_fails:
|
163
194
|
return ""
|
164
195
|
parsed_stdout: str = (
|
165
|
-
parse_stdout(stdout, prefix=
|
196
|
+
parse_stdout(stdout, prefix="[red]| [/red]") if process else ""
|
166
197
|
)
|
167
198
|
env_overrides_as_string = ""
|
168
199
|
if env_overrides:
|
@@ -171,11 +202,11 @@ def run_command( # noqa: C901
|
|
171
202
|
)
|
172
203
|
env_overrides_as_string = f"{env_overrides_as_string} "
|
173
204
|
error_string = (
|
174
|
-
f"runem:
|
175
|
-
f"\n\t{env_overrides_as_string}{cmd_string}"
|
176
|
-
f"\
|
205
|
+
f"runem: [red bold]FATAL[/red bold]: command failed: [blue]{label}[/blue]"
|
206
|
+
f"\n\t[yellow]{env_overrides_as_string}{cmd_string}[/yellow]"
|
207
|
+
f"\n[red underline]| ERROR[/red underline]: [blue]{label}[/blue]"
|
177
208
|
f"\n{str(parsed_stdout)}"
|
178
|
-
f"\
|
209
|
+
f"\n[red underline]| ERROR END[/red underline]"
|
179
210
|
)
|
180
211
|
|
181
212
|
if isinstance(err, RunCommandBadExitCode):
|
@@ -184,7 +215,10 @@ def run_command( # noqa: C901
|
|
184
215
|
raise RunCommandUnhandledError(error_string) from err
|
185
216
|
|
186
217
|
if verbose:
|
187
|
-
log(
|
218
|
+
log(
|
219
|
+
f"running: done: [blue]{label}[/blue]: [yellow]{cmd_string}[/yellow]",
|
220
|
+
prefix=decorate_logs,
|
221
|
+
)
|
188
222
|
|
189
223
|
if record_sub_job_time is not None:
|
190
224
|
# Capture how long this run took
|