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.
- hpcflow/__pyinstaller/hook-hpcflow.py +9 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/environments.yaml +1 -1
- hpcflow/sdk/__init__.py +26 -15
- hpcflow/sdk/app.py +2192 -768
- hpcflow/sdk/cli.py +506 -296
- hpcflow/sdk/cli_common.py +105 -7
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +115 -43
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +674 -318
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +125 -84
- hpcflow/sdk/config/types.py +148 -0
- hpcflow/sdk/core/__init__.py +25 -1
- hpcflow/sdk/core/actions.py +1771 -1059
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +139 -79
- hpcflow/sdk/core/command_files.py +263 -287
- hpcflow/sdk/core/commands.py +145 -112
- hpcflow/sdk/core/element.py +828 -535
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +455 -52
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +751 -347
- hpcflow/sdk/core/loop_cache.py +164 -47
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +1100 -627
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +21 -37
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +1649 -1339
- hpcflow/sdk/core/task_schema.py +308 -196
- hpcflow/sdk/core/test_utils.py +191 -114
- hpcflow/sdk/core/types.py +440 -0
- hpcflow/sdk/core/utils.py +485 -309
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +2544 -1178
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
- hpcflow/sdk/demo/cli.py +53 -33
- hpcflow/sdk/helper/cli.py +18 -15
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +122 -71
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +1360 -606
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +568 -188
- hpcflow/sdk/persistence/pending.py +382 -179
- hpcflow/sdk/persistence/store_resource.py +39 -23
- hpcflow/sdk/persistence/types.py +318 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +1337 -433
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +1651 -692
- hpcflow/sdk/submission/schedulers/__init__.py +167 -39
- hpcflow/sdk/submission/schedulers/direct.py +121 -81
- hpcflow/sdk/submission/schedulers/sge.py +170 -129
- hpcflow/sdk/submission/schedulers/slurm.py +291 -268
- hpcflow/sdk/submission/schedulers/utils.py +12 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +150 -29
- hpcflow/sdk/submission/shells/bash.py +283 -173
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +228 -170
- hpcflow/sdk/submission/submission.py +1014 -335
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +182 -12
- hpcflow/sdk/utils/arrays.py +71 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +12 -0
- hpcflow/sdk/utils/strings.py +33 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +27 -6
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +866 -85
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
- hpcflow/tests/unit/test_action.py +262 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +33 -6
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +134 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +142 -16
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +50 -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_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +1396 -84
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +29 -7
- hpcflow/tests/unit/test_persistence.py +237 -42
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +117 -6
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +23 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +38 -89
- 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/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +334 -1
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +160 -15
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6587 -3
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
- hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -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.0a199.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
- {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()
|