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.
Files changed (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -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, sig=signal.SIGTERM, include_parent=True, timeout=None, on_terminate=None
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
- "sig" and return a (gone, still_alive) tuple.
31
- "on_terminate", if specified, is a callback function which is
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
- gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate)
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(watch_file_path, logger)
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 = {"creationflags": subprocess.CREATE_NO_WINDOW}
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 = app.run_time_info.invocation_command
105
- args += [
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
- else:
161
- with PID_file.open("rt") as fp:
162
- helper_pid = int(fp.read().strip())
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 = get_helper_PID(app)
172
- if pid_info:
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 = get_helper_PID(app)
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 = get_helper_PID(app)
205
- if pid_info:
206
- proc = psutil.Process(pid_info[0])
207
- create_time = datetime.fromtimestamp(proc.create_time())
208
- uptime = datetime.now() - create_time
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(app, timeout, controller, logger):
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 = get_helper_PID(app)
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(f"Stopping all watchers.")
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
- timeout_s = timeout.total_seconds()
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()
@@ -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 FileSystemEventHandler, PatternMatchingEventHandler
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__(self, workflow_dirs_file_path, watch_interval, logger):
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
- self.watch_interval = int(watch_interval)
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 = PatternMatchingEventHandler(patterns=["watch_workflows.txt"])
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(path, logger):
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__(self, workflow_paths, watch_interval, logger):
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 = FileSystemEventHandler()
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 = PollingObserver(timeout=self.watch_interval)
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
- self.observer.schedule(self.event_handler, path=i["path"], recursive=False)
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
- self.observer.start()
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(f"Updating watched workflows.")
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
  """