hpcflow 0.1.15__py3-none-any.whl → 0.2.0a271__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 (275) hide show
  1. hpcflow/__init__.py +2 -11
  2. hpcflow/__pyinstaller/__init__.py +5 -0
  3. hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
  4. hpcflow/_version.py +1 -1
  5. hpcflow/app.py +43 -0
  6. hpcflow/cli.py +2 -461
  7. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  8. hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
  9. hpcflow/data/jinja_templates/test/test_template.txt +8 -0
  10. hpcflow/data/programs/hello_world/README.md +1 -0
  11. hpcflow/data/programs/hello_world/hello_world.c +87 -0
  12. hpcflow/data/programs/hello_world/linux/hello_world +0 -0
  13. hpcflow/data/programs/hello_world/macos/hello_world +0 -0
  14. hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
  15. hpcflow/data/scripts/__init__.py +1 -0
  16. hpcflow/data/scripts/bad_script.py +2 -0
  17. hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
  18. hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
  19. hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
  20. hpcflow/data/scripts/do_nothing.py +2 -0
  21. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  22. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  23. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  24. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  25. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  26. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  27. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  28. hpcflow/data/scripts/generate_t1_file_01.py +7 -0
  29. hpcflow/data/scripts/import_future_script.py +7 -0
  30. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  31. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  32. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  33. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  34. hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
  35. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  36. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  37. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  38. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  39. hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
  40. hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
  41. hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
  42. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  43. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  44. hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
  45. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
  46. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  47. hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
  48. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
  49. hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
  50. hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
  51. hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
  52. hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
  53. hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
  54. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  55. hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
  56. hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
  57. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  58. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  59. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  60. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  61. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  62. hpcflow/data/scripts/parse_t1_file_01.py +4 -0
  63. hpcflow/data/scripts/script_exit_test.py +5 -0
  64. hpcflow/data/template_components/__init__.py +1 -0
  65. hpcflow/data/template_components/command_files.yaml +26 -0
  66. hpcflow/data/template_components/environments.yaml +13 -0
  67. hpcflow/data/template_components/parameters.yaml +14 -0
  68. hpcflow/data/template_components/task_schemas.yaml +139 -0
  69. hpcflow/data/workflows/workflow_1.yaml +5 -0
  70. hpcflow/examples.ipynb +1037 -0
  71. hpcflow/sdk/__init__.py +149 -0
  72. hpcflow/sdk/app.py +4266 -0
  73. hpcflow/sdk/cli.py +1479 -0
  74. hpcflow/sdk/cli_common.py +385 -0
  75. hpcflow/sdk/config/__init__.py +5 -0
  76. hpcflow/sdk/config/callbacks.py +246 -0
  77. hpcflow/sdk/config/cli.py +388 -0
  78. hpcflow/sdk/config/config.py +1410 -0
  79. hpcflow/sdk/config/config_file.py +501 -0
  80. hpcflow/sdk/config/errors.py +272 -0
  81. hpcflow/sdk/config/types.py +150 -0
  82. hpcflow/sdk/core/__init__.py +38 -0
  83. hpcflow/sdk/core/actions.py +3857 -0
  84. hpcflow/sdk/core/app_aware.py +25 -0
  85. hpcflow/sdk/core/cache.py +224 -0
  86. hpcflow/sdk/core/command_files.py +814 -0
  87. hpcflow/sdk/core/commands.py +424 -0
  88. hpcflow/sdk/core/element.py +2071 -0
  89. hpcflow/sdk/core/enums.py +221 -0
  90. hpcflow/sdk/core/environment.py +256 -0
  91. hpcflow/sdk/core/errors.py +1043 -0
  92. hpcflow/sdk/core/execute.py +207 -0
  93. hpcflow/sdk/core/json_like.py +809 -0
  94. hpcflow/sdk/core/loop.py +1320 -0
  95. hpcflow/sdk/core/loop_cache.py +282 -0
  96. hpcflow/sdk/core/object_list.py +933 -0
  97. hpcflow/sdk/core/parameters.py +3371 -0
  98. hpcflow/sdk/core/rule.py +196 -0
  99. hpcflow/sdk/core/run_dir_files.py +57 -0
  100. hpcflow/sdk/core/skip_reason.py +7 -0
  101. hpcflow/sdk/core/task.py +3792 -0
  102. hpcflow/sdk/core/task_schema.py +993 -0
  103. hpcflow/sdk/core/test_utils.py +538 -0
  104. hpcflow/sdk/core/types.py +447 -0
  105. hpcflow/sdk/core/utils.py +1207 -0
  106. hpcflow/sdk/core/validation.py +87 -0
  107. hpcflow/sdk/core/values.py +477 -0
  108. hpcflow/sdk/core/workflow.py +4820 -0
  109. hpcflow/sdk/core/zarr_io.py +206 -0
  110. hpcflow/sdk/data/__init__.py +13 -0
  111. hpcflow/sdk/data/config_file_schema.yaml +34 -0
  112. hpcflow/sdk/data/config_schema.yaml +260 -0
  113. hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
  114. hpcflow/sdk/data/files_spec_schema.yaml +5 -0
  115. hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
  116. hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
  117. hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
  118. hpcflow/sdk/demo/__init__.py +3 -0
  119. hpcflow/sdk/demo/cli.py +242 -0
  120. hpcflow/sdk/helper/__init__.py +3 -0
  121. hpcflow/sdk/helper/cli.py +137 -0
  122. hpcflow/sdk/helper/helper.py +300 -0
  123. hpcflow/sdk/helper/watcher.py +192 -0
  124. hpcflow/sdk/log.py +288 -0
  125. hpcflow/sdk/persistence/__init__.py +18 -0
  126. hpcflow/sdk/persistence/base.py +2817 -0
  127. hpcflow/sdk/persistence/defaults.py +6 -0
  128. hpcflow/sdk/persistence/discovery.py +39 -0
  129. hpcflow/sdk/persistence/json.py +954 -0
  130. hpcflow/sdk/persistence/pending.py +948 -0
  131. hpcflow/sdk/persistence/store_resource.py +203 -0
  132. hpcflow/sdk/persistence/types.py +309 -0
  133. hpcflow/sdk/persistence/utils.py +73 -0
  134. hpcflow/sdk/persistence/zarr.py +2388 -0
  135. hpcflow/sdk/runtime.py +320 -0
  136. hpcflow/sdk/submission/__init__.py +3 -0
  137. hpcflow/sdk/submission/enums.py +70 -0
  138. hpcflow/sdk/submission/jobscript.py +2379 -0
  139. hpcflow/sdk/submission/schedulers/__init__.py +281 -0
  140. hpcflow/sdk/submission/schedulers/direct.py +233 -0
  141. hpcflow/sdk/submission/schedulers/sge.py +376 -0
  142. hpcflow/sdk/submission/schedulers/slurm.py +598 -0
  143. hpcflow/sdk/submission/schedulers/utils.py +25 -0
  144. hpcflow/sdk/submission/shells/__init__.py +52 -0
  145. hpcflow/sdk/submission/shells/base.py +229 -0
  146. hpcflow/sdk/submission/shells/bash.py +504 -0
  147. hpcflow/sdk/submission/shells/os_version.py +115 -0
  148. hpcflow/sdk/submission/shells/powershell.py +352 -0
  149. hpcflow/sdk/submission/submission.py +1402 -0
  150. hpcflow/sdk/submission/types.py +140 -0
  151. hpcflow/sdk/typing.py +194 -0
  152. hpcflow/sdk/utils/arrays.py +69 -0
  153. hpcflow/sdk/utils/deferred_file.py +55 -0
  154. hpcflow/sdk/utils/hashing.py +16 -0
  155. hpcflow/sdk/utils/patches.py +31 -0
  156. hpcflow/sdk/utils/strings.py +69 -0
  157. hpcflow/tests/api/test_api.py +32 -0
  158. hpcflow/tests/conftest.py +123 -0
  159. hpcflow/tests/data/__init__.py +0 -0
  160. hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
  161. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  162. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  163. hpcflow/tests/data/workflow_1.json +10 -0
  164. hpcflow/tests/data/workflow_1.yaml +5 -0
  165. hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
  166. hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
  167. hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
  168. hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
  169. hpcflow/tests/programs/test_programs.py +180 -0
  170. hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
  171. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  172. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
  173. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  174. hpcflow/tests/scripts/test_main_scripts.py +1361 -0
  175. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  176. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  177. hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
  178. hpcflow/tests/unit/test_action.py +1066 -0
  179. hpcflow/tests/unit/test_action_rule.py +24 -0
  180. hpcflow/tests/unit/test_app.py +132 -0
  181. hpcflow/tests/unit/test_cache.py +46 -0
  182. hpcflow/tests/unit/test_cli.py +172 -0
  183. hpcflow/tests/unit/test_command.py +377 -0
  184. hpcflow/tests/unit/test_config.py +195 -0
  185. hpcflow/tests/unit/test_config_file.py +162 -0
  186. hpcflow/tests/unit/test_element.py +666 -0
  187. hpcflow/tests/unit/test_element_iteration.py +88 -0
  188. hpcflow/tests/unit/test_element_set.py +158 -0
  189. hpcflow/tests/unit/test_group.py +115 -0
  190. hpcflow/tests/unit/test_input_source.py +1479 -0
  191. hpcflow/tests/unit/test_input_value.py +398 -0
  192. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  193. hpcflow/tests/unit/test_json_like.py +1247 -0
  194. hpcflow/tests/unit/test_loop.py +2674 -0
  195. hpcflow/tests/unit/test_meta_task.py +325 -0
  196. hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
  197. hpcflow/tests/unit/test_object_list.py +116 -0
  198. hpcflow/tests/unit/test_parameter.py +243 -0
  199. hpcflow/tests/unit/test_persistence.py +664 -0
  200. hpcflow/tests/unit/test_resources.py +243 -0
  201. hpcflow/tests/unit/test_run.py +286 -0
  202. hpcflow/tests/unit/test_run_directories.py +29 -0
  203. hpcflow/tests/unit/test_runtime.py +9 -0
  204. hpcflow/tests/unit/test_schema_input.py +372 -0
  205. hpcflow/tests/unit/test_shell.py +129 -0
  206. hpcflow/tests/unit/test_slurm.py +39 -0
  207. hpcflow/tests/unit/test_submission.py +502 -0
  208. hpcflow/tests/unit/test_task.py +2560 -0
  209. hpcflow/tests/unit/test_task_schema.py +182 -0
  210. hpcflow/tests/unit/test_utils.py +616 -0
  211. hpcflow/tests/unit/test_value_sequence.py +549 -0
  212. hpcflow/tests/unit/test_values.py +91 -0
  213. hpcflow/tests/unit/test_workflow.py +827 -0
  214. hpcflow/tests/unit/test_workflow_template.py +186 -0
  215. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  216. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  217. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  218. hpcflow/tests/unit/utils/test_patches.py +5 -0
  219. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  220. hpcflow/tests/unit/utils/test_strings.py +97 -0
  221. hpcflow/tests/workflows/__init__.py +0 -0
  222. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  223. hpcflow/tests/workflows/test_jobscript.py +355 -0
  224. hpcflow/tests/workflows/test_run_status.py +198 -0
  225. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  226. hpcflow/tests/workflows/test_submission.py +140 -0
  227. hpcflow/tests/workflows/test_workflows.py +564 -0
  228. hpcflow/tests/workflows/test_zip.py +18 -0
  229. hpcflow/viz_demo.ipynb +6794 -0
  230. hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
  231. hpcflow-0.2.0a271.dist-info/METADATA +65 -0
  232. hpcflow-0.2.0a271.dist-info/RECORD +237 -0
  233. {hpcflow-0.1.15.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
  234. hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
  235. hpcflow/api.py +0 -490
  236. hpcflow/archive/archive.py +0 -307
  237. hpcflow/archive/cloud/cloud.py +0 -45
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -427
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -233
  243. hpcflow/copytree.py +0 -66
  244. hpcflow/data/examples/_config.yml +0 -14
  245. hpcflow/data/examples/damask/demo/1.run.yml +0 -4
  246. hpcflow/data/examples/damask/demo/2.process.yml +0 -29
  247. hpcflow/data/examples/damask/demo/geom.geom +0 -2052
  248. hpcflow/data/examples/damask/demo/load.load +0 -1
  249. hpcflow/data/examples/damask/demo/material.config +0 -185
  250. hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
  251. hpcflow/data/examples/damask/inputs/load.load +0 -1
  252. hpcflow/data/examples/damask/inputs/material.config +0 -185
  253. hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
  254. hpcflow/data/examples/damask/profiles/damask.yml +0 -4
  255. hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
  256. hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
  257. hpcflow/data/examples/damask/profiles/default.yml +0 -6
  258. hpcflow/data/examples/thinking.yml +0 -177
  259. hpcflow/errors.py +0 -2
  260. hpcflow/init_db.py +0 -37
  261. hpcflow/models.py +0 -2595
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -322
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -166
  268. hpcflow/variables.py +0 -543
  269. hpcflow-0.1.15.dist-info/METADATA +0 -168
  270. hpcflow-0.1.15.dist-info/RECORD +0 -45
  271. hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.15.dist-info/top_level.txt +0 -1
  273. /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
  274. /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
  275. /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
@@ -0,0 +1,192 @@
1
+ """
2
+ File-system watcher classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from collections.abc import Callable
7
+ from datetime import timedelta
8
+ from logging import Logger
9
+ from pathlib import Path
10
+ from typing import cast
11
+ from watchdog.observers.polling import PollingObserver
12
+ from watchdog.events import (
13
+ FileSystemEvent,
14
+ FileSystemEventHandler,
15
+ PatternMatchingEventHandler,
16
+ )
17
+
18
+
19
+ class _PMEHDelegate(PatternMatchingEventHandler):
20
+ def __init__(self, pattern: str, on_modified: Callable[[FileSystemEvent], None]):
21
+ super().__init__(patterns=[pattern])
22
+ self.__on_modified = on_modified
23
+
24
+ def on_modified(self, event: FileSystemEvent) -> None:
25
+ self.__on_modified(event)
26
+
27
+
28
+ class _FSEHDelegate(FileSystemEventHandler):
29
+ def __init__(self, on_modified: Callable[[FileSystemEvent], None]) -> None:
30
+ self.__on_modified = on_modified
31
+
32
+ def on_modified(self, event: FileSystemEvent) -> None:
33
+ self.__on_modified(event)
34
+
35
+
36
+ class MonitorController:
37
+ """
38
+ Controller for tracking watch files.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ workflow_dirs_file_path: str | Path,
44
+ watch_interval: float | timedelta,
45
+ logger: Logger,
46
+ ):
47
+ if isinstance(watch_interval, timedelta):
48
+ self.watch_interval = int(watch_interval.total_seconds())
49
+ else:
50
+ self.watch_interval = int(watch_interval)
51
+ self.workflow_dirs_file_path = Path(workflow_dirs_file_path).absolute()
52
+ self.logger = logger
53
+
54
+ if not self.workflow_dirs_file_path.exists():
55
+ self.logger.info(
56
+ f"Watch file does not exist; creating {str(self.workflow_dirs_file_path)}."
57
+ )
58
+ with self.workflow_dirs_file_path.open("wt") as fp:
59
+ fp.write("\n")
60
+
61
+ self.logger.info(f"Watching file: {str(self.workflow_dirs_file_path)}")
62
+
63
+ self.event_handler = _PMEHDelegate("watch_workflows.txt", self.on_modified)
64
+
65
+ self.observer = PollingObserver(timeout=self.watch_interval)
66
+ self.observer.schedule(
67
+ self.event_handler,
68
+ path=cast("str", self.workflow_dirs_file_path.parent),
69
+ recursive=False,
70
+ )
71
+
72
+ self.observer.start()
73
+
74
+ workflow_paths = self.parse_watch_workflows_file(
75
+ self.workflow_dirs_file_path, logger=self.logger
76
+ )
77
+ self.workflow_monitor = WorkflowMonitor(
78
+ workflow_paths,
79
+ watch_interval=self.watch_interval,
80
+ logger=self.logger,
81
+ )
82
+
83
+ @staticmethod
84
+ def parse_watch_workflows_file(
85
+ path: str | Path, logger: Logger
86
+ ) -> list[dict[str, Path]]:
87
+ """
88
+ Parse the file describing what workflows to watch.
89
+ """
90
+ # TODO: and parse element IDs as well; and record which are set/unset.
91
+ with Path(path).open("rt") as fp:
92
+ lns = fp.readlines()
93
+
94
+ wks: list[dict[str, Path]] = []
95
+ for ln in lns:
96
+ ln_s = ln.strip()
97
+ if not ln_s:
98
+ continue
99
+ wk_path = Path(ln_s).absolute()
100
+ if not wk_path.is_dir():
101
+ logger.warning(f"{str(wk_path)} is not a workflow")
102
+ continue
103
+
104
+ wks.append(
105
+ {
106
+ "path": wk_path,
107
+ }
108
+ )
109
+
110
+ return wks
111
+
112
+ def on_modified(self, event: FileSystemEvent):
113
+ """
114
+ Callback when files are modified.
115
+ """
116
+ self.logger.info(f"Watch file modified: {event.src_path!r}")
117
+ wks = self.parse_watch_workflows_file(
118
+ cast("str", event.src_path), logger=self.logger
119
+ )
120
+ self.workflow_monitor.update_workflow_paths(wks)
121
+
122
+ def join(self) -> None:
123
+ """
124
+ Join the worker thread.
125
+ """
126
+ self.observer.join()
127
+
128
+ def stop(self) -> None:
129
+ """
130
+ Stop this monitor.
131
+ """
132
+ self.observer.stop()
133
+ self.observer.join() # wait for it to stop!
134
+ self.workflow_monitor.stop()
135
+
136
+
137
+ class WorkflowMonitor:
138
+ """
139
+ Workflow monitor.
140
+ """
141
+
142
+ def __init__(
143
+ self,
144
+ workflow_paths: list[dict[str, Path]],
145
+ watch_interval: float | timedelta,
146
+ logger: Logger,
147
+ ):
148
+ if isinstance(watch_interval, timedelta):
149
+ self.watch_interval = int(watch_interval.total_seconds())
150
+ else:
151
+ self.watch_interval = int(watch_interval)
152
+
153
+ self.event_handler = _FSEHDelegate(self.on_modified)
154
+ self.workflow_paths = workflow_paths
155
+ self.logger = logger
156
+
157
+ self._monitor_workflow_paths()
158
+
159
+ def _monitor_workflow_paths(self) -> None:
160
+ observer = PollingObserver(timeout=self.watch_interval)
161
+ self.observer: PollingObserver | None = observer
162
+ for i in self.workflow_paths:
163
+ observer.schedule(
164
+ self.event_handler, path=cast("str", i["path"]), recursive=False
165
+ )
166
+ self.logger.info(f"Watching workflow: {i['path'].name}")
167
+
168
+ observer.start()
169
+
170
+ def on_modified(self, event: FileSystemEvent):
171
+ """
172
+ Triggered on a workflow being modified.
173
+ """
174
+ self.logger.info(f"Workflow modified: {event.src_path!r}")
175
+
176
+ def update_workflow_paths(self, new_paths: list[dict[str, Path]]):
177
+ """
178
+ Change the set of paths to monitored workflows.
179
+ """
180
+ self.logger.info("Updating watched workflows.")
181
+ self.stop()
182
+ self.workflow_paths = new_paths
183
+ self._monitor_workflow_paths()
184
+
185
+ def stop(self) -> None:
186
+ """
187
+ Stop this monitor.
188
+ """
189
+ if self.observer:
190
+ self.observer.stop()
191
+ self.observer.join() # wait for it to stop!
192
+ self.observer = None
hpcflow/sdk/log.py ADDED
@@ -0,0 +1,288 @@
1
+ """
2
+ Interface to the standard logger, and performance logging utility.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from functools import wraps
7
+ import logging
8
+ import logging.handlers
9
+ from pathlib import Path
10
+ import time
11
+ from collections import defaultdict
12
+ from collections.abc import Callable, Sequence
13
+ import statistics
14
+ from dataclasses import dataclass
15
+ from typing import ClassVar, TypeVar, TYPE_CHECKING
16
+ from typing_extensions import ParamSpec
17
+
18
+ if TYPE_CHECKING:
19
+ from .app import BaseApp
20
+
21
+
22
+ P = ParamSpec("P")
23
+ T = TypeVar("T")
24
+
25
+
26
+ @dataclass
27
+ class _Summary:
28
+ """
29
+ Summary of a particular node's execution time.
30
+ """
31
+
32
+ number: int
33
+ mean: float
34
+ stddev: float
35
+ min: float
36
+ max: float
37
+ sum: float
38
+ children: dict[tuple[str, ...], _Summary]
39
+
40
+
41
+ class TimeIt:
42
+ """
43
+ Method execution time instrumentation.
44
+ """
45
+
46
+ #: Whether the instrumentation is active.
47
+ active: ClassVar = False
48
+ #: Where to log to.
49
+ file_path: ClassVar[str | None] = None
50
+ #: The details be tracked.
51
+ timers: ClassVar[dict[tuple[str, ...], list[float]]] = defaultdict(list)
52
+ #: Traces of the stack.
53
+ trace: ClassVar[list[str]] = []
54
+ #: Trace indices.
55
+ trace_idx: ClassVar[list[int]] = []
56
+ #: Preceding traces.
57
+ trace_prev: ClassVar[list[str]] = []
58
+ #: Preceding trace indices.
59
+ trace_idx_prev: ClassVar[list[int]] = []
60
+
61
+ def __enter__(self):
62
+ self.__class__.active = True
63
+ return self
64
+
65
+ def __exit__(self, exc_type, exc_val, exc_tb):
66
+ try:
67
+ self.__class__.summarise_string()
68
+ finally:
69
+ self.__class__.reset()
70
+ self.__class__.active = False
71
+
72
+ @classmethod
73
+ def decorator(cls, func: Callable[P, T]) -> Callable[P, T]:
74
+ """
75
+ Decorator for a method that is to have its execution time monitored.
76
+ """
77
+
78
+ @wraps(func)
79
+ def wrapper(*args, **kwargs) -> T:
80
+ if not cls.active:
81
+ return func(*args, **kwargs)
82
+
83
+ cls.trace.append(func.__qualname__)
84
+
85
+ if cls.trace_prev == cls.trace:
86
+ new_trace_idx = cls.trace_idx_prev[-1] + 1
87
+ else:
88
+ new_trace_idx = 0
89
+ cls.trace_idx.append(new_trace_idx)
90
+
91
+ tic = time.perf_counter()
92
+ out = func(*args, **kwargs)
93
+ toc = time.perf_counter()
94
+ elapsed = toc - tic
95
+
96
+ cls.timers[tuple(cls.trace)].append(elapsed)
97
+
98
+ cls.trace_prev = list(cls.trace)
99
+ cls.trace_idx_prev = list(cls.trace_idx)
100
+
101
+ cls.trace.pop()
102
+ cls.trace_idx.pop()
103
+
104
+ return out
105
+
106
+ return wrapper
107
+
108
+ @classmethod
109
+ def _summarise(cls) -> dict[tuple[str, ...], _Summary]:
110
+ """
111
+ Produce a machine-readable summary of method execution time statistics.
112
+ """
113
+ stats = {
114
+ k: _Summary(
115
+ len(v),
116
+ statistics.mean(v),
117
+ statistics.pstdev(v),
118
+ min(v),
119
+ max(v),
120
+ sum(v),
121
+ {},
122
+ )
123
+ for k, v in cls.timers.items()
124
+ }
125
+
126
+ # make a graph
127
+ for key in sorted(stats, key=lambda x: len(x), reverse=True):
128
+ if len(key) == 1:
129
+ continue
130
+ value = stats.pop(key)
131
+ parent_key = key[:-1]
132
+ if parent_key in stats:
133
+ stats[parent_key].children[key] = value
134
+
135
+ return stats
136
+
137
+ @classmethod
138
+ def summarise_string(cls) -> None:
139
+ """
140
+ Produce a human-readable summary of method execution time statistics.
141
+ """
142
+
143
+ def _format_nodes(
144
+ node: dict[tuple[str, ...], _Summary],
145
+ depth: int = 0,
146
+ depth_final: Sequence[bool] = (),
147
+ ):
148
+ for idx, (k, v) in enumerate(node.items()):
149
+ is_final_child = idx == len(node) - 1
150
+ angle = "└ " if is_final_child else "├ "
151
+ bars = ""
152
+ if depth > 0:
153
+ bars = "".join(f"{'│ ' if not i else ' '}" for i in depth_final)
154
+ k_str = bars + (angle if depth > 0 else "") + f"{k[depth]}"
155
+ min_str = f"{v.min:10.6f}" if v.number > 1 else f"{f'-':^12s}"
156
+ max_str = f"{v.max:10.6f}" if v.number > 1 else f"{f'-':^12s}"
157
+ stddev_str = f"({v.stddev:8.6f})" if v.number > 1 else f"{f' ':^10s}"
158
+ out.append(
159
+ f"{k_str:.<80s} {v.sum:12.6f} "
160
+ f"{v.mean:10.6f} {stddev_str} {v.number:8d} "
161
+ f"{min_str} {max_str} "
162
+ )
163
+ depth_final_next = list(depth_final)
164
+ if depth > 0:
165
+ depth_final_next.append(is_final_child)
166
+ _format_nodes(v.children, depth + 1, depth_final_next)
167
+
168
+ summary = cls._summarise()
169
+
170
+ out = [
171
+ f"{'function':^80s} {'sum /s':^12s} {'mean (stddev) /s':^20s} {'N':^8s} "
172
+ f"{'min /s':^12s} {'max /s':^12s}"
173
+ ]
174
+ _format_nodes(summary)
175
+ out_str = "\n".join(out)
176
+ if cls.file_path:
177
+ Path(cls.file_path).write_text(out_str, encoding="utf-8")
178
+ else:
179
+ print(out_str)
180
+
181
+ @classmethod
182
+ def reset(cls):
183
+ cls.timers = defaultdict(list)
184
+ cls.trace = []
185
+ cls.trace_idx = []
186
+ cls.trace_prev = []
187
+ cls.trace_idx_prev = []
188
+
189
+
190
+ class AppLog:
191
+ """
192
+ Application log control.
193
+ """
194
+
195
+ #: Default logging level for the console.
196
+ DEFAULT_LOG_CONSOLE_LEVEL: ClassVar = "WARNING"
197
+ #: Default logging level for log files.
198
+ DEFAULT_LOG_FILE_LEVEL: ClassVar = "WARNING"
199
+
200
+ def __init__(self, app: BaseApp, log_console_level: str | None = None) -> None:
201
+ #: The application context.
202
+ self._app = app
203
+ #: The base logger for the application.
204
+ self.logger = logging.getLogger(app.package_name)
205
+ self.logger.setLevel(logging.WARNING)
206
+ #: The handler for directing logging messages to the console.
207
+ self.console_handler = self.__add_console_logger(
208
+ level=log_console_level or AppLog.DEFAULT_LOG_CONSOLE_LEVEL
209
+ )
210
+ self.file_handler: logging.FileHandler | None = None
211
+
212
+ def _ensure_logger_level(self):
213
+ """Ensure the logger's level is set to a level that triggers the handlers.
214
+
215
+ Notes
216
+ -----
217
+ Previously, we fixed the logger to DEBUG, but we found other Python packages
218
+ could then trigger debug logs in hpcflow even though the handlers were set to e.g.
219
+ ERROR.
220
+
221
+ """
222
+ min_level = min((handler.level for handler in self.logger.handlers), default=0)
223
+ if self.logger.level != min_level:
224
+ self.logger.setLevel(min_level)
225
+
226
+ def __add_console_logger(self, level: str, fmt: str | None = None) -> logging.Handler:
227
+ fmt = fmt or "%(levelname)s %(name)s: %(message)s"
228
+ handler = logging.StreamHandler()
229
+ handler.setFormatter(logging.Formatter(fmt))
230
+ handler.setLevel(level)
231
+ self.logger.addHandler(handler)
232
+ self._ensure_logger_level()
233
+ return handler
234
+
235
+ def update_console_level(self, new_level: str | None = None) -> None:
236
+ """
237
+ Set the logging level for console messages.
238
+ """
239
+ new_level = new_level or AppLog.DEFAULT_LOG_CONSOLE_LEVEL
240
+ self.console_handler.setLevel(new_level.upper())
241
+ self._ensure_logger_level()
242
+
243
+ def update_file_level(self, new_level: str | None = None) -> None:
244
+ if self.file_handler:
245
+ new_level = new_level or AppLog.DEFAULT_LOG_FILE_LEVEL
246
+ self.file_handler.setLevel(new_level.upper())
247
+ self._ensure_logger_level()
248
+
249
+ def add_file_logger(
250
+ self,
251
+ path: str | Path,
252
+ level: str | None = None,
253
+ fmt: str | None = None,
254
+ max_bytes: int | None = None,
255
+ backup_count: int = 4,
256
+ ) -> None:
257
+ """
258
+ Add a log file.
259
+ """
260
+ path = Path(path)
261
+ fmt = fmt or "%(asctime)s %(levelname)s %(name)s: %(message)s"
262
+ level = level or AppLog.DEFAULT_LOG_FILE_LEVEL
263
+ max_bytes = max_bytes or int(50e6)
264
+
265
+ if not path.parent.is_dir():
266
+ self.logger.info(f"Generating log file parent directory: {path.parent!r}")
267
+ path.parent.mkdir(exist_ok=True, parents=True)
268
+
269
+ handler = logging.handlers.RotatingFileHandler(
270
+ filename=path,
271
+ maxBytes=max_bytes,
272
+ backupCount=backup_count,
273
+ )
274
+ handler.setFormatter(logging.Formatter(fmt))
275
+ handler.setLevel(level.upper())
276
+ self.logger.addHandler(handler)
277
+ self.file_handler = handler
278
+ self._ensure_logger_level()
279
+
280
+ def remove_file_handler(self) -> None:
281
+ """Remove the file handler."""
282
+ if self.file_handler:
283
+ self.logger.debug(
284
+ f"Removing file handler from the AppLog: {self.file_handler!r}."
285
+ )
286
+ self.logger.removeHandler(self.file_handler)
287
+ self.file_handler = None
288
+ self._ensure_logger_level()
@@ -0,0 +1,18 @@
1
+ """
2
+ Workflow persistence subsystem.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .base import PersistentStore
10
+
11
+
12
+ def store_cls_from_str(store_format: str) -> type[PersistentStore]:
13
+ """
14
+ Get the class that implements the persistence store from its name.
15
+ """
16
+ from .discovery import store_cls_from_str as impl
17
+
18
+ return impl(store_format)