hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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.
- hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/sdk/__init__.py +21 -15
- hpcflow/sdk/app.py +2133 -770
- hpcflow/sdk/cli.py +281 -250
- hpcflow/sdk/cli_common.py +6 -2
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +77 -42
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +578 -311
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +112 -85
- hpcflow/sdk/config/types.py +145 -0
- hpcflow/sdk/core/actions.py +1054 -994
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +81 -63
- hpcflow/sdk/core/command_files.py +275 -185
- hpcflow/sdk/core/commands.py +111 -107
- hpcflow/sdk/core/element.py +724 -503
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +398 -51
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +380 -334
- hpcflow/sdk/core/loop_cache.py +160 -43
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +728 -600
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +33 -22
- hpcflow/sdk/core/task.py +1546 -1325
- hpcflow/sdk/core/task_schema.py +240 -196
- hpcflow/sdk/core/test_utils.py +126 -88
- hpcflow/sdk/core/types.py +387 -0
- hpcflow/sdk/core/utils.py +410 -305
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +1192 -1028
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/demo/cli.py +46 -33
- hpcflow/sdk/helper/cli.py +18 -16
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +83 -59
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +988 -586
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +408 -153
- hpcflow/sdk/persistence/pending.py +158 -123
- hpcflow/sdk/persistence/store_resource.py +37 -22
- hpcflow/sdk/persistence/types.py +307 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +477 -420
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +444 -404
- hpcflow/sdk/submission/schedulers/__init__.py +133 -40
- hpcflow/sdk/submission/schedulers/direct.py +97 -71
- hpcflow/sdk/submission/schedulers/sge.py +132 -126
- hpcflow/sdk/submission/schedulers/slurm.py +263 -268
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +102 -29
- hpcflow/sdk/submission/shells/bash.py +72 -55
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +37 -29
- hpcflow/sdk/submission/submission.py +203 -257
- hpcflow/sdk/submission/types.py +143 -0
- hpcflow/sdk/typing.py +163 -12
- hpcflow/tests/conftest.py +8 -6
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_main_scripts.py +60 -30
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
- hpcflow/tests/unit/test_action.py +86 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +13 -6
- hpcflow/tests/unit/test_cli.py +1 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +20 -15
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +3 -1
- hpcflow/tests/unit/test_element_set.py +29 -19
- hpcflow/tests/unit/test_group.py +4 -2
- hpcflow/tests/unit/test_input_source.py +116 -93
- hpcflow/tests/unit/test_input_value.py +29 -24
- hpcflow/tests/unit/test_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +65 -58
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +16 -7
- hpcflow/tests/unit/test_persistence.py +48 -35
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +8 -3
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +3 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +39 -19
- hpcflow/tests/unit/test_task.py +352 -247
- hpcflow/tests/unit/test_task_schema.py +33 -20
- hpcflow/tests/unit/test_utils.py +9 -11
- hpcflow/tests/unit/test_value_sequence.py +15 -12
- hpcflow/tests/unit/test_workflow.py +114 -83
- hpcflow/tests/unit/test_workflow_template.py +0 -1
- hpcflow/tests/workflows/test_jobscript.py +2 -1
- hpcflow/tests/workflows/test_workflows.py +18 -13
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
- hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
- hpcflow/sdk/core/parallel.py +0 -21
- hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/helper/helper.py
CHANGED
@@ -2,20 +2,24 @@
|
|
2
2
|
Implementation of a helper process used to monitor jobs.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
5
6
|
from datetime import datetime, timedelta
|
6
7
|
import logging
|
7
8
|
from logging.handlers import RotatingFileHandler
|
8
9
|
import os
|
9
10
|
from pathlib import Path
|
10
11
|
import signal
|
11
|
-
import socket
|
12
12
|
import subprocess
|
13
13
|
import sys
|
14
14
|
import time
|
15
|
-
|
15
|
+
from typing import Any, TYPE_CHECKING
|
16
16
|
import psutil
|
17
17
|
|
18
|
-
from .watcher import MonitorController
|
18
|
+
from hpcflow.sdk.helper.watcher import MonitorController
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from collections.abc import Callable
|
22
|
+
from ..app import BaseApp
|
19
23
|
|
20
24
|
|
21
25
|
DEFAULT_TIMEOUT = 3600 # seconds
|
@@ -24,12 +28,23 @@ DEFAULT_WATCH_INTERVAL = 10 # seconds
|
|
24
28
|
|
25
29
|
|
26
30
|
def kill_proc_tree(
|
27
|
-
pid
|
28
|
-
|
31
|
+
pid: int,
|
32
|
+
sig=signal.SIGTERM,
|
33
|
+
include_parent: bool = True,
|
34
|
+
timeout: float | None = None,
|
35
|
+
on_terminate: Callable[[psutil.Process], object] | None = None,
|
36
|
+
) -> tuple[list[psutil.Process], list[psutil.Process]]:
|
29
37
|
"""Kill a process tree (including grandchildren) with signal
|
30
|
-
|
31
|
-
|
38
|
+
`sig` and return a (gone, still_alive) tuple.
|
39
|
+
`on_terminate`, if specified, is a callback function which is
|
32
40
|
called as soon as a child terminates.
|
41
|
+
|
42
|
+
Returns
|
43
|
+
-------
|
44
|
+
list[Process]:
|
45
|
+
The process and subprocesses that have died.
|
46
|
+
list[Process]:
|
47
|
+
The process and subprocesses that are still alive.
|
33
48
|
"""
|
34
49
|
assert pid != os.getpid(), "won't kill myself"
|
35
50
|
parent = psutil.Process(pid)
|
@@ -41,39 +56,40 @@ def kill_proc_tree(
|
|
41
56
|
p.send_signal(sig)
|
42
57
|
except psutil.NoSuchProcess:
|
43
58
|
pass
|
44
|
-
|
45
|
-
return (gone, alive)
|
59
|
+
return psutil.wait_procs(children, timeout=timeout, callback=on_terminate)
|
46
60
|
|
47
61
|
|
48
|
-
def get_PID_file_path(app):
|
62
|
+
def get_PID_file_path(app: BaseApp) -> Path:
|
49
63
|
"""Get the path to the file containing the process ID of the helper, if running."""
|
50
64
|
return app.user_data_dir / "pid.txt"
|
51
65
|
|
52
66
|
|
53
|
-
def get_watcher_file_path(app):
|
67
|
+
def get_watcher_file_path(app: BaseApp) -> Path:
|
54
68
|
"""Get the path to the watcher file, which contains a list of workflows to watch."""
|
55
69
|
return app.user_data_dir / "watch_workflows.txt"
|
56
70
|
|
57
71
|
|
58
|
-
def get_helper_log_path(app):
|
72
|
+
def get_helper_log_path(app: BaseApp) -> Path:
|
59
73
|
"""Get the log file path for the helper."""
|
60
74
|
return app.user_data_dir / "helper.log"
|
61
75
|
|
62
76
|
|
63
|
-
def get_helper_watch_list(app):
|
77
|
+
def get_helper_watch_list(app: BaseApp):
|
64
78
|
"""Get the list of workflows currently being watched by the helper process."""
|
65
|
-
logger = get_helper_logger(app)
|
66
79
|
watch_file_path = get_watcher_file_path(app)
|
67
80
|
if watch_file_path.exists():
|
68
|
-
return MonitorController.parse_watch_workflows_file(
|
81
|
+
return MonitorController.parse_watch_workflows_file(
|
82
|
+
watch_file_path, get_helper_logger(app)
|
83
|
+
)
|
84
|
+
return None
|
69
85
|
|
70
86
|
|
71
87
|
def start_helper(
|
72
|
-
app,
|
73
|
-
timeout=DEFAULT_TIMEOUT,
|
74
|
-
timeout_check_interval=DEFAULT_TIMEOUT_CHECK,
|
75
|
-
watch_interval=DEFAULT_WATCH_INTERVAL,
|
76
|
-
logger=None,
|
88
|
+
app: BaseApp,
|
89
|
+
timeout: timedelta | float = DEFAULT_TIMEOUT,
|
90
|
+
timeout_check_interval: timedelta | float = DEFAULT_TIMEOUT_CHECK,
|
91
|
+
watch_interval: timedelta | float = DEFAULT_WATCH_INTERVAL,
|
92
|
+
logger: logging.Logger | None = None,
|
77
93
|
):
|
78
94
|
"""
|
79
95
|
Start the helper process.
|
@@ -90,9 +106,9 @@ def start_helper(
|
|
90
106
|
f"Starting helper with timeout={timeout!r}, timeout_check_interval="
|
91
107
|
f"{timeout_check_interval!r} and watch_interval={watch_interval!r}."
|
92
108
|
)
|
93
|
-
kwargs = {}
|
109
|
+
kwargs: dict[str, Any] = {}
|
94
110
|
if os.name == "nt":
|
95
|
-
kwargs =
|
111
|
+
kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
96
112
|
|
97
113
|
if isinstance(timeout, timedelta):
|
98
114
|
timeout = timeout.total_seconds()
|
@@ -101,8 +117,8 @@ def start_helper(
|
|
101
117
|
if isinstance(watch_interval, timedelta):
|
102
118
|
watch_interval = watch_interval.total_seconds()
|
103
119
|
|
104
|
-
args =
|
105
|
-
|
120
|
+
args = [
|
121
|
+
*app.run_time_info.invocation_command,
|
106
122
|
"--config-dir",
|
107
123
|
str(app.config.config_directory),
|
108
124
|
"helper",
|
@@ -137,10 +153,10 @@ def start_helper(
|
|
137
153
|
|
138
154
|
|
139
155
|
def restart_helper(
|
140
|
-
app,
|
141
|
-
timeout=DEFAULT_TIMEOUT,
|
142
|
-
timeout_check_interval=DEFAULT_TIMEOUT_CHECK,
|
143
|
-
watch_interval=DEFAULT_WATCH_INTERVAL,
|
156
|
+
app: BaseApp,
|
157
|
+
timeout: timedelta | float = DEFAULT_TIMEOUT,
|
158
|
+
timeout_check_interval: timedelta | float = DEFAULT_TIMEOUT_CHECK,
|
159
|
+
watch_interval: timedelta | float = DEFAULT_WATCH_INTERVAL,
|
144
160
|
):
|
145
161
|
"""
|
146
162
|
Restart the helper process.
|
@@ -149,7 +165,7 @@ def restart_helper(
|
|
149
165
|
start_helper(app, timeout, timeout_check_interval, watch_interval, logger=logger)
|
150
166
|
|
151
167
|
|
152
|
-
def get_helper_PID(app):
|
168
|
+
def get_helper_PID(app: BaseApp):
|
153
169
|
"""
|
154
170
|
Get the process ID of the helper process.
|
155
171
|
"""
|
@@ -157,20 +173,18 @@ def get_helper_PID(app):
|
|
157
173
|
if not PID_file.is_file():
|
158
174
|
print("Helper not running!")
|
159
175
|
return None
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
return helper_pid, PID_file
|
176
|
+
with PID_file.open("rt") as fp:
|
177
|
+
helper_pid = int(fp.read().strip())
|
178
|
+
return helper_pid, PID_file
|
164
179
|
|
165
180
|
|
166
|
-
def stop_helper(app, return_logger=False):
|
181
|
+
def stop_helper(app: BaseApp, return_logger: bool = False):
|
167
182
|
"""
|
168
183
|
Stop the helper process.
|
169
184
|
"""
|
170
185
|
logger = get_helper_logger(app)
|
171
|
-
pid_info
|
172
|
-
|
173
|
-
logger.info(f"Stopping helper.")
|
186
|
+
if pid_info := get_helper_PID(app):
|
187
|
+
logger.info("Stopping helper.")
|
174
188
|
pid, pid_file = pid_info
|
175
189
|
kill_proc_tree(pid=pid)
|
176
190
|
pid_file.unlink()
|
@@ -179,37 +193,34 @@ def stop_helper(app, return_logger=False):
|
|
179
193
|
logger.info(f"Deleting watcher file: {str(workflow_dirs_file_path)}")
|
180
194
|
workflow_dirs_file_path.unlink()
|
181
195
|
|
182
|
-
if return_logger
|
183
|
-
return logger
|
196
|
+
return logger if return_logger else None
|
184
197
|
|
185
198
|
|
186
|
-
def clear_helper(app):
|
199
|
+
def clear_helper(app: BaseApp):
|
187
200
|
"""
|
188
201
|
Stop the helper or remove any stale information relating to it.
|
189
202
|
"""
|
190
203
|
try:
|
191
204
|
stop_helper(app)
|
192
205
|
except psutil.NoSuchProcess:
|
193
|
-
pid_info
|
194
|
-
if pid_info:
|
206
|
+
if pid_info := get_helper_PID(app):
|
195
207
|
pid_file = pid_info[1]
|
196
208
|
print(f"Removing file {pid_file!r}")
|
197
209
|
pid_file.unlink()
|
198
210
|
|
199
211
|
|
200
|
-
def get_helper_uptime(app):
|
212
|
+
def get_helper_uptime(app: BaseApp) -> None | timedelta:
|
201
213
|
"""
|
202
214
|
Get the amount of time that the helper has been running.
|
203
215
|
"""
|
204
|
-
pid_info
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
return uptime
|
216
|
+
if not (pid_info := get_helper_PID(app)):
|
217
|
+
return None
|
218
|
+
proc = psutil.Process(pid_info[0])
|
219
|
+
create_time = datetime.fromtimestamp(proc.create_time())
|
220
|
+
return datetime.now() - create_time
|
210
221
|
|
211
222
|
|
212
|
-
def get_helper_logger(app):
|
223
|
+
def get_helper_logger(app: BaseApp) -> logging.Logger:
|
213
224
|
"""
|
214
225
|
Get the logger for helper-related messages.
|
215
226
|
"""
|
@@ -224,17 +235,21 @@ def get_helper_logger(app):
|
|
224
235
|
return logger
|
225
236
|
|
226
237
|
|
227
|
-
def helper_timeout(
|
238
|
+
def helper_timeout(
|
239
|
+
app: BaseApp,
|
240
|
+
timeout: timedelta,
|
241
|
+
controller: MonitorController,
|
242
|
+
logger: logging.Logger,
|
243
|
+
):
|
228
244
|
"""Kill the helper due to running duration exceeding the timeout."""
|
229
245
|
|
230
246
|
logger.info(f"Helper exiting due to timeout ({timeout!r}).")
|
231
|
-
pid_info
|
232
|
-
if pid_info:
|
247
|
+
if pid_info := get_helper_PID(app):
|
233
248
|
pid_file = pid_info[1]
|
234
249
|
logger.info(f"Deleting PID file: {pid_file!r}.")
|
235
250
|
pid_file.unlink()
|
236
251
|
|
237
|
-
logger.info(
|
252
|
+
logger.info("Stopping all watchers.")
|
238
253
|
controller.stop()
|
239
254
|
controller.join()
|
240
255
|
|
@@ -245,10 +260,10 @@ def helper_timeout(app, timeout, controller, logger):
|
|
245
260
|
|
246
261
|
|
247
262
|
def run_helper(
|
248
|
-
app,
|
249
|
-
timeout=DEFAULT_TIMEOUT,
|
250
|
-
timeout_check_interval=DEFAULT_TIMEOUT_CHECK,
|
251
|
-
watch_interval=DEFAULT_WATCH_INTERVAL,
|
263
|
+
app: BaseApp,
|
264
|
+
timeout: timedelta | float = DEFAULT_TIMEOUT,
|
265
|
+
timeout_check_interval: timedelta | float = DEFAULT_TIMEOUT_CHECK,
|
266
|
+
watch_interval: timedelta | float = DEFAULT_WATCH_INTERVAL,
|
252
267
|
):
|
253
268
|
"""
|
254
269
|
Run the helper core.
|
@@ -260,11 +275,8 @@ def run_helper(
|
|
260
275
|
# TODO: we will want to set the timeout to be slightly more than the largest allowable
|
261
276
|
# walltime in the case of scheduler submissions.
|
262
277
|
|
263
|
-
if isinstance(timeout, timedelta):
|
264
|
-
|
265
|
-
else:
|
266
|
-
timeout_s = timeout
|
267
|
-
timeout = timedelta(seconds=timeout_s)
|
278
|
+
if not isinstance(timeout, timedelta):
|
279
|
+
timeout = timedelta(seconds=timeout)
|
268
280
|
|
269
281
|
if isinstance(timeout_check_interval, timedelta):
|
270
282
|
timeout_check_interval_s = timeout_check_interval.total_seconds()
|
hpcflow/sdk/helper/watcher.py
CHANGED
@@ -2,10 +2,34 @@
|
|
2
2
|
File-system watcher classes.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
6
|
+
from collections.abc import Callable
|
5
7
|
from datetime import timedelta
|
8
|
+
from logging import Logger
|
6
9
|
from pathlib import Path
|
7
10
|
from watchdog.observers.polling import PollingObserver
|
8
|
-
from watchdog.events import
|
11
|
+
from watchdog.events import (
|
12
|
+
FileSystemEvent,
|
13
|
+
FileSystemEventHandler,
|
14
|
+
PatternMatchingEventHandler,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
class _PMEHDelegate(PatternMatchingEventHandler):
|
19
|
+
def __init__(self, pattern: str, on_modified: Callable[[FileSystemEvent], None]):
|
20
|
+
super().__init__([pattern])
|
21
|
+
self.__on_modified = on_modified
|
22
|
+
|
23
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
24
|
+
self.__on_modified(event)
|
25
|
+
|
26
|
+
|
27
|
+
class _FSEHDelegate(FileSystemEventHandler):
|
28
|
+
def __init__(self, on_modified: Callable[[FileSystemEvent], None]) -> None:
|
29
|
+
self.__on_modified = on_modified
|
30
|
+
|
31
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
32
|
+
self.__on_modified(event)
|
9
33
|
|
10
34
|
|
11
35
|
class MonitorController:
|
@@ -13,12 +37,16 @@ class MonitorController:
|
|
13
37
|
Controller for tracking watch files.
|
14
38
|
"""
|
15
39
|
|
16
|
-
def __init__(
|
17
|
-
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
workflow_dirs_file_path: str | Path,
|
43
|
+
watch_interval: float | timedelta,
|
44
|
+
logger: Logger,
|
45
|
+
):
|
18
46
|
if isinstance(watch_interval, timedelta):
|
19
|
-
watch_interval = watch_interval.total_seconds()
|
20
|
-
|
21
|
-
|
47
|
+
self.watch_interval = int(watch_interval.total_seconds())
|
48
|
+
else:
|
49
|
+
self.watch_interval = int(watch_interval)
|
22
50
|
self.workflow_dirs_file_path = Path(workflow_dirs_file_path).absolute()
|
23
51
|
self.logger = logger
|
24
52
|
|
@@ -31,8 +59,7 @@ class MonitorController:
|
|
31
59
|
|
32
60
|
self.logger.info(f"Watching file: {str(self.workflow_dirs_file_path)}")
|
33
61
|
|
34
|
-
self.event_handler =
|
35
|
-
self.event_handler.on_modified = self.on_modified
|
62
|
+
self.event_handler = _PMEHDelegate("watch_workflows.txt", self.on_modified)
|
36
63
|
|
37
64
|
self.observer = PollingObserver(timeout=self.watch_interval)
|
38
65
|
self.observer.schedule(
|
@@ -53,7 +80,9 @@ class MonitorController:
|
|
53
80
|
)
|
54
81
|
|
55
82
|
@staticmethod
|
56
|
-
def parse_watch_workflows_file(
|
83
|
+
def parse_watch_workflows_file(
|
84
|
+
path: str | Path, logger: Logger
|
85
|
+
) -> list[dict[str, Path]]:
|
57
86
|
"""
|
58
87
|
Parse the file describing what workflows to watch.
|
59
88
|
"""
|
@@ -61,7 +90,7 @@ class MonitorController:
|
|
61
90
|
with Path(path).open("rt") as fp:
|
62
91
|
lns = fp.readlines()
|
63
92
|
|
64
|
-
wks = []
|
93
|
+
wks: list[dict[str, Path]] = []
|
65
94
|
for ln in lns:
|
66
95
|
ln_s = ln.strip()
|
67
96
|
if not ln_s:
|
@@ -79,7 +108,7 @@ class MonitorController:
|
|
79
108
|
|
80
109
|
return wks
|
81
110
|
|
82
|
-
def on_modified(self, event):
|
111
|
+
def on_modified(self, event: FileSystemEvent):
|
83
112
|
"""
|
84
113
|
Callback when files are modified.
|
85
114
|
"""
|
@@ -87,13 +116,13 @@ class MonitorController:
|
|
87
116
|
wks = self.parse_watch_workflows_file(event.src_path, logger=self.logger)
|
88
117
|
self.workflow_monitor.update_workflow_paths(wks)
|
89
118
|
|
90
|
-
def join(self):
|
119
|
+
def join(self) -> None:
|
91
120
|
"""
|
92
121
|
Join the worker thread.
|
93
122
|
"""
|
94
123
|
self.observer.join()
|
95
124
|
|
96
|
-
def stop(self):
|
125
|
+
def stop(self) -> None:
|
97
126
|
"""
|
98
127
|
Stop this monitor.
|
99
128
|
"""
|
@@ -107,44 +136,48 @@ class WorkflowMonitor:
|
|
107
136
|
Workflow monitor.
|
108
137
|
"""
|
109
138
|
|
110
|
-
def __init__(
|
111
|
-
|
139
|
+
def __init__(
|
140
|
+
self,
|
141
|
+
workflow_paths: list[dict[str, Path]],
|
142
|
+
watch_interval: float | timedelta,
|
143
|
+
logger: Logger,
|
144
|
+
):
|
112
145
|
if isinstance(watch_interval, timedelta):
|
113
|
-
watch_interval = watch_interval.total_seconds()
|
146
|
+
self.watch_interval = int(watch_interval.total_seconds())
|
147
|
+
else:
|
148
|
+
self.watch_interval = int(watch_interval)
|
114
149
|
|
115
|
-
self.event_handler =
|
116
|
-
self.event_handler.on_modified = self.on_modified
|
150
|
+
self.event_handler = _FSEHDelegate(self.on_modified)
|
117
151
|
self.workflow_paths = workflow_paths
|
118
|
-
self.watch_interval = int(watch_interval)
|
119
152
|
self.logger = logger
|
120
153
|
|
121
154
|
self._monitor_workflow_paths()
|
122
155
|
|
123
|
-
def _monitor_workflow_paths(self):
|
124
|
-
|
125
|
-
self.observer =
|
156
|
+
def _monitor_workflow_paths(self) -> None:
|
157
|
+
observer = PollingObserver(timeout=self.watch_interval)
|
158
|
+
self.observer: PollingObserver | None = observer
|
126
159
|
for i in self.workflow_paths:
|
127
|
-
|
160
|
+
observer.schedule(self.event_handler, path=i["path"], recursive=False)
|
128
161
|
self.logger.info(f"Watching workflow: {i['path'].name}")
|
129
162
|
|
130
|
-
|
163
|
+
observer.start()
|
131
164
|
|
132
|
-
def on_modified(self, event):
|
165
|
+
def on_modified(self, event: FileSystemEvent):
|
133
166
|
"""
|
134
167
|
Triggered on a workflow being modified.
|
135
168
|
"""
|
136
169
|
self.logger.info(f"Workflow modified: {event.src_path}")
|
137
170
|
|
138
|
-
def update_workflow_paths(self, new_paths):
|
171
|
+
def update_workflow_paths(self, new_paths: list[dict[str, Path]]):
|
139
172
|
"""
|
140
173
|
Change the set of paths to monitored workflows.
|
141
174
|
"""
|
142
|
-
self.logger.info(
|
175
|
+
self.logger.info("Updating watched workflows.")
|
143
176
|
self.stop()
|
144
177
|
self.workflow_paths = new_paths
|
145
178
|
self._monitor_workflow_paths()
|
146
179
|
|
147
|
-
def stop(self):
|
180
|
+
def stop(self) -> None:
|
148
181
|
"""
|
149
182
|
Stop this monitor.
|
150
183
|
"""
|