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
hpcflow/sdk/core/commands.py
CHANGED
@@ -2,22 +2,32 @@
|
|
2
2
|
Model of a command run in an action.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
5
6
|
from dataclasses import dataclass, field
|
6
7
|
from functools import partial
|
7
8
|
from pathlib import Path
|
8
9
|
import re
|
9
|
-
from typing import
|
10
|
+
from typing import Any, ClassVar, TYPE_CHECKING
|
10
11
|
|
11
12
|
import numpy as np
|
12
13
|
|
13
|
-
from hpcflow.sdk import
|
14
|
+
from hpcflow.sdk.typing import hydrate
|
14
15
|
from hpcflow.sdk.core.element import ElementResources
|
15
16
|
from hpcflow.sdk.core.errors import NoCLIFormatMethodError
|
16
17
|
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
17
18
|
from hpcflow.sdk.core.parameters import ParameterValue
|
18
19
|
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
22
|
+
from re import Pattern
|
23
|
+
from .actions import ActionRule, Action
|
24
|
+
from .element import ElementActionRun
|
25
|
+
from .environment import Environment
|
26
|
+
from ..submission.shells import Shell
|
27
|
+
|
19
28
|
|
20
29
|
@dataclass
|
30
|
+
@hydrate
|
21
31
|
class Command(JSONLike):
|
22
32
|
"""
|
23
33
|
A command that may be run within a workflow action.
|
@@ -43,10 +53,7 @@ class Command(JSONLike):
|
|
43
53
|
Rules that state whether this command is eligible to run.
|
44
54
|
"""
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
_app_attr = "app"
|
49
|
-
_child_objects = (
|
56
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
50
57
|
ChildObjectSpec(
|
51
58
|
name="rules",
|
52
59
|
class_name="ActionRule",
|
@@ -57,22 +64,27 @@ class Command(JSONLike):
|
|
57
64
|
|
58
65
|
#: The actual command.
|
59
66
|
#: Overrides :py:attr:`executable`.
|
60
|
-
command:
|
67
|
+
command: str | None = None
|
61
68
|
#: The executable to run,
|
62
69
|
#: from the set of executable managed by the environment.
|
63
|
-
executable:
|
70
|
+
executable: str | None = None
|
64
71
|
#: The arguments to pass in.
|
65
|
-
arguments:
|
72
|
+
arguments: list[str] | None = None
|
66
73
|
#: Values that may be substituted when preparing the arguments.
|
67
|
-
variables:
|
74
|
+
variables: dict[str, str] | None = None
|
68
75
|
#: The name of a file to write standard output to.
|
69
|
-
stdout:
|
76
|
+
stdout: str | None = None
|
70
77
|
#: The name of a file to write standard error to.
|
71
|
-
stderr:
|
78
|
+
stderr: str | None = None
|
72
79
|
#: The name of a file to read standard input from.
|
73
|
-
stdin:
|
80
|
+
stdin: str | None = None
|
74
81
|
#: Rules that state whether this command is eligible to run.
|
75
|
-
rules:
|
82
|
+
rules: list[ActionRule] = field(default_factory=list)
|
83
|
+
|
84
|
+
action: Action | None = None # assigned by parent Action
|
85
|
+
|
86
|
+
def __post_init__(self):
|
87
|
+
self._set_parent_refs()
|
76
88
|
|
77
89
|
def __repr__(self) -> str:
|
78
90
|
out = []
|
@@ -95,61 +107,78 @@ class Command(JSONLike):
|
|
95
107
|
|
96
108
|
return f"{self.__class__.__name__}({', '.join(out)})"
|
97
109
|
|
98
|
-
def
|
110
|
+
def __eq__(self, other):
|
111
|
+
if not isinstance(other, self.__class__):
|
112
|
+
return False
|
113
|
+
return (
|
114
|
+
self.command == other.command
|
115
|
+
and self.executable == other.executable
|
116
|
+
and self.arguments == other.arguments
|
117
|
+
and self.variables == other.variables
|
118
|
+
and self.stdout == other.stdout
|
119
|
+
and self.stderr == other.stderr
|
120
|
+
and self.stdin == other.stdin
|
121
|
+
and self.rules == other.rules
|
122
|
+
)
|
123
|
+
|
124
|
+
def __get_initial_command_line(self) -> str:
|
99
125
|
if self.command:
|
100
126
|
return self.command
|
101
127
|
else:
|
102
128
|
return self.executable or ""
|
103
129
|
|
104
|
-
|
130
|
+
__EXE_SCRIPT_RE: ClassVar[Pattern] = re.compile(r"\<\<(executable|script):(.*?)\>\>")
|
131
|
+
__ENV_SPEC_RE: ClassVar[Pattern] = re.compile(r"\<\<env:(.*?)\>\>")
|
132
|
+
|
133
|
+
def get_command_line(
|
134
|
+
self, EAR: ElementActionRun, shell: Shell, env: Environment
|
135
|
+
) -> tuple[str, list[tuple[str, ...]]]:
|
105
136
|
"""Return the resolved command line.
|
106
137
|
|
107
138
|
This is ordinarily called at run-time by `Workflow.write_commands`.
|
108
|
-
|
109
139
|
"""
|
110
140
|
|
111
|
-
self.
|
112
|
-
cmd_str = self.
|
141
|
+
self._app.persistence_logger.debug("Command.get_command_line")
|
142
|
+
cmd_str = self.__get_initial_command_line()
|
113
143
|
|
114
144
|
def _format_sum(iterable: Iterable) -> str:
|
115
145
|
return str(sum(iterable))
|
116
146
|
|
117
147
|
def _join(iterable: Iterable, delim: str) -> str:
|
118
|
-
return delim.join(str
|
148
|
+
return delim.join(map(str, iterable))
|
119
149
|
|
120
|
-
parse_types = {
|
150
|
+
parse_types: dict[str, Callable[..., str]] = {
|
121
151
|
"sum": _format_sum,
|
122
152
|
"join": _join,
|
123
153
|
}
|
124
154
|
|
125
|
-
def exec_script_repl(match_obj):
|
155
|
+
def exec_script_repl(match_obj: re.Match[str]) -> str:
|
126
156
|
typ, val = match_obj.groups()
|
127
157
|
if typ == "executable":
|
128
158
|
executable = env.executables.get(val)
|
129
159
|
filterable = ElementResources.get_env_instance_filterable_attributes()
|
130
|
-
filter_exec = {
|
160
|
+
filter_exec = {attr: EAR.get_resources().get(attr) for attr in filterable}
|
131
161
|
exec_cmd = executable.filter_instances(**filter_exec)[0].command
|
132
|
-
|
162
|
+
return exec_cmd.replace("<<num_cores>>", str(EAR.resources.num_cores))
|
133
163
|
elif typ == "script":
|
134
|
-
|
135
|
-
|
164
|
+
# TODO: is this needed? we have <<script_name>> <<script_path>> etc as command variables
|
165
|
+
return EAR.action.get_script_name(val)
|
166
|
+
else:
|
167
|
+
raise ValueError("impossible match occurred")
|
136
168
|
|
137
|
-
def input_param_repl(match_obj, inp_val):
|
169
|
+
def input_param_repl(match_obj: re.Match[str], inp_val) -> str:
|
138
170
|
_, func, func_kwargs, method, method_kwargs = match_obj.groups()
|
139
171
|
|
140
172
|
if isinstance(inp_val, ParameterValue):
|
141
173
|
if not method:
|
142
174
|
method = "CLI_format"
|
143
175
|
if not hasattr(inp_val, method):
|
144
|
-
raise NoCLIFormatMethodError(
|
145
|
-
|
146
|
-
f"object {inp_val!r}."
|
147
|
-
)
|
148
|
-
kwargs = self._prepare_kwargs_from_string(args_str=method_kwargs)
|
176
|
+
raise NoCLIFormatMethodError(method, inp_val)
|
177
|
+
kwargs = self.__prepare_kwargs_from_string(args_str=method_kwargs)
|
149
178
|
inp_val = getattr(inp_val, method)(**kwargs)
|
150
179
|
|
151
180
|
if func:
|
152
|
-
kwargs = self.
|
181
|
+
kwargs = self.__prepare_kwargs_from_string(
|
153
182
|
args_str=func_kwargs,
|
154
183
|
doubled_quoted_args=["delim"],
|
155
184
|
)
|
@@ -158,12 +187,9 @@ class Command(JSONLike):
|
|
158
187
|
return str(inp_val)
|
159
188
|
|
160
189
|
file_regex = r"(\<\<file:{}\>\>?)"
|
161
|
-
exe_script_regex = r"\<\<(executable|script):(.*?)\>\>"
|
162
|
-
env_specs_regex = r"\<\<env:(.*?)\>\>"
|
163
190
|
|
164
191
|
# substitute executables:
|
165
|
-
cmd_str =
|
166
|
-
pattern=exe_script_regex,
|
192
|
+
cmd_str = self.__EXE_SCRIPT_RE.sub(
|
167
193
|
repl=exec_script_repl,
|
168
194
|
string=cmd_str,
|
169
195
|
)
|
@@ -172,21 +198,24 @@ class Command(JSONLike):
|
|
172
198
|
# an `<<args>>` variable::
|
173
199
|
for var_key, var_val in (self.variables or {}).items():
|
174
200
|
# substitute any `<<env:>>` specifiers
|
175
|
-
var_val =
|
176
|
-
|
177
|
-
repl=lambda match_obj: EAR.env_spec[match_obj.group(1)],
|
201
|
+
var_val = self.__ENV_SPEC_RE.sub(
|
202
|
+
repl=lambda match_obj: EAR.env_spec[match_obj[1]],
|
178
203
|
string=var_val,
|
179
204
|
)
|
180
205
|
cmd_str = cmd_str.replace(f"<<{var_key}>>", var_val)
|
181
206
|
if "<<args>>" in cmd_str:
|
182
|
-
args_str = " ".join(self.arguments or
|
207
|
+
args_str = " ".join(self.arguments or ())
|
183
208
|
ends_in_args = cmd_str.endswith("<<args>>")
|
184
209
|
cmd_str = cmd_str.replace("<<args>>", args_str)
|
185
210
|
if ends_in_args and not args_str:
|
186
211
|
cmd_str = cmd_str.rstrip()
|
187
212
|
|
188
213
|
# remove any left over "<<args>>" and "<<script_name>>"s:
|
189
|
-
cmd_str =
|
214
|
+
cmd_str = (
|
215
|
+
cmd_str.replace("<<args>>", "")
|
216
|
+
.replace("<<script_name>>", "")
|
217
|
+
.replace("<<script_path>>", "")
|
218
|
+
)
|
190
219
|
|
191
220
|
# substitute input parameters in command:
|
192
221
|
types_pattern = "|".join(parse_types)
|
@@ -203,7 +232,10 @@ class Command(JSONLike):
|
|
203
232
|
cmd_inp = ".".join(cmd_inp_parts[:-1])
|
204
233
|
else:
|
205
234
|
cmd_inp = cmd_inp_full
|
206
|
-
inp_val = EAR.get(
|
235
|
+
inp_val = EAR.get(
|
236
|
+
f"inputs.{cmd_inp}",
|
237
|
+
raise_on_unset=True,
|
238
|
+
) # TODO: what if schema output?
|
207
239
|
pattern_i = pattern.format(
|
208
240
|
types_pattern=types_pattern,
|
209
241
|
name=re.escape(cmd_inp),
|
@@ -214,9 +246,11 @@ class Command(JSONLike):
|
|
214
246
|
string=cmd_str,
|
215
247
|
)
|
216
248
|
|
217
|
-
# substitute input files in command:
|
218
|
-
for cmd_file in EAR.action.
|
219
|
-
file_path = EAR.get(
|
249
|
+
# substitute input/output files in command:
|
250
|
+
for cmd_file in EAR.action.get_command_file_labels():
|
251
|
+
file_path = EAR.get(
|
252
|
+
f"input_files.{cmd_file}", raise_on_unset=True
|
253
|
+
) or EAR.get(f"output_files.{cmd_file}", raise_on_unset=True)
|
220
254
|
# assuming we have copied this file to the EAR directory, then we just
|
221
255
|
# need the file name:
|
222
256
|
file_name = Path(file_path).name
|
@@ -226,7 +260,7 @@ class Command(JSONLike):
|
|
226
260
|
string=cmd_str,
|
227
261
|
)
|
228
262
|
|
229
|
-
shell_vars = []
|
263
|
+
shell_vars: list[tuple[str, ...]] = []
|
230
264
|
out_types = self.get_output_types()
|
231
265
|
if out_types["stdout"]:
|
232
266
|
# TODO: also map stderr/both if possible
|
@@ -246,56 +280,53 @@ class Command(JSONLike):
|
|
246
280
|
|
247
281
|
return cmd_str, shell_vars
|
248
282
|
|
249
|
-
|
283
|
+
# note: we use "parameter" rather than "output", because it could be a schema
|
284
|
+
# output or schema input.
|
285
|
+
__PARAM_RE: ClassVar[Pattern] = re.compile(
|
286
|
+
r"(?:\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(\w+)"
|
287
|
+
r"(?:\.(?:\w+)\((?:.*?)\))?\)?\>\>?)"
|
288
|
+
)
|
289
|
+
|
290
|
+
def get_output_types(self) -> Mapping[str, str | None]:
|
250
291
|
"""
|
251
292
|
Get whether stdout and stderr are workflow parameters.
|
252
293
|
"""
|
253
|
-
|
254
|
-
# output or schema input.
|
255
|
-
pattern = (
|
256
|
-
r"(?:\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(\w+)"
|
257
|
-
r"(?:\.(?:\w+)\((?:.*?)\))?\)?\>\>?)"
|
258
|
-
)
|
259
|
-
out = {"stdout": None, "stderr": None}
|
294
|
+
out: dict[str, str | None] = {"stdout": None, "stderr": None}
|
260
295
|
for i, label in zip((self.stdout, self.stderr), ("stdout", "stderr")):
|
261
|
-
if i:
|
262
|
-
|
263
|
-
if match:
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
)
|
271
|
-
out[label] = param_typ
|
296
|
+
if i and (match := self.__PARAM_RE.search(i)):
|
297
|
+
param_typ: str = match[1]
|
298
|
+
if match.span(0) != (0, len(i)):
|
299
|
+
raise ValueError(
|
300
|
+
f"If specified as a parameter, `{label}` must not include"
|
301
|
+
f" any characters other than the parameter "
|
302
|
+
f"specification, but this was given: {i!r}."
|
303
|
+
)
|
304
|
+
out[label] = param_typ
|
272
305
|
return out
|
273
306
|
|
274
307
|
@staticmethod
|
275
|
-
def
|
276
|
-
|
308
|
+
def __prepare_kwargs_from_string(
|
309
|
+
args_str: str | None, doubled_quoted_args: list[str] | None = None
|
310
|
+
) -> dict[str, str]:
|
277
311
|
if args_str is None:
|
278
|
-
return
|
312
|
+
return {}
|
279
313
|
|
314
|
+
kwargs: dict[str, str] = {}
|
280
315
|
# deal with specified double-quoted arguments first if it exists:
|
281
|
-
for quote_arg in doubled_quoted_args or
|
316
|
+
for quote_arg in doubled_quoted_args or ():
|
282
317
|
quote_pat = r'.*({quote_arg}="(.*)").*'.format(quote_arg=quote_arg)
|
283
|
-
match
|
284
|
-
if match:
|
318
|
+
if match := re.match(quote_pat, args_str):
|
285
319
|
quote_str, quote_contents = match.groups()
|
286
320
|
args_str = args_str.replace(quote_str, "")
|
287
321
|
kwargs[quote_arg] = quote_contents
|
288
322
|
|
289
|
-
args_str
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
name_i = i_split[0].strip()
|
294
|
-
value = i_split[1].strip()
|
295
|
-
kwargs[name_i] = value
|
323
|
+
if args_str := args_str.strip().strip(","):
|
324
|
+
for arg_part in args_str.split(","):
|
325
|
+
name_i, value_i = map(str.strip, arg_part.split("="))
|
326
|
+
kwargs[name_i] = value_i
|
296
327
|
return kwargs
|
297
328
|
|
298
|
-
def process_std_stream(self, name: str, value: str, stderr: bool):
|
329
|
+
def process_std_stream(self, name: str, value: str, stderr: bool) -> Any:
|
299
330
|
"""
|
300
331
|
Process a description of a standard stread from a command to get how it becomes
|
301
332
|
a workflow parameter for later actions.
|
@@ -310,15 +341,19 @@ class Command(JSONLike):
|
|
310
341
|
If true, this is handling the stderr stream. If false, the stdout stream.
|
311
342
|
"""
|
312
343
|
|
313
|
-
def _parse_list(
|
344
|
+
def _parse_list(
|
345
|
+
lst_str: str, item_type: str = "str", delim: str = " "
|
346
|
+
) -> list[Any]:
|
314
347
|
return [parse_types[item_type](i) for i in lst_str.split(delim)]
|
315
348
|
|
316
|
-
def _parse_array(
|
349
|
+
def _parse_array(
|
350
|
+
arr_str: str, item_type: str = "float", delim: str = " "
|
351
|
+
) -> np.ndarray[Any, np.dtype[Any]]:
|
317
352
|
return np.array(
|
318
353
|
_parse_list(lst_str=arr_str, item_type=item_type, delim=delim)
|
319
354
|
)
|
320
355
|
|
321
|
-
def _parse_bool(bool_str):
|
356
|
+
def _parse_bool(bool_str: str) -> bool:
|
322
357
|
bool_str = bool_str.lower()
|
323
358
|
if bool_str in ("true", "1"):
|
324
359
|
return True
|
@@ -331,7 +366,7 @@ class Command(JSONLike):
|
|
331
366
|
f"{self.stderr if stderr else self.stdout!r}."
|
332
367
|
)
|
333
368
|
|
334
|
-
parse_types = {
|
369
|
+
parse_types: dict[str, Callable[[str], Any]] = {
|
335
370
|
"str": str,
|
336
371
|
"int": int,
|
337
372
|
"float": float,
|
@@ -341,6 +376,7 @@ class Command(JSONLike):
|
|
341
376
|
}
|
342
377
|
types_pattern = "|".join(parse_types)
|
343
378
|
|
379
|
+
# TODO: use str.removeprefix in 3.9 onwards
|
344
380
|
out_name = name.replace("outputs.", "")
|
345
381
|
pattern = (
|
346
382
|
r"(\<\<(?:({types_pattern})(?:\[(.*)\])?\()?parameter:{name}(?:\.(\w+)"
|
@@ -348,42 +384,39 @@ class Command(JSONLike):
|
|
348
384
|
)
|
349
385
|
pattern = pattern.format(types_pattern=types_pattern, name=out_name)
|
350
386
|
spec = self.stderr if stderr else self.stdout
|
351
|
-
|
387
|
+
assert spec is not None
|
388
|
+
self._app.submission_logger.info(
|
352
389
|
f"processing shell standard stream according to spec: {spec!r}"
|
353
390
|
)
|
354
|
-
param = self.
|
355
|
-
match
|
356
|
-
try:
|
357
|
-
groups = match.groups()
|
358
|
-
except AttributeError:
|
391
|
+
param = self._app.Parameter(out_name)
|
392
|
+
if (match := re.match(pattern, spec)) is None:
|
359
393
|
return value
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
394
|
+
groups = match.groups()
|
395
|
+
parse_type, parse_args_str = groups[1:3]
|
396
|
+
parse_args = self.__prepare_kwargs_from_string(
|
397
|
+
args_str=parse_args_str,
|
398
|
+
doubled_quoted_args=["delim"],
|
399
|
+
)
|
400
|
+
if param._value_class:
|
401
|
+
method, method_args_str = groups[3:5]
|
402
|
+
method_args = self.__prepare_kwargs_from_string(
|
403
|
+
args_str=method_args_str,
|
364
404
|
doubled_quoted_args=["delim"],
|
365
405
|
)
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
doubled_quoted_args=["delim"],
|
371
|
-
)
|
372
|
-
method = method or "CLI_parse"
|
373
|
-
value = getattr(param._value_class, method)(value, **method_args)
|
374
|
-
if parse_type:
|
375
|
-
value = parse_types[parse_type](value, **parse_args)
|
406
|
+
method = method or "CLI_parse"
|
407
|
+
value = getattr(param._value_class, method)(value, **method_args)
|
408
|
+
if parse_type:
|
409
|
+
value = parse_types[parse_type](value, **parse_args)
|
376
410
|
|
377
411
|
return value
|
378
412
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
413
|
+
__EXE_RE: ClassVar[Pattern] = re.compile(r"\<\<(?:executable):(.*?)\>\>")
|
414
|
+
|
415
|
+
@classmethod
|
416
|
+
def _extract_executable_labels(cls, cmd_str: str) -> Sequence[str]:
|
417
|
+
return cls.__EXE_RE.findall(cmd_str)
|
383
418
|
|
384
|
-
def get_required_executables(self) ->
|
419
|
+
def get_required_executables(self) -> Sequence[str]:
|
385
420
|
"""Return executable labels required by this command."""
|
386
421
|
# an executable label might appear in the `command` or `executable` attribute:
|
387
|
-
|
388
|
-
exe_labels = self._extract_executable_labels(cmd_str)
|
389
|
-
return exe_labels
|
422
|
+
return self._extract_executable_labels(self.__get_initial_command_line())
|