hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a199__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 (176) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +9 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/bad_script.py +2 -0
  5. hpcflow/data/scripts/do_nothing.py +2 -0
  6. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  7. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  8. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  11. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  12. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  13. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  15. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  16. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  23. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  24. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  25. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  26. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  27. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  28. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  29. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  30. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  31. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  32. hpcflow/data/scripts/script_exit_test.py +5 -0
  33. hpcflow/data/template_components/environments.yaml +1 -1
  34. hpcflow/sdk/__init__.py +26 -15
  35. hpcflow/sdk/app.py +2192 -768
  36. hpcflow/sdk/cli.py +506 -296
  37. hpcflow/sdk/cli_common.py +105 -7
  38. hpcflow/sdk/config/__init__.py +1 -1
  39. hpcflow/sdk/config/callbacks.py +115 -43
  40. hpcflow/sdk/config/cli.py +126 -103
  41. hpcflow/sdk/config/config.py +674 -318
  42. hpcflow/sdk/config/config_file.py +131 -95
  43. hpcflow/sdk/config/errors.py +125 -84
  44. hpcflow/sdk/config/types.py +148 -0
  45. hpcflow/sdk/core/__init__.py +25 -1
  46. hpcflow/sdk/core/actions.py +1771 -1059
  47. hpcflow/sdk/core/app_aware.py +24 -0
  48. hpcflow/sdk/core/cache.py +139 -79
  49. hpcflow/sdk/core/command_files.py +263 -287
  50. hpcflow/sdk/core/commands.py +145 -112
  51. hpcflow/sdk/core/element.py +828 -535
  52. hpcflow/sdk/core/enums.py +192 -0
  53. hpcflow/sdk/core/environment.py +74 -93
  54. hpcflow/sdk/core/errors.py +455 -52
  55. hpcflow/sdk/core/execute.py +207 -0
  56. hpcflow/sdk/core/json_like.py +540 -272
  57. hpcflow/sdk/core/loop.py +751 -347
  58. hpcflow/sdk/core/loop_cache.py +164 -47
  59. hpcflow/sdk/core/object_list.py +370 -207
  60. hpcflow/sdk/core/parameters.py +1100 -627
  61. hpcflow/sdk/core/rule.py +59 -41
  62. hpcflow/sdk/core/run_dir_files.py +21 -37
  63. hpcflow/sdk/core/skip_reason.py +7 -0
  64. hpcflow/sdk/core/task.py +1649 -1339
  65. hpcflow/sdk/core/task_schema.py +308 -196
  66. hpcflow/sdk/core/test_utils.py +191 -114
  67. hpcflow/sdk/core/types.py +440 -0
  68. hpcflow/sdk/core/utils.py +485 -309
  69. hpcflow/sdk/core/validation.py +82 -9
  70. hpcflow/sdk/core/workflow.py +2544 -1178
  71. hpcflow/sdk/core/zarr_io.py +98 -137
  72. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  73. hpcflow/sdk/demo/cli.py +53 -33
  74. hpcflow/sdk/helper/cli.py +18 -15
  75. hpcflow/sdk/helper/helper.py +75 -63
  76. hpcflow/sdk/helper/watcher.py +61 -28
  77. hpcflow/sdk/log.py +122 -71
  78. hpcflow/sdk/persistence/__init__.py +8 -31
  79. hpcflow/sdk/persistence/base.py +1360 -606
  80. hpcflow/sdk/persistence/defaults.py +6 -0
  81. hpcflow/sdk/persistence/discovery.py +38 -0
  82. hpcflow/sdk/persistence/json.py +568 -188
  83. hpcflow/sdk/persistence/pending.py +382 -179
  84. hpcflow/sdk/persistence/store_resource.py +39 -23
  85. hpcflow/sdk/persistence/types.py +318 -0
  86. hpcflow/sdk/persistence/utils.py +14 -11
  87. hpcflow/sdk/persistence/zarr.py +1337 -433
  88. hpcflow/sdk/runtime.py +44 -41
  89. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  90. hpcflow/sdk/submission/jobscript.py +1651 -692
  91. hpcflow/sdk/submission/schedulers/__init__.py +167 -39
  92. hpcflow/sdk/submission/schedulers/direct.py +121 -81
  93. hpcflow/sdk/submission/schedulers/sge.py +170 -129
  94. hpcflow/sdk/submission/schedulers/slurm.py +291 -268
  95. hpcflow/sdk/submission/schedulers/utils.py +12 -2
  96. hpcflow/sdk/submission/shells/__init__.py +14 -15
  97. hpcflow/sdk/submission/shells/base.py +150 -29
  98. hpcflow/sdk/submission/shells/bash.py +283 -173
  99. hpcflow/sdk/submission/shells/os_version.py +31 -30
  100. hpcflow/sdk/submission/shells/powershell.py +228 -170
  101. hpcflow/sdk/submission/submission.py +1014 -335
  102. hpcflow/sdk/submission/types.py +140 -0
  103. hpcflow/sdk/typing.py +182 -12
  104. hpcflow/sdk/utils/arrays.py +71 -0
  105. hpcflow/sdk/utils/deferred_file.py +55 -0
  106. hpcflow/sdk/utils/hashing.py +16 -0
  107. hpcflow/sdk/utils/patches.py +12 -0
  108. hpcflow/sdk/utils/strings.py +33 -0
  109. hpcflow/tests/api/test_api.py +32 -0
  110. hpcflow/tests/conftest.py +27 -6
  111. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  112. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  113. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  114. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  115. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  116. hpcflow/tests/scripts/test_main_scripts.py +866 -85
  117. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  118. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  119. hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
  120. hpcflow/tests/unit/test_action.py +262 -75
  121. hpcflow/tests/unit/test_action_rule.py +9 -4
  122. hpcflow/tests/unit/test_app.py +33 -6
  123. hpcflow/tests/unit/test_cache.py +46 -0
  124. hpcflow/tests/unit/test_cli.py +134 -1
  125. hpcflow/tests/unit/test_command.py +71 -54
  126. hpcflow/tests/unit/test_config.py +142 -16
  127. hpcflow/tests/unit/test_config_file.py +21 -18
  128. hpcflow/tests/unit/test_element.py +58 -62
  129. hpcflow/tests/unit/test_element_iteration.py +50 -1
  130. hpcflow/tests/unit/test_element_set.py +29 -19
  131. hpcflow/tests/unit/test_group.py +4 -2
  132. hpcflow/tests/unit/test_input_source.py +116 -93
  133. hpcflow/tests/unit/test_input_value.py +29 -24
  134. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  135. hpcflow/tests/unit/test_json_like.py +44 -35
  136. hpcflow/tests/unit/test_loop.py +1396 -84
  137. hpcflow/tests/unit/test_meta_task.py +325 -0
  138. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  139. hpcflow/tests/unit/test_object_list.py +17 -12
  140. hpcflow/tests/unit/test_parameter.py +29 -7
  141. hpcflow/tests/unit/test_persistence.py +237 -42
  142. hpcflow/tests/unit/test_resources.py +20 -18
  143. hpcflow/tests/unit/test_run.py +117 -6
  144. hpcflow/tests/unit/test_run_directories.py +29 -0
  145. hpcflow/tests/unit/test_runtime.py +2 -1
  146. hpcflow/tests/unit/test_schema_input.py +23 -15
  147. hpcflow/tests/unit/test_shell.py +23 -2
  148. hpcflow/tests/unit/test_slurm.py +8 -7
  149. hpcflow/tests/unit/test_submission.py +38 -89
  150. hpcflow/tests/unit/test_task.py +352 -247
  151. hpcflow/tests/unit/test_task_schema.py +33 -20
  152. hpcflow/tests/unit/test_utils.py +9 -11
  153. hpcflow/tests/unit/test_value_sequence.py +15 -12
  154. hpcflow/tests/unit/test_workflow.py +114 -83
  155. hpcflow/tests/unit/test_workflow_template.py +0 -1
  156. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  157. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  158. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  159. hpcflow/tests/unit/utils/test_patches.py +5 -0
  160. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  161. hpcflow/tests/workflows/__init__.py +0 -0
  162. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  163. hpcflow/tests/workflows/test_jobscript.py +334 -1
  164. hpcflow/tests/workflows/test_run_status.py +198 -0
  165. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  166. hpcflow/tests/workflows/test_submission.py +140 -0
  167. hpcflow/tests/workflows/test_workflows.py +160 -15
  168. hpcflow/tests/workflows/test_zip.py +18 -0
  169. hpcflow/viz_demo.ipynb +6587 -3
  170. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
  171. hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
  172. hpcflow/sdk/core/parallel.py +0 -21
  173. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  174. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
  175. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
  176. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,207 @@
