runem 0.0.17__py3-none-any.whl → 0.0.18__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/command_line.py CHANGED
@@ -147,7 +147,7 @@ def parse_args(
147
147
 
148
148
  args = parser.parse_args(argv[1:])
149
149
 
150
- options: Options = _initialise_options(config_metadata, args)
150
+ options: Options = initialise_options(config_metadata, args)
151
151
 
152
152
  if not _validate_filters(config_metadata, args):
153
153
  sys.exit(1)
@@ -214,7 +214,7 @@ def _validate_filters(
214
214
  return True
215
215
 
216
216
 
217
- def _initialise_options(
217
+ def initialise_options(
218
218
  config_metadata: ConfigMetadata,
219
219
  args: argparse.Namespace,
220
220
  ) -> Options:
@@ -227,7 +227,7 @@ def _initialise_options(
227
227
  option["name"]: option["default"] for option in config_metadata.options_config
228
228
  }
229
229
  if config_metadata.options_config and args.overrides_on: # pragma: no branch
230
- for option_name in args.overrides_on:
230
+ for option_name in args.overrides_on: # pragma: no branch
231
231
  options[option_name] = True
232
232
  if config_metadata.options_config and args.overrides_off: # pragma: no branch
233
233
  for option_name in args.overrides_off:
runem/config_parse.py CHANGED
@@ -4,7 +4,7 @@ import typing
4
4
  from collections import defaultdict
5
5
 
6
6
  from runem.config_metadata import ConfigMetadata
7
- from runem.job_function_python import get_job_function
7
+ from runem.job_wrapper_python import get_job_wrapper
8
8
  from runem.log import log
9
9
  from runem.types import (
10
10
  Config,
@@ -79,7 +79,7 @@ def parse_job_config(
79
79
  sys.exit(1)
80
80
 
81
81
  # try and load the function _before_ we schedule it's execution
82
- get_job_function(job, cfg_filepath)
82
+ get_job_wrapper(job, cfg_filepath)
83
83
  phase_id: PhaseName = job["when"]["phase"]
84
84
  in_out_jobs_by_phase[phase_id].append(job)
85
85
 
@@ -2,16 +2,17 @@ import inspect
2
2
  import os
3
3
  import pathlib
4
4
  import typing
5
+ import uuid
5
6
  from datetime import timedelta
6
7
  from timeit import default_timer as timer
7
8
 
8
9
  from runem.config_metadata import ConfigMetadata
9
- from runem.job_function_python import get_job_function
10
+ from runem.job_wrapper_python import get_job_wrapper
10
11
  from runem.log import log
11
12
  from runem.types import FilePathList, FilePathListLookup, JobConfig, JobReturn, JobTags
12
13
 
13
14
 
14
- def job_runner_inner(
15
+ def job_execute_inner(
15
16
  job_config: JobConfig,
16
17
  config_metadata: ConfigMetadata,
17
18
  file_lists: FilePathListLookup,
@@ -27,7 +28,7 @@ def job_runner_inner(
27
28
  function: typing.Callable
28
29
  job_tags: JobTags = set(job_config["when"]["tags"])
29
30
  os.chdir(root_path)
30
- function = get_job_function(job_config, config_metadata.cfg_filepath)
31
+ function = get_job_wrapper(job_config, config_metadata.cfg_filepath)
31
32
 
32
33
  # get the files for all files found for this job's tags
33
34
  file_list: FilePathList = []
@@ -74,13 +75,18 @@ def job_runner_inner(
74
75
  return (timing_data, reports)
75
76
 
76
77
 
77
- def job_runner(
78
+ def job_execute(
78
79
  job_config: JobConfig,
80
+ running_jobs: typing.Dict[str, str],
79
81
  config_metadata: ConfigMetadata,
80
82
  file_lists: FilePathListLookup,
81
83
  ) -> typing.Tuple[typing.Tuple[str, timedelta], JobReturn]:
82
- """Thing wrapper around job_runner_inner needed fro mocking in tests.
84
+ """Thin-wrapper around job_execute_inner needed for mocking in tests.
83
85
 
84
86
  Needed for faster tests.
85
87
  """
86
- return job_runner_inner(job_config, config_metadata, file_lists)
88
+ this_id: str = str(uuid.uuid4())
89
+ running_jobs[this_id] = job_config["label"]
90
+ results = job_execute_inner(job_config, config_metadata, file_lists)
91
+ del running_jobs[this_id]
92
+ return results
runem/job_filter.py CHANGED
@@ -77,10 +77,14 @@ def filter_jobs(
77
77
  log(f"filtering for tags {printable_set(tags_to_run)}", decorate=True, end="")
78
78
  if tags_to_avoid:
79
79
  if tags_to_run:
80
- log(", ", end="")
80
+ log(", ", decorate=False, end="")
81
81
  else:
82
- log(decorate=True)
83
- log(f"excluding jobs with tags {printable_set(tags_to_avoid)}", end="")
82
+ log(decorate=True, end="")
83
+ log(
84
+ f"excluding jobs with tags {printable_set(tags_to_avoid)}",
85
+ decorate=False,
86
+ end="",
87
+ )
84
88
  if tags_to_run or tags_to_avoid:
85
89
  log(decorate=False)
86
90
  filtered_jobs: PhaseGroupedJobs = defaultdict(list)
@@ -20,20 +20,35 @@ def _load_python_function_from_module(
20
20
  ).absolute()
21
21
 
22
22
  # load the function
23
- module_spec = module_spec_from_file_location(function_to_load, abs_module_file_path)
24
- if not module_spec:
23
+ try:
24
+ module_spec = module_spec_from_file_location(
25
+ function_to_load, abs_module_file_path
26
+ )
27
+ if not module_spec:
28
+ raise FileNotFoundError()
29
+ if not module_spec.loader:
30
+ raise FunctionNotFound("unable to load module")
31
+ except FileNotFoundError as err:
25
32
  raise FunctionNotFound(
26
33
  (
27
34
  f"unable to load '{function_to_load}' from '{str(module_file_path)} "
28
35
  f"relative to '{str(cfg_filepath)}"
29
36
  )
30
- )
37
+ ) from err
31
38
 
32
39
  module = module_from_spec(module_spec)
33
- sys.modules[module_name] = module
34
- if not module_spec.loader:
40
+ if not module:
35
41
  raise FunctionNotFound("unable to load module")
36
- module_spec.loader.exec_module(module)
42
+ sys.modules[module_name] = module
43
+ try:
44
+ module_spec.loader.exec_module(module)
45
+ except FileNotFoundError as err:
46
+ raise FunctionNotFound(
47
+ (
48
+ f"unable to load '{function_to_load}' from '{str(module_file_path)} "
49
+ f"relative to '{str(cfg_filepath)}"
50
+ )
51
+ ) from err
37
52
  try:
38
53
  function: JobFunction = getattr(module, function_to_load)
39
54
  except AttributeError as err:
@@ -70,7 +85,7 @@ def _find_job_module(cfg_filepath: pathlib.Path, module_file_path: str) -> pathl
70
85
  return module_path.relative_to(cfg_filepath.parent.absolute())
71
86
 
72
87
 
73
- def get_job_function(job_config: JobConfig, cfg_filepath: pathlib.Path) -> JobFunction:
88
+ def get_job_wrapper(job_config: JobConfig, cfg_filepath: pathlib.Path) -> JobFunction:
74
89
  """Given a job-description dynamically loads the job-function so we can call it.
75
90
 
76
91
  Side-effects: also re-addressed the job-config.
runem/log.py CHANGED
@@ -1,8 +1,11 @@
1
+ import typing
1
2
 
2
- def log(msg:str="", decorate:bool=True, end: str|None=None)->None:
3
- """thin wrapper around 'print', so we can change the output
4
-
5
- One way we change it is to decorate the output with 'runem'"""
3
+
4
+ def log(msg: str = "", decorate: bool = True, end: typing.Optional[str] = None) -> None:
5
+ """Thin wrapper around 'print', so we can change the output.
6
+
7
+ One way we change it is to decorate the output with 'runem'
8
+ """
6
9
  if decorate:
7
10
  msg = f"runem: {msg}"
8
- print(msg, end=end)
11
+ print(msg, end=end)
runem/report.py CHANGED
@@ -26,7 +26,10 @@ def _plot_times(
26
26
  phase_run_oder: OrderedPhases,
27
27
  timing_data: JobRunTimesByPhase,
28
28
  ) -> timedelta:
29
- """Prints a report to terminal on how well we performed."""
29
+ """Prints a report to terminal on how well we performed.
30
+
31
+ Also calculates the wall-clock time-saved for the user.
32
+ """
30
33
  labels: typing.List[str] = []
31
34
  times: typing.List[float] = []
32
35
  job_time_sum: timedelta = timedelta() # init to 0
@@ -66,12 +69,31 @@ def _plot_times(
66
69
  return time_saved
67
70
 
68
71
 
72
+ def _print_reports_by_phase(
73
+ phase_run_oder: OrderedPhases, report_data: JobRunReportByPhase
74
+ ) -> None:
75
+ """Logs out the reports by grouped by phase."""
76
+ for phase in phase_run_oder:
77
+ report_urls: ReportUrls = report_data[phase]
78
+ job_report_url_info: ReportUrlInfo
79
+ for job_report_url_info in report_urls:
80
+ if not job_report_url_info:
81
+ continue
82
+ log(f"report: {str(job_report_url_info[0])}: {str(job_report_url_info[1])}")
83
+
84
+
69
85
  def report_on_run(
70
86
  phase_run_oder: OrderedPhases,
71
87
  job_run_metadatas: JobRunMetadatasByPhase,
72
88
  overall_runtime: timedelta,
73
- ):
89
+ ) -> timedelta:
90
+ """Generate high-level reports AND prints out any reports returned by jobs.
91
+
92
+ IMPORTANT: returns the wall-clock time saved to the user.
93
+ """
74
94
  log("reports:")
95
+
96
+ # First, collate all data, timing and reports
75
97
  timing_data: JobRunTimesByPhase = defaultdict(list)
76
98
  report_data: JobRunReportByPhase = defaultdict(list)
77
99
  phase: PhaseName
@@ -81,17 +103,21 @@ def report_on_run(
81
103
  for timing, reports in job_run_metadatas[phase]:
82
104
  timing_data[phase].append(timing)
83
105
  if reports:
106
+ # the job returned some report urls, record them against the
107
+ # job's phase
84
108
  report_data[phase].extend(reports["reportUrls"])
109
+
110
+ # Now plot the times on the terminal to give a visual report of the timing.
111
+ # Also, calculate the time saved by runem, a key selling-point metric
85
112
  time_saved: timedelta = _plot_times(
86
113
  overall_run_time=overall_runtime,
87
114
  phase_run_oder=phase_run_oder,
88
115
  timing_data=timing_data,
89
116
  )
90
- for phase in phase_run_oder:
91
- report_urls: ReportUrls = report_data[phase]
92
- job_report_url_info: ReportUrlInfo
93
- for job_report_url_info in report_urls:
94
- if not job_report_url_info:
95
- continue
96
- log(f"report: {str(job_report_url_info[0])}: {str(job_report_url_info[1])}")
117
+
118
+ # Penultimate-ly print out the available reports grouped by run-phase.
119
+ _print_reports_by_phase(phase_run_oder, report_data)
120
+
121
+ # Return the key metric for runem, the wall-clock time saved to the user
122
+ # TODO: write this to disk
97
123
  return time_saved
runem/runem.py CHANGED
@@ -23,19 +23,23 @@ import multiprocessing
23
23
  import os
24
24
  import pathlib
25
25
  import sys
26
+ import time
26
27
  import typing
27
28
  from collections import defaultdict
28
29
  from datetime import timedelta
29
30
  from itertools import repeat
31
+ from multiprocessing.managers import DictProxy, ValueProxy
30
32
  from timeit import default_timer as timer
31
33
 
34
+ from halo import Halo
35
+
32
36
  from runem.command_line import parse_args
33
37
  from runem.config import load_config
34
38
  from runem.config_metadata import ConfigMetadata
35
39
  from runem.config_parse import parse_config
36
40
  from runem.files import find_files
41
+ from runem.job_execute import job_execute
37
42
  from runem.job_filter import filter_jobs
38
- from runem.job_runner import job_runner
39
43
  from runem.log import log
40
44
  from runem.report import report_on_run
41
45
  from runem.types import (
@@ -75,6 +79,22 @@ def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
75
79
  return config_metadata
76
80
 
77
81
 
82
+ def _progress_updater(
83
+ label: str, running_jobs: typing.Dict[str, str], is_running: ValueProxy
84
+ ) -> None:
85
+ spinner = Halo(text="", spinner="dots")
86
+ spinner.start()
87
+
88
+ while is_running.value:
89
+ running_job_names: typing.List[str] = [
90
+ f"'{job}'" for job in sorted(list(running_jobs.values()))
91
+ ]
92
+ printable_jobs: str = ", ".join(running_job_names)
93
+ spinner.text = f"{label}: {printable_jobs}"
94
+ time.sleep(0.1)
95
+ spinner.stop()
96
+
97
+
78
98
  def _process_jobs(
79
99
  config_metadata: ConfigMetadata,
80
100
  file_lists: FilePathListLookup,
@@ -91,28 +111,44 @@ def _process_jobs(
91
111
  it and, for instance, run the longest-running job first with quicker
92
112
  jobs completing around it, then we would work out that schedule here.
93
113
  """
94
- max_num_concurrent_procs: int = (
114
+ num_concurrent_procs: int = (
95
115
  config_metadata.args.procs
96
116
  if config_metadata.args.procs != -1
97
117
  else multiprocessing.cpu_count()
98
118
  )
99
- num_concurrent_procs:int = min(max_num_concurrent_procs, len(jobs))
119
+ num_concurrent_procs = min(num_concurrent_procs, len(jobs))
100
120
  log(
101
121
  (
102
- f"Running '{phase}' with {num_concurrent_procs} workers (of "
103
- f"{max_num_concurrent_procs} max) processing {len(jobs)} jobs"
122
+ f"Running '{phase}' with {num_concurrent_procs} workers "
123
+ f"processing {len(jobs)} jobs"
104
124
  )
105
125
  )
106
- with multiprocessing.Pool(processes=num_concurrent_procs) as pool:
107
- # use starmap so we can pass down the job-configs and the args and the files
108
- in_out_job_run_metadatas[phase] = pool.starmap(
109
- job_runner,
110
- zip(
111
- jobs,
112
- repeat(config_metadata),
113
- repeat(file_lists),
114
- ),
126
+
127
+ with multiprocessing.Manager() as manager:
128
+ running_jobs: DictProxy[typing.Any, typing.Any] = manager.dict()
129
+ is_running: ValueProxy = manager.Value("b", True)
130
+
131
+ terminal_writer_process = multiprocessing.Process(
132
+ target=_progress_updater, args=(phase, running_jobs, is_running)
115
133
  )
134
+ terminal_writer_process.start()
135
+
136
+ try:
137
+ with multiprocessing.Pool(processes=num_concurrent_procs) as pool:
138
+ # use starmap so we can pass down the job-configs and the args and the files
139
+ in_out_job_run_metadatas[phase] = pool.starmap(
140
+ job_execute,
141
+ zip(
142
+ jobs,
143
+ repeat(running_jobs),
144
+ repeat(config_metadata),
145
+ repeat(file_lists),
146
+ ),
147
+ )
148
+ finally:
149
+ # Signal the terminal_writer process to exit
150
+ is_running.value = False
151
+ terminal_writer_process.join()
116
152
 
117
153
 
118
154
  def _process_jobs_by_phase(
runem/types.py CHANGED
@@ -18,8 +18,9 @@ JobPhases = typing.Set[str]
18
18
  JobTags = typing.Set[JobTag]
19
19
  PhaseName = str
20
20
  OrderedPhases = typing.Tuple[PhaseName, ...]
21
- ReportUrl = str | pathlib.Path
22
- ReportUrlInfo = typing.Tuple[str, ReportUrl]
21
+ ReportName = str
22
+ ReportUrl = typing.Union[str, pathlib.Path]
23
+ ReportUrlInfo = typing.Tuple[ReportName, ReportUrl]
23
24
  ReportUrls = typing.List[ReportUrlInfo]
24
25
 
25
26
 
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: runem
3
- Version: 0.0.17
3
+ Version: 0.0.18
4
4
  Summary: Awesome runem created by lursight
5
5
  Home-page: https://github.com/lursight/runem/
6
6
  Author: lursight
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
+ Requires-Dist: halo
9
10
  Requires-Dist: PyYAML
10
11
  Provides-Extra: test
11
12
  Requires-Dist: black ==23.11.0 ; extra == 'test'
@@ -0,0 +1,25 @@
1
+ runem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ runem/__main__.py,sha256=dsOiVZegpfK9JOs5n7UmbX5iwwbj7iFkEbLoVeEgAn4,136
3
+ runem/base.py,sha256=EZfR7FIlwEdU9Vfe47Wk2DOO8GQqpKxxLNKp6YHueZ4,316
4
+ runem/cli.py,sha256=Hz9j6Iy1SWjW2uZlQZp0eX48NDWD1-QhIqlBzr_qz0s,125
5
+ runem/command_line.py,sha256=lb1tvYOEVYpB_bUTRL8KxjLFuV4vKn8btFKm-28GMu0,9464
6
+ runem/config.py,sha256=h0r2WYUlAgdokUS1VU1sjFHgrN_cEq339ph4Kn1IBBM,1937
7
+ runem/config_metadata.py,sha256=EjCEqx9-2mtMrFf3lJJ8HFhfmScioZKeY_c9Rzajne8,2836
8
+ runem/config_parse.py,sha256=gso5Lziw8yII5jGSx8fR-IAs7nxUzhzitnRAEXsg5f8,5157
9
+ runem/files.py,sha256=vAI17m-Y1tRGot6_vDpTuBTkKm8xdByGoh7HCfNkMFU,1900
10
+ runem/job_execute.py,sha256=x-IGTG5KYnTJtCYm4jP8_WOQ_UM_BAiG5b3UUKp527A,3042
11
+ runem/job_filter.py,sha256=nltSRK1IKp2fuTCHOZFcii0FBL-R3RKvrnyWXTPjoMw,3478
12
+ runem/job_wrapper_python.py,sha256=TG9bzDe1dgTnsrXrCPd6LzN8qbMkH4rEv_vXC_zlJc0,4226
13
+ runem/log.py,sha256=m1lI4V8ygM53pY4Go4eEzvEJY8srVoItxUNhdp_Vrqg,314
14
+ runem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ runem/report.py,sha256=AyD2yT_8ODo5QRuFkVFIecp6ycT4XElJbleWviZbCfI,4032
16
+ runem/run_command.py,sha256=6QxH8pb4Lug-d0MCP2lBKq2TJlQloxRlq0hYJM6IScc,4528
17
+ runem/runem.py,sha256=Q5ryZtArAGuTlHF07aobJkRIzurKIUsMSUPzEksL6LY,8227
18
+ runem/types.py,sha256=Hw7G7ut7yNaoc3XiDNKeM-cAU-DDu3fDOne13FrutS0,5374
19
+ runem/utils.py,sha256=3N_kel9LsriiMq7kOjT14XhfxUOgz4hdDg97wlLKm3U,221
20
+ runem-0.0.18.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
21
+ runem-0.0.18.dist-info/METADATA,sha256=VzkG4gPaopCtwCnEx1XRGLk9iNWpLPjl4x8BY-JvSSA,15909
22
+ runem-0.0.18.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
23
+ runem-0.0.18.dist-info/entry_points.txt,sha256=nu0g_vBeuPihYtimbtlNusxWovylMppvJ8UxdJlJfvM,46
24
+ runem-0.0.18.dist-info/top_level.txt,sha256=gK6iqh9OfHDDpErioCC9ul_zx2Q5zWTALtcuGU7Vil4,6
25
+ runem-0.0.18.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- runem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- runem/__main__.py,sha256=dsOiVZegpfK9JOs5n7UmbX5iwwbj7iFkEbLoVeEgAn4,136
3
- runem/base.py,sha256=EZfR7FIlwEdU9Vfe47Wk2DOO8GQqpKxxLNKp6YHueZ4,316
4
- runem/cli.py,sha256=Hz9j6Iy1SWjW2uZlQZp0eX48NDWD1-QhIqlBzr_qz0s,125
5
- runem/command_line.py,sha256=4h5YL_eyDKH472BxMeQi9ONjxlGKpe4rI7Ny09kY2Y0,9445
6
- runem/config.py,sha256=h0r2WYUlAgdokUS1VU1sjFHgrN_cEq339ph4Kn1IBBM,1937
7
- runem/config_metadata.py,sha256=EjCEqx9-2mtMrFf3lJJ8HFhfmScioZKeY_c9Rzajne8,2836
8
- runem/config_parse.py,sha256=s7Ty7i-KwktFg2LdLWoJaxRXnWCCT19hVGYbwgwBLp8,5160
9
- runem/files.py,sha256=vAI17m-Y1tRGot6_vDpTuBTkKm8xdByGoh7HCfNkMFU,1900
10
- runem/job_filter.py,sha256=90gY-p29vEb17v8R1CCgBkfQs4oDRTgvBdT9YZBiWSA,3391
11
- runem/job_function_python.py,sha256=iNRKQp0Rlkh1VektOxUSa_KqrrY0yB7jDChcRt-s6oQ,3754
12
- runem/job_runner.py,sha256=FUnNinHXQJAT-0OCD4rWJ81rwqsPwGAzuejvr3bHIL0,2852
13
- runem/log.py,sha256=tzpOW5UOwq6u56v56U4tR1_00FMZIiRYHNg11pG-vv8,275
14
- runem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- runem/report.py,sha256=d3wPg_fqefaMTW1EQTU74zo4-qtPiF-MfWPKH3Mokbg,3085
16
- runem/run_command.py,sha256=6QxH8pb4Lug-d0MCP2lBKq2TJlQloxRlq0hYJM6IScc,4528
17
- runem/runem.py,sha256=_Xr4tITqpgYAI_Ty2ZrWFtq0_5328840NSxHTTUOiYI,7031
18
- runem/types.py,sha256=IxUDGHOQZUc86OPjDHDKwrcBFr78v-MNvTpPwUzWoaI,5337
19
- runem/utils.py,sha256=3N_kel9LsriiMq7kOjT14XhfxUOgz4hdDg97wlLKm3U,221
20
- runem-0.0.17.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
21
- runem-0.0.17.dist-info/METADATA,sha256=kt13DdIGd0yuQenTfPPKovO0-xz5K7h5LzjlP_H2czc,15889
22
- runem-0.0.17.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
23
- runem-0.0.17.dist-info/entry_points.txt,sha256=nu0g_vBeuPihYtimbtlNusxWovylMppvJ8UxdJlJfvM,46
24
- runem-0.0.17.dist-info/top_level.txt,sha256=gK6iqh9OfHDDpErioCC9ul_zx2Q5zWTALtcuGU7Vil4,6
25
- runem-0.0.17.dist-info/RECORD,,
File without changes