runem 0.0.29__py3-none-any.whl → 0.0.31__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/cli.py +1 -0
- runem/command_line.py +33 -8
- runem/config.py +58 -9
- runem/config_metadata.py +8 -0
- runem/config_parse.py +188 -13
- runem/files.py +50 -7
- runem/hook_manager.py +116 -0
- runem/job_execute.py +22 -21
- runem/job_filter.py +2 -2
- runem/job_runner_simple_command.py +7 -1
- runem/job_wrapper.py +11 -5
- runem/job_wrapper_python.py +7 -7
- runem/log.py +8 -0
- runem/report.py +18 -14
- runem/runem.py +30 -13
- runem/types.py +47 -4
- {runem-0.0.29.dist-info → runem-0.0.31.dist-info}/METADATA +18 -28
- runem-0.0.31.dist-info/RECORD +33 -0
- runem-0.0.29.dist-info/RECORD +0 -32
- {runem-0.0.29.dist-info → runem-0.0.31.dist-info}/LICENSE +0 -0
- {runem-0.0.29.dist-info → runem-0.0.31.dist-info}/WHEEL +0 -0
- {runem-0.0.29.dist-info → runem-0.0.31.dist-info}/entry_points.txt +0 -0
- {runem-0.0.29.dist-info → runem-0.0.31.dist-info}/top_level.txt +0 -0
runem/hook_manager.py
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
import typing
|
2
|
+
from collections import defaultdict
|
3
|
+
|
4
|
+
from runem.config_metadata import ConfigMetadata
|
5
|
+
from runem.job import Job
|
6
|
+
from runem.job_execute import job_execute
|
7
|
+
from runem.log import log
|
8
|
+
from runem.types import (
|
9
|
+
FilePathListLookup,
|
10
|
+
HookConfig,
|
11
|
+
HookName,
|
12
|
+
Hooks,
|
13
|
+
HooksStore,
|
14
|
+
JobConfig,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
class HookManager:
|
19
|
+
hooks_store: HooksStore
|
20
|
+
|
21
|
+
def __init__(self, hooks: Hooks, verbose: bool) -> None:
|
22
|
+
self.hooks_store: HooksStore = defaultdict(list)
|
23
|
+
self.initialise_hooks(hooks, verbose)
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def is_valid_hook_name(hook_name: typing.Union[HookName, str]) -> bool:
|
27
|
+
"""Returns True/False depending on hook-name validity."""
|
28
|
+
if isinstance(hook_name, str):
|
29
|
+
try:
|
30
|
+
HookName(hook_name) # lookup by value
|
31
|
+
return True
|
32
|
+
except ValueError:
|
33
|
+
return False
|
34
|
+
# the type is a HookName
|
35
|
+
if not isinstance(hook_name, HookName):
|
36
|
+
return False
|
37
|
+
return True
|
38
|
+
|
39
|
+
def register_hook(
|
40
|
+
self, hook_name: HookName, hook_config: HookConfig, verbose: bool
|
41
|
+
) -> None:
|
42
|
+
"""Registers a hook_config to a specific hook-type."""
|
43
|
+
if not self.is_valid_hook_name(hook_name):
|
44
|
+
raise ValueError(f"Hook {hook_name} does not exist.")
|
45
|
+
self.hooks_store[hook_name].append(hook_config)
|
46
|
+
if verbose:
|
47
|
+
log(
|
48
|
+
f"hooks: registered hook for '{hook_name}', "
|
49
|
+
f"have {len(self.hooks_store[hook_name])}: "
|
50
|
+
f"{Job.get_job_name(hook_config)}" # type: ignore[arg-type]
|
51
|
+
)
|
52
|
+
|
53
|
+
def deregister_hook(
|
54
|
+
self, hook_name: HookName, hook_config: HookConfig, verbose: bool
|
55
|
+
) -> None:
|
56
|
+
"""Deregisters a hook_config from a specific hook-type."""
|
57
|
+
if not (
|
58
|
+
hook_name in self.hooks_store and hook_config in self.hooks_store[hook_name]
|
59
|
+
):
|
60
|
+
raise ValueError(f"Function not found in hook {hook_name}.")
|
61
|
+
self.hooks_store[hook_name].remove(hook_config)
|
62
|
+
if verbose:
|
63
|
+
log(
|
64
|
+
f"hooks: deregistered hooks for '{hook_name}', "
|
65
|
+
f"have {len(self.hooks_store[hook_name])}"
|
66
|
+
)
|
67
|
+
|
68
|
+
def invoke_hooks(
|
69
|
+
self,
|
70
|
+
hook_name: HookName,
|
71
|
+
config_metadata: ConfigMetadata,
|
72
|
+
**kwargs: typing.Any,
|
73
|
+
) -> None:
|
74
|
+
"""Invokes all functions registered to a specific hook."""
|
75
|
+
hooks: typing.List[HookConfig] = self.hooks_store.get(hook_name, [])
|
76
|
+
if config_metadata.args.verbose:
|
77
|
+
log(f"hooks: invoking {len(hooks)} hooks for '{hook_name}'")
|
78
|
+
|
79
|
+
hook_config: HookConfig
|
80
|
+
for hook_config in hooks:
|
81
|
+
job_config: JobConfig = {
|
82
|
+
"label": str(hook_name),
|
83
|
+
"ctx": None,
|
84
|
+
"when": {"phase": str(hook_name), "tags": {str(hook_name)}},
|
85
|
+
}
|
86
|
+
if "addr" in hook_config:
|
87
|
+
job_config["addr"] = hook_config["addr"]
|
88
|
+
if "command" in hook_config:
|
89
|
+
job_config["command"] = hook_config["command"]
|
90
|
+
file_lists: FilePathListLookup = defaultdict(list)
|
91
|
+
file_lists[str(hook_name)] = [__file__]
|
92
|
+
job_execute(
|
93
|
+
job_config,
|
94
|
+
running_jobs={},
|
95
|
+
config_metadata=config_metadata,
|
96
|
+
file_lists=file_lists,
|
97
|
+
**kwargs,
|
98
|
+
)
|
99
|
+
|
100
|
+
if config_metadata.args.verbose:
|
101
|
+
log(f"hooks: done invoking '{hook_name}'")
|
102
|
+
|
103
|
+
def initialise_hooks(self, hooks: Hooks, verbose: bool) -> None:
|
104
|
+
"""Initialised the hook with the configured data."""
|
105
|
+
if verbose:
|
106
|
+
num_hooks: int = sum(len(hooks_list) for hooks_list in hooks.values())
|
107
|
+
if num_hooks:
|
108
|
+
log(f"hooks: initialising {num_hooks} hooks")
|
109
|
+
for hook_name in hooks:
|
110
|
+
hook: HookConfig
|
111
|
+
if verbose:
|
112
|
+
log(
|
113
|
+
f"hooks:\tinitialising {len(hooks[hook_name])} hooks for '{hook_name}'"
|
114
|
+
)
|
115
|
+
for hook in hooks[hook_name]:
|
116
|
+
self.register_hook(hook_name, hook, verbose)
|
runem/job_execute.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import inspect
|
2
1
|
import os
|
3
2
|
import pathlib
|
4
3
|
import typing
|
@@ -10,7 +9,7 @@ from runem.config_metadata import ConfigMetadata
|
|
10
9
|
from runem.informative_dict import ReadOnlyInformativeDict
|
11
10
|
from runem.job import Job
|
12
11
|
from runem.job_wrapper import get_job_wrapper
|
13
|
-
from runem.log import log
|
12
|
+
from runem.log import error, log
|
14
13
|
from runem.types import (
|
15
14
|
FilePathListLookup,
|
16
15
|
JobConfig,
|
@@ -27,6 +26,7 @@ def job_execute_inner(
|
|
27
26
|
job_config: JobConfig,
|
28
27
|
config_metadata: ConfigMetadata,
|
29
28
|
file_lists: FilePathListLookup,
|
29
|
+
**kwargs: typing.Any,
|
30
30
|
) -> typing.Tuple[JobTiming, JobReturn]:
|
31
31
|
"""Wrapper for running a job inside a sub-process.
|
32
32
|
|
@@ -73,31 +73,26 @@ def job_execute_inner(
|
|
73
73
|
os.chdir(root_path)
|
74
74
|
|
75
75
|
start = timer()
|
76
|
-
func_signature = inspect.signature(function)
|
77
76
|
if config_metadata.args.verbose:
|
78
77
|
log(f"job: running: '{Job.get_job_name(job_config)}'")
|
79
78
|
reports: JobReturn
|
80
79
|
try:
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
label=Job.get_job_name(job_config),
|
94
|
-
job=job_config,
|
95
|
-
record_sub_job_time=_record_sub_job_time,
|
96
|
-
)
|
80
|
+
reports = function(
|
81
|
+
options=ReadOnlyInformativeDict(config_metadata.options), # type: ignore
|
82
|
+
file_list=file_list,
|
83
|
+
procs=config_metadata.args.procs,
|
84
|
+
root_path=root_path,
|
85
|
+
verbose=config_metadata.args.verbose,
|
86
|
+
# unpack useful data points from the job_config
|
87
|
+
label=Job.get_job_name(job_config),
|
88
|
+
job=job_config,
|
89
|
+
record_sub_job_time=_record_sub_job_time,
|
90
|
+
**kwargs,
|
91
|
+
)
|
97
92
|
except BaseException: # pylint: disable=broad-exception-caught
|
98
93
|
# log that we hit an error on this job and re-raise
|
99
94
|
log(decorate=False)
|
100
|
-
|
95
|
+
error(f"job: job '{Job.get_job_name(job_config)}' failed to complete!")
|
101
96
|
# re-raise
|
102
97
|
raise
|
103
98
|
|
@@ -114,6 +109,7 @@ def job_execute(
|
|
114
109
|
running_jobs: typing.Dict[str, str],
|
115
110
|
config_metadata: ConfigMetadata,
|
116
111
|
file_lists: FilePathListLookup,
|
112
|
+
**kwargs: typing.Any,
|
117
113
|
) -> typing.Tuple[JobTiming, JobReturn]:
|
118
114
|
"""Thin-wrapper around job_execute_inner needed for mocking in tests.
|
119
115
|
|
@@ -121,6 +117,11 @@ def job_execute(
|
|
121
117
|
"""
|
122
118
|
this_id: str = str(uuid.uuid4())
|
123
119
|
running_jobs[this_id] = Job.get_job_name(job_config)
|
124
|
-
results = job_execute_inner(
|
120
|
+
results = job_execute_inner(
|
121
|
+
job_config,
|
122
|
+
config_metadata,
|
123
|
+
file_lists,
|
124
|
+
**kwargs,
|
125
|
+
)
|
125
126
|
del running_jobs[this_id]
|
126
127
|
return results
|
runem/job_filter.py
CHANGED
@@ -35,7 +35,7 @@ def _should_filter_out_by_tags(
|
|
35
35
|
if verbose:
|
36
36
|
log(
|
37
37
|
(
|
38
|
-
f"not running job '{job
|
38
|
+
f"not running job '{Job.get_job_name(job)}' because it doesn't have "
|
39
39
|
f"any of the following tags: {printable_set(tags)}"
|
40
40
|
)
|
41
41
|
)
|
@@ -46,7 +46,7 @@ def _should_filter_out_by_tags(
|
|
46
46
|
if verbose:
|
47
47
|
log(
|
48
48
|
(
|
49
|
-
f"not running job '{job
|
49
|
+
f"not running job '{Job.get_job_name(job)}' because it contains the "
|
50
50
|
f"following tags: {printable_set(has_tags_to_avoid)}"
|
51
51
|
)
|
52
52
|
)
|
@@ -5,6 +5,12 @@ from runem.run_command import run_command
|
|
5
5
|
from runem.types import JobConfig
|
6
6
|
|
7
7
|
|
8
|
+
def validate_simple_command(command_string: str) -> typing.List[str]:
|
9
|
+
# use shlex to handle parsing of the command string, a non-trivial problem.
|
10
|
+
split_command: typing.List[str] = shlex.split(command_string)
|
11
|
+
return split_command
|
12
|
+
|
13
|
+
|
8
14
|
def job_runner_simple_command(
|
9
15
|
**kwargs: typing.Any,
|
10
16
|
) -> None:
|
@@ -17,7 +23,7 @@ def job_runner_simple_command(
|
|
17
23
|
command_string: str = job_config["command"]
|
18
24
|
|
19
25
|
# use shlex to handle parsing of the command string, a non-trivial problem.
|
20
|
-
result =
|
26
|
+
result = validate_simple_command(command_string)
|
21
27
|
|
22
28
|
# preserve quotes for consistent handling of strings and avoid the "word
|
23
29
|
# splitting" problem for unix-like shells.
|
runem/job_wrapper.py
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
import pathlib
|
2
2
|
|
3
|
-
from runem.job_runner_simple_command import
|
3
|
+
from runem.job_runner_simple_command import (
|
4
|
+
job_runner_simple_command,
|
5
|
+
validate_simple_command,
|
6
|
+
)
|
4
7
|
from runem.job_wrapper_python import get_job_wrapper_py_func
|
5
|
-
from runem.types import
|
8
|
+
from runem.types import JobFunction, JobWrapper
|
6
9
|
|
7
10
|
|
8
|
-
def get_job_wrapper(
|
11
|
+
def get_job_wrapper(job_wrapper: JobWrapper, cfg_filepath: pathlib.Path) -> JobFunction:
|
9
12
|
"""Given a job-description determines the job-runner, returning it as a function.
|
10
13
|
|
11
14
|
NOTE: Side-effects: also re-addressed the job-config in the case of functions see
|
12
15
|
get_job_function.
|
13
16
|
"""
|
14
|
-
if "command" in
|
17
|
+
if "command" in job_wrapper:
|
18
|
+
# validate that the command is "understandable" and usable.
|
19
|
+
command_string: str = job_wrapper["command"]
|
20
|
+
validate_simple_command(command_string)
|
15
21
|
return job_runner_simple_command # type: ignore # NO_COMMIT
|
16
22
|
|
17
23
|
# if we do not have a simple command address assume we have just an addressed
|
18
24
|
# function
|
19
|
-
return get_job_wrapper_py_func(
|
25
|
+
return get_job_wrapper_py_func(job_wrapper, cfg_filepath)
|
runem/job_wrapper_python.py
CHANGED
@@ -3,7 +3,7 @@ import sys
|
|
3
3
|
from importlib.util import module_from_spec
|
4
4
|
from importlib.util import spec_from_file_location as module_spec_from_file_location
|
5
5
|
|
6
|
-
from runem.types import FunctionNotFound,
|
6
|
+
from runem.types import FunctionNotFound, JobFunction, JobWrapper
|
7
7
|
|
8
8
|
|
9
9
|
def _load_python_function_from_module(
|
@@ -86,22 +86,22 @@ def _find_job_module(cfg_filepath: pathlib.Path, module_file_path: str) -> pathl
|
|
86
86
|
|
87
87
|
|
88
88
|
def get_job_wrapper_py_func(
|
89
|
-
|
89
|
+
job_wrapper: JobWrapper, cfg_filepath: pathlib.Path
|
90
90
|
) -> JobFunction:
|
91
91
|
"""For a job, dynamically loads the associated python job-function.
|
92
92
|
|
93
93
|
Side-effects: also re-addressed the job-config.
|
94
94
|
"""
|
95
|
-
function_to_load: str =
|
95
|
+
function_to_load: str = job_wrapper["addr"]["function"]
|
96
96
|
try:
|
97
97
|
module_file_path: pathlib.Path = _find_job_module(
|
98
|
-
cfg_filepath,
|
98
|
+
cfg_filepath, job_wrapper["addr"]["file"]
|
99
99
|
)
|
100
100
|
except FunctionNotFound as err:
|
101
101
|
raise FunctionNotFound(
|
102
102
|
(
|
103
|
-
|
104
|
-
f"job.addr.file '{
|
103
|
+
"runem failed to find "
|
104
|
+
f"job.addr.file '{job_wrapper['addr']['file']}' looking for "
|
105
105
|
f"job.addr.function '{function_to_load}'"
|
106
106
|
)
|
107
107
|
) from err
|
@@ -118,5 +118,5 @@ def get_job_wrapper_py_func(
|
|
118
118
|
)
|
119
119
|
|
120
120
|
# re-write the job-config file-path for the module with the one that worked
|
121
|
-
|
121
|
+
job_wrapper["addr"]["file"] = str(module_file_path)
|
122
122
|
return function
|
runem/log.py
CHANGED
@@ -14,3 +14,11 @@ def log(msg: str = "", decorate: bool = True, end: typing.Optional[str] = None)
|
|
14
14
|
# print in a blocking manner, waiting for system resources to free up if a
|
15
15
|
# runem job is contending on stdout or similar.
|
16
16
|
blocking_print(msg, end=end)
|
17
|
+
|
18
|
+
|
19
|
+
def warn(msg: str) -> None:
|
20
|
+
log(f"WARNING: {msg}")
|
21
|
+
|
22
|
+
|
23
|
+
def error(msg: str) -> None:
|
24
|
+
log(f"ERROR: {msg}")
|
runem/report.py
CHANGED
@@ -43,7 +43,7 @@ def _align_bar_graphs_workaround(original_text: str) -> str:
|
|
43
43
|
return formatted_text
|
44
44
|
|
45
45
|
|
46
|
-
def
|
46
|
+
def replace_bar_graph_characters(text: str, end_str: str, replace_char: str) -> str:
|
47
47
|
"""Replaces block characters in lines containing `end_str` with give char.
|
48
48
|
|
49
49
|
Args:
|
@@ -56,16 +56,19 @@ def _replace_bar_characters(text: str, end_str: str, replace_char: str) -> str:
|
|
56
56
|
"""
|
57
57
|
# Define the block character and its light shade replacement
|
58
58
|
block_chars = (
|
59
|
-
"
|
59
|
+
"▏▎▍▋▊▉█▌▐▄▀─" # Extend this string with any additional block characters you use
|
60
|
+
"░·" # also include the chars we might replace with for special bars
|
60
61
|
)
|
61
62
|
|
62
63
|
text_lines: typing.List[str] = text.split("\n")
|
63
64
|
|
64
65
|
# Process each line, replacing block characters if `end_str` is present
|
65
66
|
modified_lines = [
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
(
|
68
|
+
line.translate(str.maketrans(block_chars, replace_char * len(block_chars)))
|
69
|
+
if end_str in line
|
70
|
+
else line
|
71
|
+
)
|
69
72
|
for line in text_lines
|
70
73
|
]
|
71
74
|
|
@@ -74,12 +77,12 @@ def _replace_bar_characters(text: str, end_str: str, replace_char: str) -> str:
|
|
74
77
|
|
75
78
|
def _semi_shade_phase_totals(text: str) -> str:
|
76
79
|
light_shade_char = "░"
|
77
|
-
return
|
80
|
+
return replace_bar_graph_characters(text, "(user-time)", light_shade_char)
|
78
81
|
|
79
82
|
|
80
83
|
def _dot_jobs(text: str) -> str:
|
81
84
|
dot_char = "·"
|
82
|
-
return
|
85
|
+
return replace_bar_graph_characters(text, "(+)", dot_char)
|
83
86
|
|
84
87
|
|
85
88
|
def _plot_times(
|
@@ -103,8 +106,8 @@ def _plot_times(
|
|
103
106
|
|
104
107
|
for idx, phase in enumerate(phase_run_oder):
|
105
108
|
not_last_phase: bool = idx < len(phase_run_oder) - 1
|
106
|
-
utf8_phase = "├" if not_last_phase else "└"
|
107
|
-
utf8_phase_group = "│" if not_last_phase else "
|
109
|
+
utf8_phase = " ├" if not_last_phase else " └"
|
110
|
+
utf8_phase_group = " │" if not_last_phase else " "
|
108
111
|
# log(f"Phase '{phase}' jobs took:")
|
109
112
|
phase_start_idx = len(labels)
|
110
113
|
|
@@ -121,9 +124,11 @@ def _plot_times(
|
|
121
124
|
|
122
125
|
runem_app_timing: typing.List[JobTiming] = timing_data["_app"]
|
123
126
|
job_metadata: JobTiming
|
124
|
-
for job_metadata in reversed(runem_app_timing):
|
127
|
+
for idx, job_metadata in enumerate(reversed(runem_app_timing)):
|
128
|
+
last_group: bool = idx == 0 # reverse sorted
|
129
|
+
utf8_group = "├" if not last_group else "└"
|
125
130
|
job_label, job_time_total = job_metadata["job"]
|
126
|
-
labels.insert(0, f"
|
131
|
+
labels.insert(0, f"{utf8_group}runem.{job_label}")
|
127
132
|
times.insert(0, job_time_total.total_seconds())
|
128
133
|
labels.insert(0, "runem (total wall-clock)")
|
129
134
|
times.insert(0, wall_clock_for_runem_main.total_seconds())
|
@@ -175,7 +180,7 @@ def _gen_jobs_report(
|
|
175
180
|
utf8_job = "├" if not_last else "└"
|
176
181
|
utf8_sub_jobs = "│" if not_last else " "
|
177
182
|
job_label, job_time_total = job_timing["job"]
|
178
|
-
job_bar_label: str = f"{
|
183
|
+
job_bar_label: str = f"{job_label}"
|
179
184
|
labels.append(f"{utf8_phase_group}{utf8_job}{job_bar_label}")
|
180
185
|
times.append(job_time_total.total_seconds())
|
181
186
|
job_time_sum += job_time_total
|
@@ -191,8 +196,7 @@ def _gen_jobs_report(
|
|
191
196
|
if idx == len(sub_command_times) - 1:
|
192
197
|
sub_utf8 = "└"
|
193
198
|
labels.append(
|
194
|
-
f"{utf8_phase_group}{utf8_sub_jobs}{sub_utf8}{
|
195
|
-
f".{sub_job_label} (+)"
|
199
|
+
f"{utf8_phase_group}{utf8_sub_jobs}{sub_utf8}{sub_job_label} (+)"
|
196
200
|
)
|
197
201
|
times.append(sub_job_time.total_seconds())
|
198
202
|
return job_time_sum
|
runem/runem.py
CHANGED
@@ -34,18 +34,19 @@ from timeit import default_timer as timer
|
|
34
34
|
from halo import Halo
|
35
35
|
|
36
36
|
from runem.command_line import parse_args
|
37
|
-
from runem.config import
|
37
|
+
from runem.config import load_project_config, load_user_configs
|
38
38
|
from runem.config_metadata import ConfigMetadata
|
39
|
-
from runem.config_parse import
|
39
|
+
from runem.config_parse import load_config_metadata
|
40
40
|
from runem.files import find_files
|
41
41
|
from runem.job import Job
|
42
42
|
from runem.job_execute import job_execute
|
43
43
|
from runem.job_filter import filter_jobs
|
44
|
-
from runem.log import log
|
44
|
+
from runem.log import error, log, warn
|
45
45
|
from runem.report import report_on_run
|
46
46
|
from runem.types import (
|
47
47
|
Config,
|
48
48
|
FilePathListLookup,
|
49
|
+
HookName,
|
49
50
|
JobReturn,
|
50
51
|
JobRunMetadata,
|
51
52
|
JobRunMetadatasByPhase,
|
@@ -68,8 +69,11 @@ def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
|
|
68
69
|
"""
|
69
70
|
config: Config
|
70
71
|
cfg_filepath: pathlib.Path
|
71
|
-
config, cfg_filepath =
|
72
|
-
|
72
|
+
config, cfg_filepath = load_project_config()
|
73
|
+
user_configs: typing.List[typing.Tuple[Config, pathlib.Path]] = load_user_configs()
|
74
|
+
config_metadata: ConfigMetadata = load_config_metadata(
|
75
|
+
config, cfg_filepath, user_configs, verbose=("--verbose" in argv)
|
76
|
+
)
|
73
77
|
|
74
78
|
# Now we parse the cli arguments extending them with information from the
|
75
79
|
# .runem.yml config.
|
@@ -263,18 +267,21 @@ def _process_jobs_by_phase(
|
|
263
267
|
)
|
264
268
|
if failure_exception is not None:
|
265
269
|
if config_metadata.args.verbose:
|
266
|
-
|
270
|
+
error(f"running phase {phase}: aborting run")
|
267
271
|
return failure_exception
|
268
272
|
|
269
273
|
# ALl phases completed aok.
|
270
274
|
return None
|
271
275
|
|
272
276
|
|
277
|
+
MainReturnType = typing.Tuple[
|
278
|
+
ConfigMetadata, JobRunMetadatasByPhase, typing.Optional[BaseException]
|
279
|
+
]
|
280
|
+
|
281
|
+
|
273
282
|
def _main(
|
274
283
|
argv: typing.List[str],
|
275
|
-
) ->
|
276
|
-
OrderedPhases, JobRunMetadatasByPhase, typing.Optional[BaseException]
|
277
|
-
]:
|
284
|
+
) -> MainReturnType:
|
278
285
|
start = timer()
|
279
286
|
|
280
287
|
config_metadata: ConfigMetadata = _determine_run_parameters(argv)
|
@@ -283,7 +290,10 @@ def _main(
|
|
283
290
|
os.chdir(config_metadata.cfg_filepath.parent)
|
284
291
|
|
285
292
|
file_lists: FilePathListLookup = find_files(config_metadata)
|
286
|
-
|
293
|
+
if not file_lists:
|
294
|
+
warn("no files found")
|
295
|
+
return (config_metadata, {}, None)
|
296
|
+
|
287
297
|
if config_metadata.args.verbose:
|
288
298
|
log(f"found {len(file_lists)} batches, ", end="")
|
289
299
|
for tag in sorted(file_lists.keys()):
|
@@ -322,7 +332,7 @@ def _main(
|
|
322
332
|
phase_run_report: JobReturn = None
|
323
333
|
phase_run_metadata: JobRunMetadata = (phase_run_timing, phase_run_report)
|
324
334
|
job_run_metadatas["_app"].append(phase_run_metadata)
|
325
|
-
return config_metadata
|
335
|
+
return config_metadata, job_run_metadatas, failure_exception
|
326
336
|
|
327
337
|
|
328
338
|
def timed_main(argv: typing.List[str]) -> None:
|
@@ -332,10 +342,11 @@ def timed_main(argv: typing.List[str]) -> None:
|
|
332
342
|
are representative.
|
333
343
|
"""
|
334
344
|
start = timer()
|
335
|
-
|
345
|
+
config_metadata: ConfigMetadata
|
336
346
|
job_run_metadatas: JobRunMetadatasByPhase
|
337
347
|
failure_exception: typing.Optional[BaseException]
|
338
|
-
|
348
|
+
config_metadata, job_run_metadatas, failure_exception = _main(argv)
|
349
|
+
phase_run_oder: OrderedPhases = config_metadata.phases
|
339
350
|
end = timer()
|
340
351
|
time_taken: timedelta = timedelta(seconds=end - start)
|
341
352
|
wall_clock_time_saved: timedelta
|
@@ -354,6 +365,12 @@ def timed_main(argv: typing.List[str]) -> None:
|
|
354
365
|
)
|
355
366
|
)
|
356
367
|
|
368
|
+
config_metadata.hook_manager.invoke_hooks(
|
369
|
+
hook_name=HookName.ON_EXIT,
|
370
|
+
config_metadata=config_metadata,
|
371
|
+
wall_clock_time_saved=wall_clock_time_saved,
|
372
|
+
)
|
373
|
+
|
357
374
|
if failure_exception is not None:
|
358
375
|
# we got a failure somewhere, now that we've reported the timings we
|
359
376
|
# re-raise.
|
runem/types.py
CHANGED
@@ -2,6 +2,7 @@ import argparse
|
|
2
2
|
import pathlib
|
3
3
|
import typing
|
4
4
|
from datetime import timedelta
|
5
|
+
from enum import Enum
|
5
6
|
|
6
7
|
from runem.informative_dict import InformativeDict, ReadOnlyInformativeDict
|
7
8
|
|
@@ -26,6 +27,15 @@ ReportUrlInfo = typing.Tuple[ReportName, ReportUrl]
|
|
26
27
|
ReportUrls = typing.List[ReportUrlInfo]
|
27
28
|
|
28
29
|
|
30
|
+
class HookName(Enum):
|
31
|
+
# at exit
|
32
|
+
ON_EXIT = "on-exit"
|
33
|
+
# before all tasks are run, after config is read
|
34
|
+
# BEFORE_ALL = "before-all"
|
35
|
+
# after all tasks are done, before reporting
|
36
|
+
# AFTER_ALL = "after-all"
|
37
|
+
|
38
|
+
|
29
39
|
class JobReturnData(typing.TypedDict, total=False):
|
30
40
|
"""A dict that defines job result to be reported to the user."""
|
31
41
|
|
@@ -125,7 +135,14 @@ class JobWhen(typing.TypedDict, total=False):
|
|
125
135
|
phase: PhaseName # the phase when the job should be run
|
126
136
|
|
127
137
|
|
128
|
-
class
|
138
|
+
class JobWrapper(typing.TypedDict, total=False):
|
139
|
+
"""A base-type for jobs, hooks, and things that can be invoked."""
|
140
|
+
|
141
|
+
addr: JobAddressConfig # which callable to call
|
142
|
+
command: str # a one-liner command to be run
|
143
|
+
|
144
|
+
|
145
|
+
class JobConfig(JobWrapper, total=False):
|
129
146
|
"""A dict that defines a job to be run.
|
130
147
|
|
131
148
|
It consists of the label, address, context and filter information
|
@@ -134,8 +151,6 @@ class JobConfig(typing.TypedDict, total=False):
|
|
134
151
|
"""
|
135
152
|
|
136
153
|
label: JobName # the name of the job
|
137
|
-
addr: JobAddressConfig # which callable to call
|
138
|
-
command: str # a one-liner command to be run
|
139
154
|
ctx: typing.Optional[JobContextConfig] # how to call the callable
|
140
155
|
when: JobWhen # when to call the job
|
141
156
|
|
@@ -193,6 +208,30 @@ class GlobalSerialisedConfig(typing.TypedDict):
|
|
193
208
|
config: GlobalConfig
|
194
209
|
|
195
210
|
|
211
|
+
class HookConfig(JobWrapper, total=False):
|
212
|
+
"""Specification for hooks.
|
213
|
+
|
214
|
+
Like JobConfig with use addr or command to specify what to execute.
|
215
|
+
"""
|
216
|
+
|
217
|
+
hook_name: HookName # the hook for when this is called
|
218
|
+
|
219
|
+
|
220
|
+
Hooks = typing.DefaultDict[HookName, typing.List[HookConfig]]
|
221
|
+
|
222
|
+
# A dictionary to hold hooks, with hook names as keys
|
223
|
+
HooksStore = typing.Dict[HookName, typing.List[HookConfig]]
|
224
|
+
|
225
|
+
|
226
|
+
class HookSerialisedConfig(typing.TypedDict):
|
227
|
+
"""Intended to make reading a config file easier.
|
228
|
+
|
229
|
+
Also, unlike JobSerialisedConfig, this type may not actually help readability.
|
230
|
+
"""
|
231
|
+
|
232
|
+
hook: HookConfig
|
233
|
+
|
234
|
+
|
196
235
|
class JobSerialisedConfig(typing.TypedDict):
|
197
236
|
"""Makes serialised configs easier to read.
|
198
237
|
|
@@ -204,6 +243,10 @@ class JobSerialisedConfig(typing.TypedDict):
|
|
204
243
|
job: JobConfig
|
205
244
|
|
206
245
|
|
207
|
-
ConfigNodes = typing.Union[
|
246
|
+
ConfigNodes = typing.Union[
|
247
|
+
GlobalSerialisedConfig, JobSerialisedConfig, HookSerialisedConfig
|
248
|
+
]
|
208
249
|
# The config format as it is serialised to/from disk
|
209
250
|
Config = typing.List[ConfigNodes]
|
251
|
+
|
252
|
+
UserConfigMetadata = typing.List[typing.Tuple[Config, pathlib.Path]]
|