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/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
- if "args" in func_signature.parameters:
82
- reports = function( # type: ignore # FIXME: which function do we have?
83
- config_metadata.args, config_metadata.options, file_list
84
- )
85
- else:
86
- reports = function(
87
- options=ReadOnlyInformativeDict(config_metadata.options), # type: ignore
88
- file_list=file_list,
89
- procs=config_metadata.args.procs,
90
- root_path=root_path,
91
- verbose=config_metadata.args.verbose,
92
- # unpack useful data points from the job_config
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
- log(f"job: ERROR: job '{Job.get_job_name(job_config)}' failed to complete!")
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(job_config, config_metadata, file_lists)
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['label']}' because it doesn't have "
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['label']}' because it contains the "
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 = shlex.split(command_string)
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 job_runner_simple_command
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 JobConfig, JobFunction
8
+ from runem.types import JobFunction, JobWrapper
6
9
 
7
10
 
8
- def get_job_wrapper(job_config: JobConfig, cfg_filepath: pathlib.Path) -> JobFunction:
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 job_config:
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(job_config, cfg_filepath)
25
+ return get_job_wrapper_py_func(job_wrapper, cfg_filepath)
@@ -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, JobConfig, JobFunction
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
- job_config: JobConfig, cfg_filepath: pathlib.Path
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 = job_config["addr"]["function"]
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, job_config["addr"]["file"]
98
+ cfg_filepath, job_wrapper["addr"]["file"]
99
99
  )
100
100
  except FunctionNotFound as err:
101
101
  raise FunctionNotFound(
102
102
  (
103
- f"Whilst loading job '{job_config['label']}' runem failed to find "
104
- f"job.addr.file '{job_config['addr']['file']}' looking for "
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
- job_config["addr"]["file"] = str(module_file_path)
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 _replace_bar_characters(text: str, end_str: str, replace_char: str) -> str:
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
- "▏▎▋▊█▌▐▄▀─" # Extend this string with any additional block characters you use
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
- line.translate(str.maketrans(block_chars, replace_char * len(block_chars)))
67
- if end_str in line
68
- else line
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 _replace_bar_characters(text, "(user-time)", light_shade_char)
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 _replace_bar_characters(text, "(+)", dot_char)
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"runem.{job_label}")
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"{phase}.{job_label}"
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}{job_bar_label}"
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 load_config
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 parse_config
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 = load_config()
72
- config_metadata: ConfigMetadata = parse_config(config, cfg_filepath)
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
- log(f"ERROR: running phase {phase}: aborting run")
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
- ) -> typing.Tuple[
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
- assert file_lists
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.phases, job_run_metadatas, failure_exception
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
- phase_run_oder: OrderedPhases
345
+ config_metadata: ConfigMetadata
336
346
  job_run_metadatas: JobRunMetadatasByPhase
337
347
  failure_exception: typing.Optional[BaseException]
338
- phase_run_oder, job_run_metadatas, failure_exception = _main(argv)
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 JobConfig(typing.TypedDict, total=False):
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[GlobalSerialisedConfig, JobSerialisedConfig]
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]]