1
+ import asyncio
2
+ import os
3
+ import queue
4
+ import struct
5
+ import threading
6
+ import time
7
+
8
+ import zmq
9
+
10
+ from hpcflow.sdk.core.app_aware import AppAware
11
+
12
+
13
+ class Executor(AppAware):
14
+ def __init__(self, cmd, env, package_name):
15
+
16
+ # TODO: make zmq_server optional (but required if action is abortable, or if
17
+ # `script_data_in`/`out`` is "zeromq")
18
+
19
+ self.cmd = cmd
20
+ self.env = env
21
+ self.package_name = package_name
22
+
23
+ # initialise a global ZeroMQ context for use in all threads:
24
+ zmq.Context()
25
+
26
+ self._q = None # queue for inter-thread communication
27
+
28
+ # assigned by `start_zmq_server`:
29
+ self.port_number = None
30
+ self.server_thread = None
31
+
32
+ # assigned on (non-aborted) completion of the subprocess via `_subprocess_runner`:
33
+ self.return_code = None
34
+
35
+ @property
36
+ def q(self):
37
+ if not self._q:
38
+ self._q = queue.Queue()
39
+ return self._q
40
+
41
+ @property
42
+ def zmq_context(self):
43
+ return zmq.Context.instance()
44
+
45
+ def _zmq_server(self):
46
+ """Start a ZeroMQ server on a random port.
47
+
48
+ This method is invoked in a separate thread via `start_zmq_server`.
49
+
50
+ """
51
+ socket = self.zmq_context.socket(zmq.REP)
52
+ port_number = socket.bind_to_random_port("tcp://*")
53
+ self._app.logger.info(f"zmq_server: started on port {port_number}")
54
+
55
+ # send port number back to main thread:
56
+ self.q.put(port_number)
57
+
58
+ self._app.logger.info(f"zmq_server: port number sent to main thread.")
59
+
60
+ # TODO: exception handling
61
+
62
+ while True:
63
+ message = socket.recv_string()
64
+ self._app.logger.info(f"zmq_server: received request: {message}")
65
+
66
+ # Check if the received message is a shutdown signal
67
+ if message in ("shutdown", "abort"):
68
+ self.q.put(message)
69
+ socket.send_string("shutting down the server")
70
+ break
71
+
72
+ else:
73
+ socket.send_string(f"received request: {message}")
74
+
75
+ socket.close()
76
+ self._app.logger.info("zmq_server: server stopped")
77
+
78
+ def start_zmq_server(self) -> int:
79
+
80
+ # start the server thread
81
+ server_thread = threading.Thread(target=self._zmq_server)
82
+ server_thread.start()
83
+
84
+ self._app.logger.info(f"server thread started")
85
+
86
+ if os.name == "nt":
87
+ # some sort of race condition seems to exist on Windows, where self.q.get()
88
+ # will occasionally hang on the Github Actions runners. This seems to resolve
89
+ # it.
90
+ time.sleep(0.1)
91
+
92
+ # block until port number received:
93
+ port_number = self.q.get(timeout=5)
94
+ self._app.logger.info(f"received port number from server thread: {port_number}")
95
+
96
+ self.port_number = port_number
97
+ self.server_thread = server_thread
98
+
99
+ return port_number
100
+
101
+ def stop_zmq_server(self):
102
+
103
+ # send a shutdown signal to the server:
104
+ socket = self.zmq_context.socket(zmq.REQ)
105
+ address = f"tcp://localhost:{self.port_number}"
106
+ socket.connect(address)
107
+ self._app.logger.info(
108
+ f"stop_zmq_server: about to send shutdown message to server: {address!r}"
109
+ )
110
+ socket.send_string("shutdown")
111
+ send_shutdown_out = socket.recv()
112
+ self._app.logger.info(f"stop_zmq_server: received reply: {send_shutdown_out!r}")
113
+ socket.close()
114
+
115
+ # wait for the server thread to finish:
116
+ self._app.logger.info(f"stop_zmq_server: joining server thread")
117
+ self.server_thread.join()
118
+
119
+ self._app.logger.info(f"stop_zmq_server: terminating ZMQ context")
120
+ self.zmq_context.term()
121
+ if self.server_thread.is_alive():
122
+ raise RuntimeError("Server thread is still alive!")
123
+
124
+ def run(self):
125
+ """Launch the subprocess to execute the commands, and once complete, stop the
126
+ ZMQ server. Kill the subprocess if a "shutdown" or "abort" message is sent to the
127
+ server."""
128
+ asyncio.run(self._run())
129
+ return self.return_code
130
+
131
+ def _receive_stop(self):
132
+ """Wait until the queue receives a shutdown message from the server"""
133
+ while True:
134
+ if self.q.get() in ("shutdown", "abort"):
135
+ return
136
+
137
+ async def _subprocess_runner(self):
138
+ app_caps = self.package_name.upper()
139
+ env = {**self.env, f"{app_caps}_RUN_PORT": str(self.port_number)}
140
+ try:
141
+ process = await asyncio.create_subprocess_exec(*self.cmd, env=env)
142
+ self._app.logger.info(
143
+ f"_subprocess_runner: started subprocess: {process=!r}."
144
+ )
145
+ ret_code = await process.wait()
146
+ self._app.logger.info(
147
+ f"_subprocess_runner: subprocess finished with return code: {ret_code!r}."
148
+ )
149
+ self.return_code = ret_code
150
+
151
+ except asyncio.CancelledError:
152
+ process.kill()
153
+
154
+ async def _run(self):
155
+
156
+ # create tasks for the subprocess and a synchronous Queue.get retrieval:
157
+ try:
158
+ wait_abort_thread = asyncio.to_thread(self._receive_stop)
159
+ except AttributeError:
160
+ # Python 3.8
161
+ from hpcflow.sdk.core.utils import to_thread
162
+
163
+ wait_abort_thread = to_thread(self._receive_stop)
164
+
165
+ wait_abort_task = asyncio.create_task(wait_abort_thread)
166
+ subprocess_task = asyncio.create_task(self._subprocess_runner())
167
+
168
+ # wait for either: subprocess to finish, or a stop signal from the server:
169
+ _, pending = await asyncio.wait(
170
+ [wait_abort_task, subprocess_task],
171
+ return_when=asyncio.FIRST_COMPLETED,
172
+ )
173
+
174
+ # TODO: test we can SIGTERM and SIGINT the subprocess successfully?
175
+ # - add an API for sending signals to the process via the server?
176
+
177
+ if pending == {wait_abort_task}:
178
+ # subprocess completed; need to shutdown the server
179
+ self._app.logger.info(f"_run: subprocess completed; stopping zmq server")
180
+ self.stop_zmq_server()
181
+
182
+ else:
183
+ # subprocess still running but got a stop request; need to kill subprocess:
184
+ self._app.logger.info(f"_run: stop request; killing subprocess")
185
+ subprocess_task.cancel()
186
+
187
+ if self.return_code and os.name == "nt":
188
+ # Windows return codes are defined as 32-bit unsigned integers, but
189
+ # some programs might still return negative numbers, so convert to a
190
+ # signed 32-bit integer:
191
+ self.return_code = struct.unpack("i", struct.pack("I", self.return_code))[0]
192
+
193
+ @classmethod
194
+ def send_abort(cls, hostname, port_number):
195
+ """Send an abort message to a running server."""
196
+ context = zmq.Context()
197
+ socket = context.socket(zmq.REQ)
198
+ address = f"tcp://{hostname}:{port_number}"
199
+ socket.connect(address)
200
+ cls._app.logger.info(
201
+ f"send_abort: about to send abort message to server: {address!r}"
202
+ )
203
+ socket.send_string("abort")
204
+ abort_rep = socket.recv()
205
+ cls._app.logger.info(f"send_abort: received reply: {abort_rep!r}")
206
+ socket.close()
207
+ context.term()