hpcflow-new2 0.2.0a188__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.
- hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/sdk/__init__.py +21 -15
- hpcflow/sdk/app.py +2133 -770
- hpcflow/sdk/cli.py +281 -250
- hpcflow/sdk/cli_common.py +6 -2
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +77 -42
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +578 -311
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +112 -85
- hpcflow/sdk/config/types.py +145 -0
- hpcflow/sdk/core/actions.py +1054 -994
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +81 -63
- hpcflow/sdk/core/command_files.py +275 -185
- hpcflow/sdk/core/commands.py +111 -107
- hpcflow/sdk/core/element.py +724 -503
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +398 -51
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +380 -334
- hpcflow/sdk/core/loop_cache.py +160 -43
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +728 -600
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +33 -22
- hpcflow/sdk/core/task.py +1546 -1325
- hpcflow/sdk/core/task_schema.py +240 -196
- hpcflow/sdk/core/test_utils.py +126 -88
- hpcflow/sdk/core/types.py +387 -0
- hpcflow/sdk/core/utils.py +410 -305
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +1192 -1028
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/demo/cli.py +46 -33
- hpcflow/sdk/helper/cli.py +18 -16
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +83 -59
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +988 -586
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +408 -153
- hpcflow/sdk/persistence/pending.py +158 -123
- hpcflow/sdk/persistence/store_resource.py +37 -22
- hpcflow/sdk/persistence/types.py +307 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +477 -420
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +444 -404
- hpcflow/sdk/submission/schedulers/__init__.py +133 -40
- hpcflow/sdk/submission/schedulers/direct.py +97 -71
- hpcflow/sdk/submission/schedulers/sge.py +132 -126
- hpcflow/sdk/submission/schedulers/slurm.py +263 -268
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +102 -29
- hpcflow/sdk/submission/shells/bash.py +72 -55
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +37 -29
- hpcflow/sdk/submission/submission.py +203 -257
- hpcflow/sdk/submission/types.py +143 -0
- hpcflow/sdk/typing.py +163 -12
- hpcflow/tests/conftest.py +8 -6
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_main_scripts.py +60 -30
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
- hpcflow/tests/unit/test_action.py +86 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +13 -6
- hpcflow/tests/unit/test_cli.py +1 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +20 -15
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +3 -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_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +65 -58
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +16 -7
- hpcflow/tests/unit/test_persistence.py +48 -35
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +8 -3
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +3 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +39 -19
- 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/workflows/test_jobscript.py +2 -1
- hpcflow/tests/workflows/test_workflows.py +18 -13
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
- hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
- hpcflow/sdk/core/parallel.py +0 -21
- hpcflow_new2-0.2.0a188.dist-info/RECORD +0 -158
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/actions.py
CHANGED
@@ -5,22 +5,21 @@ they may be grouped together within a jobscript for efficiency.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
|
+
from collections.abc import Mapping
|
8
9
|
import copy
|
9
10
|
from dataclasses import dataclass
|
10
|
-
from datetime import datetime
|
11
|
-
import enum
|
12
11
|
import json
|
13
12
|
from pathlib import Path
|
14
13
|
import re
|
15
14
|
from textwrap import indent, dedent
|
16
|
-
from typing import
|
17
|
-
|
18
|
-
from valida.conditions import ConditionLike
|
15
|
+
from typing import cast, final, overload, TYPE_CHECKING
|
16
|
+
from typing_extensions import override
|
19
17
|
|
20
18
|
from watchdog.utils.dirsnapshot import DirectorySnapshotDiff
|
21
19
|
|
22
|
-
from hpcflow.sdk import app
|
23
20
|
from hpcflow.sdk.core import ABORT_EXIT_CODE
|
21
|
+
from hpcflow.sdk.core.app_aware import AppAware
|
22
|
+
from hpcflow.sdk.core.enums import ActionScopeType, EARStatus
|
24
23
|
from hpcflow.sdk.core.errors import (
|
25
24
|
ActionEnvironmentMissingNameError,
|
26
25
|
MissingCompatibleActionEnvironment,
|
@@ -30,6 +29,8 @@ from hpcflow.sdk.core.errors import (
|
|
30
29
|
UnsupportedScriptDataFormat,
|
31
30
|
)
|
32
31
|
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
32
|
+
from hpcflow.sdk.core.parameters import ParameterValue
|
33
|
+
from hpcflow.sdk.typing import ParamSource, hydrate
|
33
34
|
from hpcflow.sdk.core.utils import (
|
34
35
|
JSONLikeDirSnapShot,
|
35
36
|
split_param_label,
|
@@ -37,129 +38,49 @@ from hpcflow.sdk.core.utils import (
|
|
37
38
|
)
|
38
39
|
from hpcflow.sdk.log import TimeIt
|
39
40
|
from hpcflow.sdk.core.run_dir_files import RunDirAppFiles
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
41
|
+
from hpcflow.sdk.submission.enums import SubmissionStatus
|
42
|
+
|
43
|
+
if TYPE_CHECKING:
|
44
|
+
from collections.abc import Callable, Container, Iterable, Iterator, Sequence
|
45
|
+
from datetime import datetime
|
46
|
+
from re import Pattern
|
47
|
+
from typing import Any, ClassVar, Literal
|
48
|
+
from typing_extensions import Self
|
49
|
+
from valida.conditions import ConditionLike # type: ignore
|
50
|
+
|
51
|
+
from ..typing import DataIndex, ParamSource
|
52
|
+
from ..submission.jobscript import Jobscript
|
53
|
+
from .commands import Command
|
54
|
+
from .command_files import InputFileGenerator, OutputFileParser, FileSpec
|
55
|
+
from .element import (
|
56
|
+
Element,
|
57
|
+
ElementIteration,
|
58
|
+
ElementInputs,
|
59
|
+
ElementOutputs,
|
60
|
+
ElementResources,
|
61
|
+
ElementInputFiles,
|
62
|
+
ElementOutputFiles,
|
63
|
+
)
|
64
|
+
from .environment import Environment
|
65
|
+
from .parameters import SchemaParameter, Parameter
|
66
|
+
from .rule import Rule
|
67
|
+
from .task import WorkflowTask
|
68
|
+
from .task_schema import TaskSchema
|
69
|
+
from .types import ParameterDependence, ScriptData
|
70
|
+
from .workflow import Workflow
|
60
71
|
|
61
72
|
|
62
73
|
#: Keyword arguments permitted for particular scopes.
|
63
|
-
ACTION_SCOPE_ALLOWED_KWARGS = {
|
64
|
-
ActionScopeType.ANY.name:
|
65
|
-
ActionScopeType.MAIN.name:
|
66
|
-
ActionScopeType.PROCESSING.name:
|
67
|
-
ActionScopeType.INPUT_FILE_GENERATOR.name: {"file"},
|
68
|
-
ActionScopeType.OUTPUT_FILE_PARSER.name: {"output"},
|
74
|
+
ACTION_SCOPE_ALLOWED_KWARGS: Mapping[str, frozenset[str]] = {
|
75
|
+
ActionScopeType.ANY.name: frozenset(),
|
76
|
+
ActionScopeType.MAIN.name: frozenset(),
|
77
|
+
ActionScopeType.PROCESSING.name: frozenset(),
|
78
|
+
ActionScopeType.INPUT_FILE_GENERATOR.name: frozenset({"file"}),
|
79
|
+
ActionScopeType.OUTPUT_FILE_PARSER.name: frozenset({"output"}),
|
69
80
|
}
|
70
81
|
|
71
82
|
|
72
|
-
class
|
73
|
-
"""Enumeration of all possible EAR statuses, and their associated status colour."""
|
74
|
-
|
75
|
-
def __new__(cls, value, symbol, colour, doc=None):
|
76
|
-
member = object.__new__(cls)
|
77
|
-
member._value_ = value
|
78
|
-
member.colour = colour
|
79
|
-
member.symbol = symbol
|
80
|
-
member.__doc__ = doc
|
81
|
-
return member
|
82
|
-
|
83
|
-
#: Not yet associated with a submission.
|
84
|
-
pending = (
|
85
|
-
0,
|
86
|
-
".",
|
87
|
-
"grey46",
|
88
|
-
"Not yet associated with a submission.",
|
89
|
-
)
|
90
|
-
#: Associated with a prepared submission that is not yet submitted.
|
91
|
-
prepared = (
|
92
|
-
1,
|
93
|
-
".",
|
94
|
-
"grey46",
|
95
|
-
"Associated with a prepared submission that is not yet submitted.",
|
96
|
-
)
|
97
|
-
#: Submitted for execution.
|
98
|
-
submitted = (
|
99
|
-
2,
|
100
|
-
".",
|
101
|
-
"grey46",
|
102
|
-
"Submitted for execution.",
|
103
|
-
)
|
104
|
-
#: Executing now.
|
105
|
-
running = (
|
106
|
-
3,
|
107
|
-
"●",
|
108
|
-
"dodger_blue1",
|
109
|
-
"Executing now.",
|
110
|
-
)
|
111
|
-
#: Not attempted due to a failure of an upstream action on which this depends,
|
112
|
-
#: or a loop termination condition being satisfied.
|
113
|
-
skipped = (
|
114
|
-
4,
|
115
|
-
"s",
|
116
|
-
"dark_orange",
|
117
|
-
(
|
118
|
-
"Not attempted due to a failure of an upstream action on which this depends, "
|
119
|
-
"or a loop termination condition being satisfied."
|
120
|
-
),
|
121
|
-
)
|
122
|
-
#: Aborted by the user; downstream actions will be attempted.
|
123
|
-
aborted = (
|
124
|
-
5,
|
125
|
-
"A",
|
126
|
-
"deep_pink4",
|
127
|
-
"Aborted by the user; downstream actions will be attempted.",
|
128
|
-
)
|
129
|
-
#: Probably exited successfully.
|
130
|
-
success = (
|
131
|
-
6,
|
132
|
-
"■",
|
133
|
-
"green3",
|
134
|
-
"Probably exited successfully.",
|
135
|
-
)
|
136
|
-
#: Probably failed.
|
137
|
-
error = (
|
138
|
-
7,
|
139
|
-
"E",
|
140
|
-
"red3",
|
141
|
-
"Probably failed.",
|
142
|
-
)
|
143
|
-
|
144
|
-
@classmethod
|
145
|
-
def get_non_running_submitted_states(cls):
|
146
|
-
"""Return the set of all non-running states, excluding those before submission."""
|
147
|
-
return {
|
148
|
-
cls.skipped,
|
149
|
-
cls.aborted,
|
150
|
-
cls.success,
|
151
|
-
cls.error,
|
152
|
-
}
|
153
|
-
|
154
|
-
@property
|
155
|
-
def rich_repr(self):
|
156
|
-
"""
|
157
|
-
The rich representation of the value.
|
158
|
-
"""
|
159
|
-
return f"[{self.colour}]{self.symbol}[/{self.colour}]"
|
160
|
-
|
161
|
-
|
162
|
-
class ElementActionRun:
|
83
|
+
class ElementActionRun(AppAware):
|
163
84
|
"""
|
164
85
|
The Element Action Run (EAR) is an atomic unit of an enacted workflow, representing
|
165
86
|
one unit of work (e.g., particular submitted job to run a program) within that
|
@@ -204,26 +125,24 @@ class ElementActionRun:
|
|
204
125
|
Where to run the EAR (if not locally).
|
205
126
|
"""
|
206
127
|
|
207
|
-
_app_attr = "app"
|
208
|
-
|
209
128
|
def __init__(
|
210
129
|
self,
|
211
130
|
id_: int,
|
212
131
|
is_pending: bool,
|
213
|
-
element_action,
|
132
|
+
element_action: ElementAction,
|
214
133
|
index: int,
|
215
|
-
data_idx:
|
216
|
-
commands_idx:
|
217
|
-
start_time:
|
218
|
-
end_time:
|
219
|
-
snapshot_start:
|
220
|
-
snapshot_end:
|
221
|
-
submission_idx:
|
222
|
-
success:
|
134
|
+
data_idx: DataIndex,
|
135
|
+
commands_idx: list[int],
|
136
|
+
start_time: datetime | None,
|
137
|
+
end_time: datetime | None,
|
138
|
+
snapshot_start: dict[str, Any] | None,
|
139
|
+
snapshot_end: dict[str, Any] | None,
|
140
|
+
submission_idx: int | None,
|
141
|
+
success: bool | None,
|
223
142
|
skip: bool,
|
224
|
-
exit_code:
|
225
|
-
metadata:
|
226
|
-
run_hostname:
|
143
|
+
exit_code: int | None,
|
144
|
+
metadata: dict[str, Any],
|
145
|
+
run_hostname: str | None,
|
227
146
|
) -> None:
|
228
147
|
self._id = id_
|
229
148
|
self._is_pending = is_pending
|
@@ -243,16 +162,16 @@ class ElementActionRun:
|
|
243
162
|
self._run_hostname = run_hostname
|
244
163
|
|
245
164
|
# assigned on first access of corresponding properties:
|
246
|
-
self._inputs = None
|
247
|
-
self._outputs = None
|
248
|
-
self._resources = None
|
249
|
-
self._input_files = None
|
250
|
-
self._output_files = None
|
251
|
-
self._ss_start_obj = None
|
252
|
-
self._ss_end_obj = None
|
253
|
-
self._ss_diff_obj = None
|
165
|
+
self._inputs: ElementInputs | None = None
|
166
|
+
self._outputs: ElementOutputs | None = None
|
167
|
+
self._resources: ElementResources | None = None
|
168
|
+
self._input_files: ElementInputFiles | None = None
|
169
|
+
self._output_files: ElementOutputFiles | None = None
|
170
|
+
self._ss_start_obj: JSONLikeDirSnapShot | None = None
|
171
|
+
self._ss_end_obj: JSONLikeDirSnapShot | None = None
|
172
|
+
self._ss_diff_obj: DirectorySnapshotDiff | None = None
|
254
173
|
|
255
|
-
def __repr__(self):
|
174
|
+
def __repr__(self) -> str:
|
256
175
|
return (
|
257
176
|
f"{self.__class__.__name__}("
|
258
177
|
f"id={self.id_!r}, index={self.index!r}, "
|
@@ -274,110 +193,110 @@ class ElementActionRun:
|
|
274
193
|
return self._is_pending
|
275
194
|
|
276
195
|
@property
|
277
|
-
def element_action(self):
|
196
|
+
def element_action(self) -> ElementAction:
|
278
197
|
"""
|
279
198
|
The particular element action that this is a run of.
|
280
199
|
"""
|
281
200
|
return self._element_action
|
282
201
|
|
283
202
|
@property
|
284
|
-
def index(self):
|
203
|
+
def index(self) -> int:
|
285
204
|
"""Run index."""
|
286
205
|
return self._index
|
287
206
|
|
288
207
|
@property
|
289
|
-
def action(self):
|
208
|
+
def action(self) -> Action:
|
290
209
|
"""
|
291
210
|
The action this is a run of.
|
292
211
|
"""
|
293
212
|
return self.element_action.action
|
294
213
|
|
295
214
|
@property
|
296
|
-
def element_iteration(self):
|
215
|
+
def element_iteration(self) -> ElementIteration:
|
297
216
|
"""
|
298
217
|
The iteration information of this run.
|
299
218
|
"""
|
300
219
|
return self.element_action.element_iteration
|
301
220
|
|
302
221
|
@property
|
303
|
-
def element(self):
|
222
|
+
def element(self) -> Element:
|
304
223
|
"""
|
305
224
|
The element this is a run of.
|
306
225
|
"""
|
307
226
|
return self.element_iteration.element
|
308
227
|
|
309
228
|
@property
|
310
|
-
def workflow(self):
|
229
|
+
def workflow(self) -> Workflow:
|
311
230
|
"""
|
312
231
|
The workflow this is a run of.
|
313
232
|
"""
|
314
233
|
return self.element_iteration.workflow
|
315
234
|
|
316
235
|
@property
|
317
|
-
def data_idx(self):
|
236
|
+
def data_idx(self) -> DataIndex:
|
318
237
|
"""
|
319
238
|
Used for looking up input data to the EAR.
|
320
239
|
"""
|
321
240
|
return self._data_idx
|
322
241
|
|
323
242
|
@property
|
324
|
-
def commands_idx(self):
|
243
|
+
def commands_idx(self) -> Sequence[int]:
|
325
244
|
"""
|
326
245
|
Indices of commands to apply.
|
327
246
|
"""
|
328
247
|
return self._commands_idx
|
329
248
|
|
330
249
|
@property
|
331
|
-
def metadata(self):
|
250
|
+
def metadata(self) -> Mapping[str, Any]:
|
332
251
|
"""
|
333
252
|
Metadata about the EAR.
|
334
253
|
"""
|
335
254
|
return self._metadata
|
336
255
|
|
337
256
|
@property
|
338
|
-
def run_hostname(self):
|
257
|
+
def run_hostname(self) -> str | None:
|
339
258
|
"""
|
340
259
|
Where to run the EAR, if known/specified.
|
341
260
|
"""
|
342
261
|
return self._run_hostname
|
343
262
|
|
344
263
|
@property
|
345
|
-
def start_time(self):
|
264
|
+
def start_time(self) -> datetime | None:
|
346
265
|
"""
|
347
266
|
When the EAR started.
|
348
267
|
"""
|
349
268
|
return self._start_time
|
350
269
|
|
351
270
|
@property
|
352
|
-
def end_time(self):
|
271
|
+
def end_time(self) -> datetime | None:
|
353
272
|
"""
|
354
273
|
When the EAR finished.
|
355
274
|
"""
|
356
275
|
return self._end_time
|
357
276
|
|
358
277
|
@property
|
359
|
-
def submission_idx(self):
|
278
|
+
def submission_idx(self) -> int | None:
|
360
279
|
"""
|
361
280
|
What actual submission index was this?
|
362
281
|
"""
|
363
282
|
return self._submission_idx
|
364
283
|
|
365
284
|
@property
|
366
|
-
def success(self):
|
285
|
+
def success(self) -> bool | None:
|
367
286
|
"""
|
368
287
|
Did the EAR succeed?
|
369
288
|
"""
|
370
289
|
return self._success
|
371
290
|
|
372
291
|
@property
|
373
|
-
def skip(self):
|
292
|
+
def skip(self) -> bool:
|
374
293
|
"""
|
375
294
|
Was the EAR skipped?
|
376
295
|
"""
|
377
296
|
return self._skip
|
378
297
|
|
379
298
|
@property
|
380
|
-
def snapshot_start(self):
|
299
|
+
def snapshot_start(self) -> JSONLikeDirSnapShot | None:
|
381
300
|
"""
|
382
301
|
The snapshot of the data directory at the start of the run.
|
383
302
|
"""
|
@@ -389,7 +308,7 @@ class ElementActionRun:
|
|
389
308
|
return self._ss_start_obj
|
390
309
|
|
391
310
|
@property
|
392
|
-
def snapshot_end(self):
|
311
|
+
def snapshot_end(self) -> JSONLikeDirSnapShot | None:
|
393
312
|
"""
|
394
313
|
The snapshot of the data directory at the end of the run.
|
395
314
|
"""
|
@@ -398,32 +317,34 @@ class ElementActionRun:
|
|
398
317
|
return self._ss_end_obj
|
399
318
|
|
400
319
|
@property
|
401
|
-
def dir_diff(self) -> DirectorySnapshotDiff:
|
320
|
+
def dir_diff(self) -> DirectorySnapshotDiff | None:
|
402
321
|
"""
|
403
322
|
The changes to the EAR working directory due to the execution of this EAR.
|
404
323
|
"""
|
405
|
-
if
|
406
|
-
self._ss_diff_obj
|
407
|
-
|
408
|
-
)
|
324
|
+
if (
|
325
|
+
not self._ss_diff_obj
|
326
|
+
and (ss := self.snapshot_start)
|
327
|
+
and (se := self.snapshot_end)
|
328
|
+
):
|
329
|
+
self._ss_diff_obj = DirectorySnapshotDiff(ss, se)
|
409
330
|
return self._ss_diff_obj
|
410
331
|
|
411
332
|
@property
|
412
|
-
def exit_code(self):
|
333
|
+
def exit_code(self) -> int | None:
|
413
334
|
"""
|
414
335
|
The exit code of the underlying program run by the EAR, if known.
|
415
336
|
"""
|
416
337
|
return self._exit_code
|
417
338
|
|
418
339
|
@property
|
419
|
-
def task(self):
|
340
|
+
def task(self) -> WorkflowTask:
|
420
341
|
"""
|
421
342
|
The task that this EAR is part of the implementation of.
|
422
343
|
"""
|
423
344
|
return self.element_action.task
|
424
345
|
|
425
346
|
@property
|
426
|
-
def status(self):
|
347
|
+
def status(self) -> EARStatus:
|
427
348
|
"""
|
428
349
|
The state of this EAR.
|
429
350
|
"""
|
@@ -445,18 +366,16 @@ class ElementActionRun:
|
|
445
366
|
elif self.submission_idx is not None:
|
446
367
|
wk_sub_stat = self.workflow.submissions[self.submission_idx].status
|
447
368
|
|
448
|
-
if wk_sub_stat
|
369
|
+
if wk_sub_stat == SubmissionStatus.PENDING:
|
449
370
|
return EARStatus.prepared
|
450
|
-
|
451
|
-
elif wk_sub_stat.name == "SUBMITTED":
|
371
|
+
elif wk_sub_stat == SubmissionStatus.SUBMITTED:
|
452
372
|
return EARStatus.submitted
|
453
|
-
|
454
373
|
else:
|
455
374
|
RuntimeError(f"Workflow submission status not understood: {wk_sub_stat}.")
|
456
375
|
|
457
376
|
return EARStatus.pending
|
458
377
|
|
459
|
-
def get_parameter_names(self, prefix: str) ->
|
378
|
+
def get_parameter_names(self, prefix: str) -> Sequence[str]:
|
460
379
|
"""Get parameter types associated with a given prefix.
|
461
380
|
|
462
381
|
For inputs, labels are ignored. See `Action.get_parameter_names` for more
|
@@ -466,11 +385,10 @@ class ElementActionRun:
|
|
466
385
|
----------
|
467
386
|
prefix
|
468
387
|
One of "inputs", "outputs", "input_files", "output_files".
|
469
|
-
|
470
388
|
"""
|
471
389
|
return self.action.get_parameter_names(prefix)
|
472
390
|
|
473
|
-
def get_data_idx(self, path: str = None):
|
391
|
+
def get_data_idx(self, path: str | None = None) -> DataIndex:
|
474
392
|
"""
|
475
393
|
Get the data index of a value in the most recent iteration.
|
476
394
|
|
@@ -485,14 +403,37 @@ class ElementActionRun:
|
|
485
403
|
run_idx=self.index,
|
486
404
|
)
|
487
405
|
|
406
|
+
@overload
|
407
|
+
def get_parameter_sources(
|
408
|
+
self,
|
409
|
+
*,
|
410
|
+
path: str | None = None,
|
411
|
+
typ: str | None = None,
|
412
|
+
as_strings: Literal[False] = False,
|
413
|
+
use_task_index: bool = False,
|
414
|
+
) -> Mapping[str, ParamSource | list[ParamSource]]:
|
415
|
+
...
|
416
|
+
|
417
|
+
@overload
|
418
|
+
def get_parameter_sources(
|
419
|
+
self,
|
420
|
+
*,
|
421
|
+
path: str | None = None,
|
422
|
+
typ: str | None = None,
|
423
|
+
as_strings: Literal[True],
|
424
|
+
use_task_index: bool = False,
|
425
|
+
) -> Mapping[str, str]:
|
426
|
+
...
|
427
|
+
|
488
428
|
@TimeIt.decorator
|
489
429
|
def get_parameter_sources(
|
490
430
|
self,
|
491
|
-
|
492
|
-
|
431
|
+
*,
|
432
|
+
path: str | None = None,
|
433
|
+
typ: str | None = None,
|
493
434
|
as_strings: bool = False,
|
494
435
|
use_task_index: bool = False,
|
495
|
-
):
|
436
|
+
) -> Mapping[str, str] | Mapping[str, ParamSource | list[ParamSource]]:
|
496
437
|
"""
|
497
438
|
Get the source or sources of a parameter in the most recent iteration.
|
498
439
|
|
@@ -507,22 +448,31 @@ class ElementActionRun:
|
|
507
448
|
use_task_index:
|
508
449
|
Whether to use the task index.
|
509
450
|
"""
|
451
|
+
if as_strings:
|
452
|
+
return self.element_iteration.get_parameter_sources(
|
453
|
+
path,
|
454
|
+
action_idx=self.element_action.action_idx,
|
455
|
+
run_idx=self.index,
|
456
|
+
typ=typ,
|
457
|
+
as_strings=True,
|
458
|
+
use_task_index=use_task_index,
|
459
|
+
)
|
510
460
|
return self.element_iteration.get_parameter_sources(
|
511
461
|
path,
|
512
462
|
action_idx=self.element_action.action_idx,
|
513
463
|
run_idx=self.index,
|
514
464
|
typ=typ,
|
515
|
-
as_strings=
|
465
|
+
as_strings=False,
|
516
466
|
use_task_index=use_task_index,
|
517
467
|
)
|
518
468
|
|
519
469
|
def get(
|
520
470
|
self,
|
521
|
-
path: str = None,
|
522
|
-
default: Any = None,
|
471
|
+
path: str | None = None,
|
472
|
+
default: Any | None = None,
|
523
473
|
raise_on_missing: bool = False,
|
524
474
|
raise_on_unset: bool = False,
|
525
|
-
):
|
475
|
+
) -> Any:
|
526
476
|
"""
|
527
477
|
Get a value (parameter, input, output, etc.) from the most recent iteration.
|
528
478
|
|
@@ -548,153 +498,169 @@ class ElementActionRun:
|
|
548
498
|
raise_on_unset=raise_on_unset,
|
549
499
|
)
|
550
500
|
|
551
|
-
@
|
552
|
-
def get_EAR_dependencies(self, as_objects=False):
|
553
|
-
|
501
|
+
@overload
|
502
|
+
def get_EAR_dependencies(self, as_objects: Literal[False] = False) -> set[int]:
|
503
|
+
...
|
554
504
|
|
555
|
-
|
505
|
+
@overload
|
506
|
+
def get_EAR_dependencies(self, as_objects: Literal[True]) -> list[ElementActionRun]:
|
507
|
+
...
|
508
|
+
|
509
|
+
@TimeIt.decorator
|
510
|
+
def get_EAR_dependencies(self, as_objects=False) -> list[ElementActionRun] | set[int]:
|
511
|
+
"""Get EARs that this EAR depends on, or just their IDs."""
|
512
|
+
out: set[int] = set()
|
556
513
|
for src in self.get_parameter_sources(typ="EAR_output").values():
|
557
|
-
if
|
558
|
-
|
559
|
-
for src_i in src:
|
560
|
-
EAR_ID_i = src_i["EAR_ID"]
|
514
|
+
for src_i in src if isinstance(src, list) else [src]:
|
515
|
+
EAR_ID_i: int = src_i["EAR_ID"]
|
561
516
|
if EAR_ID_i != self.id_:
|
562
517
|
# don't record a self dependency!
|
563
|
-
out.
|
564
|
-
|
565
|
-
out = sorted(out)
|
518
|
+
out.add(EAR_ID_i)
|
566
519
|
|
567
520
|
if as_objects:
|
568
|
-
|
569
|
-
|
521
|
+
return self.workflow.get_EARs_from_IDs(sorted(out))
|
570
522
|
return out
|
571
523
|
|
572
|
-
def get_input_dependencies(self):
|
524
|
+
def get_input_dependencies(self) -> Mapping[str, ParamSource]:
|
573
525
|
"""Get information about locally defined input, sequence, and schema-default
|
574
526
|
values that this EAR depends on. Note this does not get values from this EAR's
|
575
527
|
task/schema, because the aim of this method is to help determine which upstream
|
576
528
|
tasks this EAR depends on."""
|
577
529
|
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
for v_i in v
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
530
|
+
wanted_types = ("local_input", "default_input")
|
531
|
+
return {
|
532
|
+
k: v_i
|
533
|
+
for k, v in self.get_parameter_sources().items()
|
534
|
+
for v_i in (v if isinstance(v, list) else [v])
|
535
|
+
if (
|
536
|
+
v_i["type"] in wanted_types
|
537
|
+
and v_i["task_insert_ID"] != self.task.insert_ID
|
538
|
+
)
|
539
|
+
}
|
540
|
+
|
541
|
+
@overload
|
542
|
+
def get_dependent_EARs(self, as_objects: Literal[False] = False) -> set[int]:
|
543
|
+
...
|
588
544
|
|
589
|
-
|
545
|
+
@overload
|
546
|
+
def get_dependent_EARs(self, as_objects: Literal[True]) -> list[ElementActionRun]:
|
547
|
+
...
|
590
548
|
|
591
549
|
def get_dependent_EARs(
|
592
|
-
self, as_objects=False
|
593
|
-
) ->
|
550
|
+
self, as_objects: bool = False
|
551
|
+
) -> list[ElementActionRun] | set[int]:
|
594
552
|
"""Get downstream EARs that depend on this EAR."""
|
595
|
-
deps =
|
596
|
-
|
597
|
-
for
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
deps = sorted(deps)
|
553
|
+
deps = {
|
554
|
+
run.id_
|
555
|
+
for task in self.workflow.tasks[self.task.index :]
|
556
|
+
for elem in task.elements[:]
|
557
|
+
for iter_ in elem.iterations
|
558
|
+
for run in iter_.action_runs
|
559
|
+
# does EAR dependency belong to self?
|
560
|
+
if self._id in run.get_EAR_dependencies()
|
561
|
+
}
|
605
562
|
if as_objects:
|
606
|
-
|
607
|
-
|
563
|
+
return self.workflow.get_EARs_from_IDs(sorted(deps))
|
608
564
|
return deps
|
609
565
|
|
610
566
|
@property
|
611
|
-
def inputs(self):
|
567
|
+
def inputs(self) -> ElementInputs:
|
612
568
|
"""
|
613
569
|
The inputs to this EAR.
|
614
570
|
"""
|
615
571
|
if not self._inputs:
|
616
|
-
self._inputs = self.
|
572
|
+
self._inputs = self._app.ElementInputs(element_action_run=self)
|
617
573
|
return self._inputs
|
618
574
|
|
619
575
|
@property
|
620
|
-
def outputs(self):
|
576
|
+
def outputs(self) -> ElementOutputs:
|
621
577
|
"""
|
622
578
|
The outputs from this EAR.
|
623
579
|
"""
|
624
580
|
if not self._outputs:
|
625
|
-
self._outputs = self.
|
581
|
+
self._outputs = self._app.ElementOutputs(element_action_run=self)
|
626
582
|
return self._outputs
|
627
583
|
|
628
584
|
@property
|
629
585
|
@TimeIt.decorator
|
630
|
-
def resources(self):
|
586
|
+
def resources(self) -> ElementResources:
|
631
587
|
"""
|
632
588
|
The resources to use with (or used by) this EAR.
|
633
589
|
"""
|
634
590
|
if not self._resources:
|
635
|
-
self._resources = self.
|
591
|
+
self._resources = self.__get_resources_obj()
|
636
592
|
return self._resources
|
637
593
|
|
638
594
|
@property
|
639
|
-
def input_files(self):
|
595
|
+
def input_files(self) -> ElementInputFiles:
|
640
596
|
"""
|
641
597
|
The input files to the controlled program.
|
642
598
|
"""
|
643
599
|
if not self._input_files:
|
644
|
-
self._input_files = self.
|
600
|
+
self._input_files = self._app.ElementInputFiles(element_action_run=self)
|
645
601
|
return self._input_files
|
646
602
|
|
647
603
|
@property
|
648
|
-
def output_files(self):
|
604
|
+
def output_files(self) -> ElementOutputFiles:
|
649
605
|
"""
|
650
606
|
The output files from the controlled program.
|
651
607
|
"""
|
652
608
|
if not self._output_files:
|
653
|
-
self._output_files = self.
|
609
|
+
self._output_files = self._app.ElementOutputFiles(element_action_run=self)
|
654
610
|
return self._output_files
|
655
611
|
|
656
612
|
@property
|
657
|
-
def env_spec(self) ->
|
613
|
+
def env_spec(self) -> Mapping[str, Any]:
|
658
614
|
"""
|
659
615
|
Environment details.
|
660
616
|
"""
|
661
|
-
|
617
|
+
if (envs := self.resources.environments) is None:
|
618
|
+
return {}
|
619
|
+
return envs[self.action.get_environment_name()]
|
662
620
|
|
663
621
|
@TimeIt.decorator
|
664
|
-
def get_resources(self):
|
622
|
+
def get_resources(self) -> Mapping[str, Any]:
|
665
623
|
"""Resolve specific resources for this EAR, considering all applicable scopes and
|
666
624
|
template-level resources."""
|
667
625
|
return self.element_iteration.get_resources(self.action)
|
668
626
|
|
669
|
-
|
627
|
+
@TimeIt.decorator
|
628
|
+
def __get_resources_obj(self) -> ElementResources:
|
629
|
+
"""Resolve specific resources for this EAR, considering all applicable scopes and
|
630
|
+
template-level resources."""
|
631
|
+
return self.element_iteration.get_resources_obj(self.action)
|
632
|
+
|
633
|
+
def get_environment_spec(self) -> Mapping[str, Any]:
|
670
634
|
"""
|
671
635
|
What environment to run in?
|
672
636
|
"""
|
673
637
|
return self.action.get_environment_spec()
|
674
638
|
|
675
|
-
def get_environment(self) ->
|
639
|
+
def get_environment(self) -> Environment:
|
676
640
|
"""
|
677
641
|
What environment to run in?
|
678
642
|
"""
|
679
643
|
return self.action.get_environment()
|
680
644
|
|
681
|
-
def get_all_previous_iteration_runs(
|
645
|
+
def get_all_previous_iteration_runs(
|
646
|
+
self, include_self: bool = True
|
647
|
+
) -> list[ElementActionRun]:
|
682
648
|
"""Get a list of run over all iterations that correspond to this run, optionally
|
683
649
|
including this run."""
|
684
650
|
self_iter = self.element_iteration
|
685
651
|
self_elem = self_iter.element
|
686
652
|
self_act_idx = self.element_action.action_idx
|
687
|
-
max_idx = self_iter.index + 1 if include_self else
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
653
|
+
max_idx = self_iter.index + (1 if include_self else 0)
|
654
|
+
return [
|
655
|
+
iter_i.actions[self_act_idx].runs[-1]
|
656
|
+
for iter_i in self_elem.iterations[:max_idx]
|
657
|
+
]
|
692
658
|
|
693
659
|
def get_input_values(
|
694
660
|
self,
|
695
|
-
inputs:
|
661
|
+
inputs: Sequence[str] | Mapping[str, Mapping[str, Any]] | None = None,
|
696
662
|
label_dict: bool = True,
|
697
|
-
) ->
|
663
|
+
) -> Mapping[str, Mapping[str, Any]]:
|
698
664
|
"""Get a dict of (optionally a subset of) inputs values for this run.
|
699
665
|
|
700
666
|
Parameters
|
@@ -714,75 +680,80 @@ class ElementActionRun:
|
|
714
680
|
if not inputs:
|
715
681
|
inputs = self.get_parameter_names("inputs")
|
716
682
|
|
717
|
-
out = {}
|
683
|
+
out: dict[str, dict[str, Any]] = {}
|
718
684
|
for inp_name in inputs:
|
719
|
-
|
720
|
-
|
721
|
-
try:
|
722
|
-
all_iters = inputs[inp_name]["all_iterations"]
|
723
|
-
except (TypeError, KeyError):
|
724
|
-
all_iters = False
|
725
|
-
|
726
|
-
if all_iters:
|
727
|
-
all_runs = self.get_all_previous_iteration_runs(include_self=True)
|
685
|
+
if self.__all_iters(inputs, inp_name):
|
728
686
|
val_i = {
|
729
687
|
f"iteration_{run_i.element_iteration.index}": {
|
730
688
|
"loop_idx": run_i.element_iteration.loop_idx,
|
731
689
|
"value": run_i.get(f"inputs.{inp_name}"),
|
732
690
|
}
|
733
|
-
for run_i in
|
691
|
+
for run_i in self.get_all_previous_iteration_runs(include_self=True)
|
734
692
|
}
|
735
693
|
else:
|
736
694
|
val_i = self.get(f"inputs.{inp_name}")
|
737
695
|
|
738
|
-
key = inp_name
|
739
|
-
if
|
740
|
-
key =
|
741
|
-
|
742
|
-
if "." in key:
|
743
|
-
# for sub-parameters, take only the final part as the dict key:
|
744
|
-
key = key.split(".")[-1]
|
745
|
-
|
746
|
-
if label_dict and label_i:
|
747
|
-
if key not in out:
|
748
|
-
out[key] = {}
|
749
|
-
out[key][label_i] = val_i
|
696
|
+
key, label_i = self.__split_input_name(inp_name, label_dict)
|
697
|
+
if label_i:
|
698
|
+
out.setdefault(key, {})[label_i] = val_i
|
750
699
|
else:
|
751
700
|
out[key] = val_i
|
752
701
|
|
753
702
|
if self.action.script_pass_env_spec:
|
754
|
-
out["env_spec"] = self.env_spec
|
703
|
+
out["env_spec"] = cast("Any", self.env_spec)
|
755
704
|
|
756
705
|
return out
|
757
706
|
|
758
|
-
|
707
|
+
@staticmethod
|
708
|
+
def __all_iters(
|
709
|
+
inputs: Sequence[str] | Mapping[str, Mapping[str, Any]], inp_name: str
|
710
|
+
) -> bool:
|
711
|
+
try:
|
712
|
+
return isinstance(inputs, Mapping) and bool(
|
713
|
+
inputs[inp_name]["all_iterations"]
|
714
|
+
)
|
715
|
+
except (TypeError, KeyError):
|
716
|
+
return False
|
717
|
+
|
718
|
+
@staticmethod
|
719
|
+
def __split_input_name(inp_name: str, label_dict: bool) -> tuple[str, str | None]:
|
720
|
+
key = inp_name
|
721
|
+
path, label = split_param_label(key)
|
722
|
+
if label_dict and path:
|
723
|
+
key = path # exclude label from key
|
724
|
+
# for sub-parameters, take only the final part as the dict key:
|
725
|
+
return key.split(".")[-1], (label if label_dict else None)
|
726
|
+
|
727
|
+
def get_input_values_direct(
|
728
|
+
self, label_dict: bool = True
|
729
|
+
) -> Mapping[str, Mapping[str, Any]]:
|
759
730
|
"""Get a dict of input values that are to be passed directly to a Python script
|
760
731
|
function."""
|
761
732
|
inputs = self.action.script_data_in_grouped.get("direct", {})
|
762
733
|
return self.get_input_values(inputs=inputs, label_dict=label_dict)
|
763
734
|
|
764
|
-
def get_IFG_input_values(self) ->
|
735
|
+
def get_IFG_input_values(self) -> Mapping[str, Any]:
|
765
736
|
"""
|
766
737
|
Get a dict of input values that are to be passed via an input file generator.
|
767
738
|
"""
|
768
739
|
if not self.action._from_expand:
|
769
740
|
raise RuntimeError(
|
770
|
-
|
771
|
-
|
741
|
+
"Cannot get input file generator inputs from this EAR because the "
|
742
|
+
"associated action is not expanded, meaning multiple IFGs might exists."
|
772
743
|
)
|
773
|
-
input_types =
|
774
|
-
inputs = {}
|
775
|
-
for
|
776
|
-
|
777
|
-
if typ in input_types:
|
778
|
-
inputs[typ] =
|
744
|
+
input_types = {param.typ for param in self.action.input_file_generators[0].inputs}
|
745
|
+
inputs: dict[str, Any] = {}
|
746
|
+
for inp in self.inputs:
|
747
|
+
assert isinstance(inp, self._app.ElementParameter)
|
748
|
+
if (typ := inp.path[len("inputs.") :]) in input_types:
|
749
|
+
inputs[typ] = inp.value
|
779
750
|
|
780
751
|
if self.action.script_pass_env_spec:
|
781
752
|
inputs["env_spec"] = self.env_spec
|
782
753
|
|
783
754
|
return inputs
|
784
755
|
|
785
|
-
def get_OFP_output_files(self) ->
|
756
|
+
def get_OFP_output_files(self) -> Mapping[str, Path]:
|
786
757
|
"""
|
787
758
|
Get a dict of output files that are going to be parsed to generate one or more
|
788
759
|
outputs.
|
@@ -790,118 +761,142 @@ class ElementActionRun:
|
|
790
761
|
# TODO: can this return multiple files for a given FileSpec?
|
791
762
|
if not self.action._from_expand:
|
792
763
|
raise RuntimeError(
|
793
|
-
|
794
|
-
|
764
|
+
"Cannot get output file parser files from this from EAR because the "
|
765
|
+
"associated action is not expanded, meaning multiple OFPs might exist."
|
795
766
|
)
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
767
|
+
return {
|
768
|
+
file_spec.label: Path(cast("str", file_spec.name.value()))
|
769
|
+
for file_spec in self.action.output_file_parsers[0].output_files
|
770
|
+
}
|
800
771
|
|
801
|
-
def get_OFP_inputs(self) ->
|
772
|
+
def get_OFP_inputs(self) -> Mapping[str, str | list[str] | Mapping[str, Any]]:
|
802
773
|
"""
|
803
774
|
Get a dict of input values that are to be passed to output file parsers.
|
804
775
|
"""
|
805
776
|
if not self.action._from_expand:
|
806
777
|
raise RuntimeError(
|
807
|
-
|
808
|
-
|
778
|
+
"Cannot get output file parser inputs from this from EAR because the "
|
779
|
+
"associated action is not expanded, meaning multiple OFPs might exist."
|
809
780
|
)
|
810
|
-
inputs = {
|
811
|
-
|
812
|
-
|
781
|
+
inputs: dict[str, str | list[str] | Mapping[str, Any]] = {
|
782
|
+
inp_typ: self.get(f"inputs.{inp_typ}")
|
783
|
+
for inp_typ in self.action.output_file_parsers[0].inputs or ()
|
784
|
+
}
|
813
785
|
|
814
786
|
if self.action.script_pass_env_spec:
|
815
787
|
inputs["env_spec"] = self.env_spec
|
816
788
|
|
817
789
|
return inputs
|
818
790
|
|
819
|
-
def get_OFP_outputs(self) ->
|
791
|
+
def get_OFP_outputs(self) -> Mapping[str, str | list[str]]:
|
820
792
|
"""
|
821
793
|
Get the outputs obtained by parsing an output file.
|
822
794
|
"""
|
823
795
|
if not self.action._from_expand:
|
824
796
|
raise RuntimeError(
|
825
|
-
|
826
|
-
|
797
|
+
"Cannot get output file parser outputs from this from EAR because the "
|
798
|
+
"associated action is not expanded, meaning multiple OFPs might exist."
|
827
799
|
)
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
800
|
+
return {
|
801
|
+
out_typ: self.get(f"outputs.{out_typ}")
|
802
|
+
for out_typ in self.action.output_file_parsers[0].outputs or ()
|
803
|
+
}
|
832
804
|
|
833
|
-
def write_source(self, js_idx: int, js_act_idx: int):
|
805
|
+
def write_source(self, js_idx: int, js_act_idx: int) -> None:
|
834
806
|
"""
|
835
807
|
Write values to files in standard formats.
|
836
808
|
"""
|
837
|
-
import h5py
|
838
|
-
|
839
809
|
for fmt, ins in self.action.script_data_in_grouped.items():
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
in_vals_processed = {}
|
844
|
-
for k, v in in_vals.items():
|
845
|
-
try:
|
846
|
-
v = v.prepare_JSON_dump()
|
847
|
-
except (AttributeError, NotImplementedError):
|
848
|
-
pass
|
849
|
-
in_vals_processed[k] = v
|
850
|
-
|
851
|
-
with dump_path.open("wt") as fp:
|
852
|
-
json.dump(in_vals_processed, fp)
|
853
|
-
|
854
|
-
elif fmt == "hdf5":
|
855
|
-
in_vals = self.get_input_values(inputs=ins, label_dict=False)
|
856
|
-
dump_path = self.action.get_param_dump_file_path_HDF5(js_idx, js_act_idx)
|
857
|
-
with h5py.File(dump_path, mode="w") as f:
|
858
|
-
for k, v in in_vals.items():
|
859
|
-
grp_k = f.create_group(k)
|
860
|
-
v.dump_to_HDF5_group(grp_k)
|
810
|
+
in_vals = self.get_input_values(inputs=ins, label_dict=False)
|
811
|
+
if writer := self.__source_writer_map.get(fmt):
|
812
|
+
writer(self, in_vals, js_idx, js_act_idx)
|
861
813
|
|
862
814
|
# write the script if it is specified as a app data script, otherwise we assume
|
863
815
|
# the script already exists in the working directory:
|
864
|
-
snip_path
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
816
|
+
if snip_path := self.action.get_snippet_script_path(
|
817
|
+
self.action.script, self.env_spec
|
818
|
+
):
|
819
|
+
with Path(snip_path.name).open("wt", newline="\n") as fp:
|
820
|
+
fp.write(self.action.compose_source(snip_path))
|
821
|
+
|
822
|
+
def __write_json_inputs(
|
823
|
+
self, in_vals: Mapping[str, ParameterValue], js_idx: int, js_act_idx: int
|
824
|
+
):
|
825
|
+
in_vals_processed: dict[str, Any] = {}
|
826
|
+
for k, v in in_vals.items():
|
827
|
+
try:
|
828
|
+
in_vals_processed[k] = (
|
829
|
+
v.prepare_JSON_dump() if isinstance(v, ParameterValue) else v
|
830
|
+
)
|
831
|
+
except (AttributeError, NotImplementedError):
|
832
|
+
in_vals_processed[k] = v
|
833
|
+
|
834
|
+
with self.action.get_param_dump_file_path_JSON(js_idx, js_act_idx).open(
|
835
|
+
"wt"
|
836
|
+
) as fp:
|
837
|
+
json.dump(in_vals_processed, fp)
|
838
|
+
|
839
|
+
def __write_hdf5_inputs(
|
840
|
+
self, in_vals: Mapping[str, ParameterValue], js_idx: int, js_act_idx: int
|
841
|
+
):
|
842
|
+
import h5py # type: ignore
|
843
|
+
|
844
|
+
with h5py.File(
|
845
|
+
self.action.get_param_dump_file_path_HDF5(js_idx, js_act_idx), mode="w"
|
846
|
+
) as h5file:
|
847
|
+
for k, v in in_vals.items():
|
848
|
+
v.dump_to_HDF5_group(h5file.create_group(k))
|
849
|
+
|
850
|
+
__source_writer_map: ClassVar[dict[str, Callable[..., None]]] = {
|
851
|
+
"json": __write_json_inputs,
|
852
|
+
"hdf5": __write_hdf5_inputs,
|
853
|
+
}
|
854
|
+
|
855
|
+
def __output_index(self, param_name: str) -> int:
|
856
|
+
return cast("int", self.data_idx[f"outputs.{param_name}"])
|
870
857
|
|
871
858
|
def _param_save(self, js_idx: int, js_act_idx: int):
|
872
859
|
"""Save script-generated parameters that are stored within the supported script
|
873
860
|
data output formats (HDF5, JSON, etc)."""
|
874
|
-
import h5py
|
861
|
+
import h5py # type: ignore
|
875
862
|
|
863
|
+
parameters = self._app.parameters
|
876
864
|
for fmt in self.action.script_data_out_grouped:
|
877
865
|
if fmt == "json":
|
878
|
-
|
879
|
-
|
880
|
-
|
866
|
+
with self.action.get_param_load_file_path_JSON(js_idx, js_act_idx).open(
|
867
|
+
mode="rt"
|
868
|
+
) as f:
|
869
|
+
file_data: dict[str, Any] = json.load(f)
|
881
870
|
for param_name, param_dat in file_data.items():
|
882
|
-
param_id = self.
|
883
|
-
param_cls
|
884
|
-
try:
|
871
|
+
param_id = self.__output_index(param_name)
|
872
|
+
if param_cls := parameters.get(param_name)._force_value_class():
|
885
873
|
param_cls.save_from_JSON(param_dat, param_id, self.workflow)
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
param_id=param_id, value=param_dat
|
892
|
-
)
|
874
|
+
else:
|
875
|
+
# try to save as a primitive:
|
876
|
+
self.workflow.set_parameter_value(
|
877
|
+
param_id=param_id, value=param_dat
|
878
|
+
)
|
893
879
|
|
894
880
|
elif fmt == "hdf5":
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
param_cls
|
881
|
+
with h5py.File(
|
882
|
+
self.action.get_param_load_file_path_HDF5(js_idx, js_act_idx),
|
883
|
+
mode="r",
|
884
|
+
) as h5file:
|
885
|
+
for param_name, h5_grp in h5file.items():
|
886
|
+
if param_cls := parameters.get(param_name)._force_value_class():
|
887
|
+
param_cls.save_from_HDF5_group(
|
888
|
+
h5_grp, self.__output_index(param_name), self.workflow
|
889
|
+
)
|
890
|
+
else:
|
891
|
+
# Unlike with JSON, we've no fallback so we warn
|
892
|
+
self._app.logger.warning(
|
893
|
+
"parameter %s could not be saved; serializer not found",
|
894
|
+
param_name,
|
895
|
+
)
|
901
896
|
|
902
897
|
def compose_commands(
|
903
|
-
self, jobscript:
|
904
|
-
) ->
|
898
|
+
self, jobscript: Jobscript, JS_action_idx: int
|
899
|
+
) -> tuple[str, Mapping[int, Sequence[tuple[str, ...]]]]:
|
905
900
|
"""
|
906
901
|
Write the EAR's enactment to disk in preparation for submission.
|
907
902
|
|
@@ -909,12 +904,13 @@ class ElementActionRun:
|
|
909
904
|
-------
|
910
905
|
commands:
|
911
906
|
List of argument words for the command that enacts the EAR.
|
907
|
+
Converted to a string.
|
912
908
|
shell_vars:
|
913
909
|
Dict whose keys are command indices, and whose values are lists of tuples,
|
914
910
|
where each tuple contains: (parameter name, shell variable name,
|
915
911
|
"stdout"/"stderr").
|
916
912
|
"""
|
917
|
-
self.
|
913
|
+
self._app.persistence_logger.debug("EAR.compose_commands")
|
918
914
|
env_spec = self.env_spec
|
919
915
|
|
920
916
|
for ifg in self.action.input_file_generators:
|
@@ -930,27 +926,23 @@ class ElementActionRun:
|
|
930
926
|
if self.action.script:
|
931
927
|
self.write_source(js_idx=jobscript.index, js_act_idx=JS_action_idx)
|
932
928
|
|
933
|
-
command_lns = []
|
934
|
-
env
|
935
|
-
|
936
|
-
command_lns += list(env.setup)
|
929
|
+
command_lns: list[str] = []
|
930
|
+
if (env := jobscript.submission.environments.get(**env_spec)).setup:
|
931
|
+
command_lns.extend(env.setup)
|
937
932
|
|
938
|
-
shell_vars
|
933
|
+
shell_vars: dict[int, list[tuple[str, ...]]] = {}
|
939
934
|
for cmd_idx, command in enumerate(self.action.commands):
|
940
935
|
if cmd_idx in self.commands_idx:
|
941
936
|
# only execute commands that have no rules, or all valid rules:
|
942
|
-
cmd_str,
|
937
|
+
cmd_str, shell_vars[cmd_idx] = command.get_command_line(
|
943
938
|
EAR=self, shell=jobscript.shell, env=env
|
944
939
|
)
|
945
|
-
shell_vars[cmd_idx] = shell_vars_i
|
946
940
|
command_lns.append(cmd_str)
|
947
941
|
|
948
|
-
|
949
|
-
|
950
|
-
return commands, shell_vars
|
942
|
+
return ("\n".join(command_lns) + "\n"), shell_vars
|
951
943
|
|
952
944
|
|
953
|
-
class ElementAction:
|
945
|
+
class ElementAction(AppAware):
|
954
946
|
"""
|
955
947
|
An abstract representation of an element's action at a particular iteration and
|
956
948
|
the runs that enact that element iteration.
|
@@ -965,20 +957,23 @@ class ElementAction:
|
|
965
957
|
The list of run indices.
|
966
958
|
"""
|
967
959
|
|
968
|
-
|
969
|
-
|
970
|
-
|
960
|
+
def __init__(
|
961
|
+
self,
|
962
|
+
element_iteration: ElementIteration,
|
963
|
+
action_idx: int,
|
964
|
+
runs: dict[Mapping[str, Any], Any],
|
965
|
+
):
|
971
966
|
self._element_iteration = element_iteration
|
972
967
|
self._action_idx = action_idx
|
973
968
|
self._runs = runs
|
974
969
|
|
975
970
|
# assigned on first access of corresponding properties:
|
976
|
-
self._run_objs = None
|
977
|
-
self._inputs = None
|
978
|
-
self._outputs = None
|
979
|
-
self._resources = None
|
980
|
-
self._input_files = None
|
981
|
-
self._output_files = None
|
971
|
+
self._run_objs: list[ElementActionRun] | None = None
|
972
|
+
self._inputs: ElementInputs | None = None
|
973
|
+
self._outputs: ElementOutputs | None = None
|
974
|
+
self._resources: ElementResources | None = None
|
975
|
+
self._input_files: ElementInputFiles | None = None
|
976
|
+
self._output_files: ElementOutputFiles | None = None
|
982
977
|
|
983
978
|
def __repr__(self):
|
984
979
|
return (
|
@@ -990,104 +985,104 @@ class ElementAction:
|
|
990
985
|
)
|
991
986
|
|
992
987
|
@property
|
993
|
-
def element_iteration(self):
|
988
|
+
def element_iteration(self) -> ElementIteration:
|
994
989
|
"""
|
995
990
|
The iteration for this action.
|
996
991
|
"""
|
997
992
|
return self._element_iteration
|
998
993
|
|
999
994
|
@property
|
1000
|
-
def element(self):
|
995
|
+
def element(self) -> Element:
|
1001
996
|
"""
|
1002
997
|
The element for this action.
|
1003
998
|
"""
|
1004
999
|
return self.element_iteration.element
|
1005
1000
|
|
1006
1001
|
@property
|
1007
|
-
def num_runs(self):
|
1002
|
+
def num_runs(self) -> int:
|
1008
1003
|
"""
|
1009
1004
|
The number of runs associated with this action.
|
1010
1005
|
"""
|
1011
1006
|
return len(self._runs)
|
1012
1007
|
|
1013
1008
|
@property
|
1014
|
-
def runs(self):
|
1009
|
+
def runs(self) -> list[ElementActionRun]:
|
1015
1010
|
"""
|
1016
1011
|
The EARs that this action is enacted by.
|
1017
1012
|
"""
|
1018
1013
|
if self._run_objs is None:
|
1019
1014
|
self._run_objs = [
|
1020
|
-
self.
|
1015
|
+
self._app.ElementActionRun(
|
1021
1016
|
element_action=self,
|
1022
1017
|
index=idx,
|
1023
1018
|
**{
|
1024
1019
|
k: v
|
1025
|
-
for k, v in
|
1020
|
+
for k, v in run_info.items()
|
1026
1021
|
if k not in ("elem_iter_ID", "action_idx")
|
1027
1022
|
},
|
1028
1023
|
)
|
1029
|
-
for idx,
|
1024
|
+
for idx, run_info in enumerate(self._runs)
|
1030
1025
|
]
|
1031
1026
|
return self._run_objs
|
1032
1027
|
|
1033
1028
|
@property
|
1034
|
-
def task(self):
|
1029
|
+
def task(self) -> WorkflowTask:
|
1035
1030
|
"""
|
1036
1031
|
The task that this action is an instance of.
|
1037
1032
|
"""
|
1038
1033
|
return self.element_iteration.task
|
1039
1034
|
|
1040
1035
|
@property
|
1041
|
-
def action_idx(self):
|
1036
|
+
def action_idx(self) -> int:
|
1042
1037
|
"""
|
1043
1038
|
The index of the action.
|
1044
1039
|
"""
|
1045
1040
|
return self._action_idx
|
1046
1041
|
|
1047
1042
|
@property
|
1048
|
-
def action(self):
|
1043
|
+
def action(self) -> Action:
|
1049
1044
|
"""
|
1050
1045
|
The abstract task that this is a concrete model of.
|
1051
1046
|
"""
|
1052
1047
|
return self.task.template.get_schema_action(self.action_idx)
|
1053
1048
|
|
1054
1049
|
@property
|
1055
|
-
def inputs(self):
|
1050
|
+
def inputs(self) -> ElementInputs:
|
1056
1051
|
"""
|
1057
1052
|
The inputs to this action.
|
1058
1053
|
"""
|
1059
1054
|
if not self._inputs:
|
1060
|
-
self._inputs = self.
|
1055
|
+
self._inputs = self._app.ElementInputs(element_action=self)
|
1061
1056
|
return self._inputs
|
1062
1057
|
|
1063
1058
|
@property
|
1064
|
-
def outputs(self):
|
1059
|
+
def outputs(self) -> ElementOutputs:
|
1065
1060
|
"""
|
1066
1061
|
The outputs from this action.
|
1067
1062
|
"""
|
1068
1063
|
if not self._outputs:
|
1069
|
-
self._outputs = self.
|
1064
|
+
self._outputs = self._app.ElementOutputs(element_action=self)
|
1070
1065
|
return self._outputs
|
1071
1066
|
|
1072
1067
|
@property
|
1073
|
-
def input_files(self):
|
1068
|
+
def input_files(self) -> ElementInputFiles:
|
1074
1069
|
"""
|
1075
1070
|
The input files to this action.
|
1076
1071
|
"""
|
1077
1072
|
if not self._input_files:
|
1078
|
-
self._input_files = self.
|
1073
|
+
self._input_files = self._app.ElementInputFiles(element_action=self)
|
1079
1074
|
return self._input_files
|
1080
1075
|
|
1081
1076
|
@property
|
1082
|
-
def output_files(self):
|
1077
|
+
def output_files(self) -> ElementOutputFiles:
|
1083
1078
|
"""
|
1084
1079
|
The output files from this action.
|
1085
1080
|
"""
|
1086
1081
|
if not self._output_files:
|
1087
|
-
self._output_files = self.
|
1082
|
+
self._output_files = self._app.ElementOutputFiles(element_action=self)
|
1088
1083
|
return self._output_files
|
1089
1084
|
|
1090
|
-
def get_data_idx(self, path: str = None, run_idx: int = -1):
|
1085
|
+
def get_data_idx(self, path: str | None = None, run_idx: int = -1) -> DataIndex:
|
1091
1086
|
"""
|
1092
1087
|
Get the data index for some path/run.
|
1093
1088
|
"""
|
@@ -1097,34 +1092,68 @@ class ElementAction:
|
|
1097
1092
|
run_idx=run_idx,
|
1098
1093
|
)
|
1099
1094
|
|
1095
|
+
@overload
|
1100
1096
|
def get_parameter_sources(
|
1101
1097
|
self,
|
1102
|
-
path: str = None,
|
1098
|
+
path: str | None = None,
|
1099
|
+
*,
|
1103
1100
|
run_idx: int = -1,
|
1104
|
-
typ: str = None,
|
1101
|
+
typ: str | None = None,
|
1102
|
+
as_strings: Literal[False] = False,
|
1103
|
+
use_task_index: bool = False,
|
1104
|
+
) -> Mapping[str, ParamSource | list[ParamSource]]:
|
1105
|
+
...
|
1106
|
+
|
1107
|
+
@overload
|
1108
|
+
def get_parameter_sources(
|
1109
|
+
self,
|
1110
|
+
path: str | None = None,
|
1111
|
+
*,
|
1112
|
+
run_idx: int = -1,
|
1113
|
+
typ: str | None = None,
|
1114
|
+
as_strings: Literal[True],
|
1115
|
+
use_task_index: bool = False,
|
1116
|
+
) -> Mapping[str, str]:
|
1117
|
+
...
|
1118
|
+
|
1119
|
+
def get_parameter_sources(
|
1120
|
+
self,
|
1121
|
+
path: str | None = None,
|
1122
|
+
*,
|
1123
|
+
run_idx: int = -1,
|
1124
|
+
typ: str | None = None,
|
1105
1125
|
as_strings: bool = False,
|
1106
1126
|
use_task_index: bool = False,
|
1107
|
-
):
|
1127
|
+
) -> Mapping[str, str] | Mapping[str, ParamSource | list[ParamSource]]:
|
1108
1128
|
"""
|
1109
1129
|
Get information about where parameters originated.
|
1110
1130
|
"""
|
1131
|
+
if as_strings:
|
1132
|
+
return self.element_iteration.get_parameter_sources(
|
1133
|
+
path,
|
1134
|
+
action_idx=self.action_idx,
|
1135
|
+
run_idx=run_idx,
|
1136
|
+
typ=typ,
|
1137
|
+
as_strings=True,
|
1138
|
+
use_task_index=use_task_index,
|
1139
|
+
)
|
1111
1140
|
return self.element_iteration.get_parameter_sources(
|
1112
1141
|
path,
|
1113
1142
|
action_idx=self.action_idx,
|
1114
1143
|
run_idx=run_idx,
|
1115
1144
|
typ=typ,
|
1116
|
-
as_strings=
|
1145
|
+
as_strings=False,
|
1117
1146
|
use_task_index=use_task_index,
|
1118
1147
|
)
|
1119
1148
|
|
1120
1149
|
def get(
|
1121
1150
|
self,
|
1122
|
-
path: str = None,
|
1151
|
+
path: str | None = None,
|
1123
1152
|
run_idx: int = -1,
|
1124
|
-
default: Any = None,
|
1153
|
+
default: Any | None = None,
|
1125
1154
|
raise_on_missing: bool = False,
|
1126
1155
|
raise_on_unset: bool = False,
|
1127
|
-
):
|
1156
|
+
) -> Any:
|
1128
1157
|
"""
|
1129
1158
|
Get the value of a parameter.
|
1130
1159
|
"""
|
@@ -1137,7 +1166,7 @@ class ElementAction:
|
|
1137
1166
|
raise_on_unset=raise_on_unset,
|
1138
1167
|
)
|
1139
1168
|
|
1140
|
-
def get_parameter_names(self, prefix: str) ->
|
1169
|
+
def get_parameter_names(self, prefix: str) -> list[str]:
|
1141
1170
|
"""Get parameter types associated with a given prefix.
|
1142
1171
|
|
1143
1172
|
For inputs, labels are ignored.
|
@@ -1152,12 +1181,13 @@ class ElementAction:
|
|
1152
1181
|
return self.action.get_parameter_names(prefix)
|
1153
1182
|
|
1154
1183
|
|
1184
|
+
@final
|
1155
1185
|
class ActionScope(JSONLike):
|
1156
1186
|
"""Class to represent the identification of a subset of task schema actions by a
|
1157
1187
|
filtering process.
|
1158
1188
|
"""
|
1159
1189
|
|
1160
|
-
_child_objects = (
|
1190
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
1161
1191
|
ChildObjectSpec(
|
1162
1192
|
name="typ",
|
1163
1193
|
json_like_name="type",
|
@@ -1166,46 +1196,53 @@ class ActionScope(JSONLike):
|
|
1166
1196
|
),
|
1167
1197
|
)
|
1168
1198
|
|
1169
|
-
|
1199
|
+
__ACTION_SCOPE_RE: ClassVar[Pattern] = re.compile(r"(\w*)(?:\[(.*)\])?")
|
1200
|
+
|
1201
|
+
def __init__(self, typ: ActionScopeType | str, **kwargs):
|
1170
1202
|
if isinstance(typ, str):
|
1171
|
-
|
1203
|
+
#: Action scope type.
|
1204
|
+
self.typ = self._app.ActionScopeType[typ.upper()]
|
1205
|
+
else:
|
1206
|
+
self.typ = typ
|
1172
1207
|
|
1173
|
-
#: Action scope type.
|
1174
|
-
self.typ = typ
|
1175
1208
|
#: Any provided extra keyword arguments.
|
1176
1209
|
self.kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
1177
1210
|
|
1178
|
-
bad_keys
|
1179
|
-
if bad_keys:
|
1211
|
+
if bad_keys := set(kwargs) - ACTION_SCOPE_ALLOWED_KWARGS[self.typ.name]:
|
1180
1212
|
raise TypeError(
|
1181
1213
|
f"The following keyword arguments are unknown for ActionScopeType "
|
1182
1214
|
f"{self.typ.name}: {bad_keys}."
|
1183
1215
|
)
|
1184
1216
|
|
1185
|
-
def __repr__(self):
|
1217
|
+
def __repr__(self) -> str:
|
1186
1218
|
kwargs_str = ""
|
1187
1219
|
if self.kwargs:
|
1188
1220
|
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in self.kwargs.items())
|
1189
1221
|
return f"{self.__class__.__name__}.{self.typ.name.lower()}({kwargs_str})"
|
1190
1222
|
|
1191
|
-
def __eq__(self, other):
|
1223
|
+
def __eq__(self, other: Any) -> bool:
|
1192
1224
|
if not isinstance(other, self.__class__):
|
1193
1225
|
return False
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1226
|
+
return self.typ is other.typ and self.kwargs == other.kwargs
|
1227
|
+
|
1228
|
+
class __customdict(dict):
|
1229
|
+
pass
|
1197
1230
|
|
1198
1231
|
@classmethod
|
1199
|
-
def _parse_from_string(cls, string):
|
1200
|
-
|
1201
|
-
|
1232
|
+
def _parse_from_string(cls, string: str) -> dict[str, str]:
|
1233
|
+
if not (match := cls.__ACTION_SCOPE_RE.search(string)):
|
1234
|
+
raise TypeError(f"unparseable ActionScope: '{string}'")
|
1235
|
+
typ_str, kwargs_str = match.groups()
|
1236
|
+
# The types of the above two variables are idiotic, but bug reports to fix it
|
1237
|
+
# get closed because "it would break existing code that makes dumb assumptions"
|
1238
|
+
kwargs: dict[str, str] = cls.__customdict({"type": cast("str", typ_str)})
|
1202
1239
|
if kwargs_str:
|
1203
|
-
for
|
1204
|
-
name, val =
|
1240
|
+
for pair_str in kwargs_str.split(","):
|
1241
|
+
name, val = pair_str.split("=")
|
1205
1242
|
kwargs[name.strip()] = val.strip()
|
1206
|
-
return
|
1243
|
+
return kwargs
|
1207
1244
|
|
1208
|
-
def to_string(self):
|
1245
|
+
def to_string(self) -> str:
|
1209
1246
|
"""
|
1210
1247
|
Render this action scope as a string.
|
1211
1248
|
"""
|
@@ -1215,59 +1252,62 @@ class ActionScope(JSONLike):
|
|
1215
1252
|
return f"{self.typ.name.lower()}{kwargs_str}"
|
1216
1253
|
|
1217
1254
|
@classmethod
|
1218
|
-
def
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1255
|
+
def _from_json_like(
|
1256
|
+
cls,
|
1257
|
+
json_like: Mapping[str, Any] | Sequence[Mapping[str, Any]],
|
1258
|
+
shared_data: Mapping[str, Any],
|
1259
|
+
) -> Self:
|
1260
|
+
if not isinstance(json_like, Mapping):
|
1261
|
+
raise TypeError("only mappings are supported for becoming an ActionScope")
|
1262
|
+
if not isinstance(json_like, cls.__customdict):
|
1263
|
+
# Wasn't processed by _parse_from_string() already
|
1264
|
+
json_like = {"type": json_like["type"], **json_like.get("kwargs", {})}
|
1265
|
+
return super()._from_json_like(json_like, shared_data)
|
1225
1266
|
|
1226
1267
|
@classmethod
|
1227
|
-
def any(cls):
|
1268
|
+
def any(cls) -> ActionScope:
|
1228
1269
|
"""
|
1229
1270
|
Any scope.
|
1230
1271
|
"""
|
1231
1272
|
return cls(typ=ActionScopeType.ANY)
|
1232
1273
|
|
1233
1274
|
@classmethod
|
1234
|
-
def main(cls):
|
1275
|
+
def main(cls) -> ActionScope:
|
1235
1276
|
"""
|
1236
1277
|
The main scope.
|
1237
1278
|
"""
|
1238
1279
|
return cls(typ=ActionScopeType.MAIN)
|
1239
1280
|
|
1240
1281
|
@classmethod
|
1241
|
-
def processing(cls):
|
1282
|
+
def processing(cls) -> ActionScope:
|
1242
1283
|
"""
|
1243
1284
|
The processing scope.
|
1244
1285
|
"""
|
1245
1286
|
return cls(typ=ActionScopeType.PROCESSING)
|
1246
1287
|
|
1247
1288
|
@classmethod
|
1248
|
-
def input_file_generator(cls, file=None):
|
1289
|
+
def input_file_generator(cls, file: str | None = None) -> ActionScope:
|
1249
1290
|
"""
|
1250
1291
|
The scope of an input file generator.
|
1251
1292
|
"""
|
1252
1293
|
return cls(typ=ActionScopeType.INPUT_FILE_GENERATOR, file=file)
|
1253
1294
|
|
1254
1295
|
@classmethod
|
1255
|
-
def output_file_parser(cls, output=None):
|
1296
|
+
def output_file_parser(cls, output: Parameter | str | None = None) -> ActionScope:
|
1256
1297
|
"""
|
1257
1298
|
The scope of an output file parser.
|
1258
1299
|
"""
|
1259
1300
|
return cls(typ=ActionScopeType.OUTPUT_FILE_PARSER, output=output)
|
1260
1301
|
|
1261
1302
|
|
1262
|
-
@dataclass
|
1303
|
+
@dataclass()
|
1304
|
+
@hydrate
|
1263
1305
|
class ActionEnvironment(JSONLike):
|
1264
1306
|
"""
|
1265
1307
|
The environment that an action is enacted within.
|
1266
1308
|
"""
|
1267
1309
|
|
1268
|
-
|
1269
|
-
|
1270
|
-
_child_objects = (
|
1310
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
1271
1311
|
ChildObjectSpec(
|
1272
1312
|
name="scope",
|
1273
1313
|
class_name="ActionScope",
|
@@ -1275,24 +1315,24 @@ class ActionEnvironment(JSONLike):
|
|
1275
1315
|
)
|
1276
1316
|
|
1277
1317
|
#: The environment document.
|
1278
|
-
environment:
|
1318
|
+
environment: Mapping[str, Any]
|
1279
1319
|
#: The scope.
|
1280
|
-
scope:
|
1281
|
-
|
1282
|
-
def __post_init__(self):
|
1283
|
-
if self.scope is None:
|
1284
|
-
self.scope = self.app.ActionScope.any()
|
1320
|
+
scope: ActionScope
|
1285
1321
|
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1322
|
+
def __init__(
|
1323
|
+
self, environment: str | dict[str, Any], scope: ActionScope | None = None
|
1324
|
+
):
|
1325
|
+
if scope is None:
|
1326
|
+
self.scope = self._app.ActionScope.any()
|
1327
|
+
else:
|
1328
|
+
self.scope = scope
|
1289
1329
|
|
1290
|
-
if
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
)
|
1330
|
+
if isinstance(environment, str):
|
1331
|
+
self.environment = {"name": environment}
|
1332
|
+
else:
|
1333
|
+
if "name" not in environment:
|
1334
|
+
raise ActionEnvironmentMissingNameError(environment)
|
1335
|
+
self.environment = copy.deepcopy(environment)
|
1296
1336
|
|
1297
1337
|
|
1298
1338
|
class ActionRule(JSONLike):
|
@@ -1318,20 +1358,23 @@ class ActionRule(JSONLike):
|
|
1318
1358
|
Documentation for this rule, if any.
|
1319
1359
|
"""
|
1320
1360
|
|
1321
|
-
_child_objects
|
1361
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
1362
|
+
ChildObjectSpec(name="rule", class_name="Rule"),
|
1363
|
+
)
|
1322
1364
|
|
1323
1365
|
def __init__(
|
1324
1366
|
self,
|
1325
|
-
rule:
|
1326
|
-
check_exists:
|
1327
|
-
check_missing:
|
1328
|
-
path:
|
1329
|
-
condition:
|
1330
|
-
cast:
|
1331
|
-
doc:
|
1367
|
+
rule: Rule | None = None,
|
1368
|
+
check_exists: str | None = None,
|
1369
|
+
check_missing: str | None = None,
|
1370
|
+
path: str | None = None,
|
1371
|
+
condition: dict[str, Any] | ConditionLike | None = None,
|
1372
|
+
cast: str | None = None,
|
1373
|
+
doc: str | None = None,
|
1332
1374
|
):
|
1333
1375
|
if rule is None:
|
1334
|
-
rule
|
1376
|
+
#: The rule to apply.
|
1377
|
+
self.rule = self._app.Rule(
|
1335
1378
|
check_exists=check_exists,
|
1336
1379
|
check_missing=check_missing,
|
1337
1380
|
path=path,
|
@@ -1340,30 +1383,28 @@ class ActionRule(JSONLike):
|
|
1340
1383
|
doc=doc,
|
1341
1384
|
)
|
1342
1385
|
elif any(
|
1343
|
-
|
1344
|
-
for
|
1386
|
+
arg is not None
|
1387
|
+
for arg in (check_exists, check_missing, path, condition, cast, doc)
|
1345
1388
|
):
|
1346
1389
|
raise TypeError(
|
1347
1390
|
f"{self.__class__.__name__} `rule` specified in addition to rule "
|
1348
1391
|
f"constructor arguments."
|
1349
1392
|
)
|
1393
|
+
else:
|
1394
|
+
self.rule = rule
|
1350
1395
|
|
1351
|
-
#: The rule to apply.
|
1352
|
-
self.rule = rule
|
1353
1396
|
#: The action that contains this rule.
|
1354
|
-
self.action = None # assigned by parent action
|
1397
|
+
self.action: Action | None = None # assigned by parent action
|
1355
1398
|
#: The command that is guarded by this rule.
|
1356
|
-
self.command = None # assigned by parent command
|
1399
|
+
self.command: Command | None = None # assigned by parent command
|
1357
1400
|
|
1358
|
-
def __eq__(self, other):
|
1401
|
+
def __eq__(self, other: Any) -> bool:
|
1359
1402
|
if type(other) is not self.__class__:
|
1360
1403
|
return False
|
1361
|
-
|
1362
|
-
return True
|
1363
|
-
return False
|
1404
|
+
return self.rule == other.rule
|
1364
1405
|
|
1365
1406
|
@TimeIt.decorator
|
1366
|
-
def test(self, element_iteration:
|
1407
|
+
def test(self, element_iteration: ElementIteration) -> bool:
|
1367
1408
|
"""
|
1368
1409
|
Test if this rule holds for a particular iteration.
|
1369
1410
|
|
@@ -1375,28 +1416,31 @@ class ActionRule(JSONLike):
|
|
1375
1416
|
return self.rule.test(element_like=element_iteration, action=self.action)
|
1376
1417
|
|
1377
1418
|
@classmethod
|
1378
|
-
def check_exists(cls, check_exists):
|
1419
|
+
def check_exists(cls, check_exists: str) -> ActionRule:
|
1379
1420
|
"""
|
1380
1421
|
Make an action rule that checks if a named attribute is present.
|
1381
1422
|
|
1382
1423
|
Parameter
|
1383
1424
|
---------
|
1384
|
-
check_exists:
|
1425
|
+
check_exists:
|
1385
1426
|
The path to the attribute to check for.
|
1386
1427
|
"""
|
1387
|
-
return cls(rule=
|
1428
|
+
return cls(rule=cls._app.Rule(check_exists=check_exists))
|
1388
1429
|
|
1389
1430
|
@classmethod
|
1390
|
-
def check_missing(cls, check_missing):
|
1431
|
+
def check_missing(cls, check_missing: str) -> ActionRule:
|
1391
1432
|
"""
|
1392
1433
|
Make an action rule that checks if a named attribute is absent.
|
1393
1434
|
|
1394
1435
|
Parameter
|
1395
1436
|
---------
|
1396
|
-
check_missing:
|
1437
|
+
check_missing:
|
1397
1438
|
The path to the attribute to check for.
|
1398
1439
|
"""
|
1399
|
-
return cls(rule=
|
1440
|
+
return cls(rule=cls._app.Rule(check_missing=check_missing))
|
1441
|
+
|
1442
|
+
|
1443
|
+
_ALL_OTHER_SYM = "*"
|
1400
1444
|
|
1401
1445
|
|
1402
1446
|
class Action(JSONLike):
|
@@ -1444,8 +1488,7 @@ class Action(JSONLike):
|
|
1444
1488
|
The names of files to be deleted after each step.
|
1445
1489
|
"""
|
1446
1490
|
|
1447
|
-
|
1448
|
-
_child_objects = (
|
1491
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
1449
1492
|
ChildObjectSpec(
|
1450
1493
|
name="commands",
|
1451
1494
|
class_name="Command",
|
@@ -1501,35 +1544,37 @@ class Action(JSONLike):
|
|
1501
1544
|
shared_data_name="command_files",
|
1502
1545
|
),
|
1503
1546
|
)
|
1504
|
-
_script_data_formats = ("direct", "json", "hdf5")
|
1547
|
+
_script_data_formats: ClassVar[tuple[str, ...]] = ("direct", "json", "hdf5")
|
1505
1548
|
|
1506
1549
|
def __init__(
|
1507
1550
|
self,
|
1508
|
-
environments:
|
1509
|
-
commands:
|
1510
|
-
script:
|
1511
|
-
script_data_in:
|
1512
|
-
script_data_out:
|
1513
|
-
script_data_files_use_opt:
|
1514
|
-
script_exe:
|
1515
|
-
script_pass_env_spec:
|
1516
|
-
abortable:
|
1517
|
-
input_file_generators:
|
1518
|
-
output_file_parsers:
|
1519
|
-
input_files:
|
1520
|
-
output_files:
|
1521
|
-
rules:
|
1522
|
-
save_files:
|
1523
|
-
clean_up:
|
1551
|
+
environments: list[ActionEnvironment] | None = None,
|
1552
|
+
commands: list[Command] | None = None,
|
1553
|
+
script: str | None = None,
|
1554
|
+
script_data_in: str | Mapping[str, str | ScriptData] | None = None,
|
1555
|
+
script_data_out: str | Mapping[str, str | ScriptData] | None = None,
|
1556
|
+
script_data_files_use_opt: bool = False,
|
1557
|
+
script_exe: str | None = None,
|
1558
|
+
script_pass_env_spec: bool = False,
|
1559
|
+
abortable: bool = False,
|
1560
|
+
input_file_generators: list[InputFileGenerator] | None = None,
|
1561
|
+
output_file_parsers: list[OutputFileParser] | None = None,
|
1562
|
+
input_files: list[FileSpec] | None = None,
|
1563
|
+
output_files: list[FileSpec] | None = None,
|
1564
|
+
rules: list[ActionRule] | None = None,
|
1565
|
+
save_files: list[FileSpec] | None = None,
|
1566
|
+
clean_up: list[str] | None = None,
|
1524
1567
|
):
|
1525
1568
|
#: The commands to be run by this action.
|
1526
1569
|
self.commands = commands or []
|
1527
1570
|
#: The name of the Python script to run.
|
1528
1571
|
self.script = script
|
1529
1572
|
#: Information about data input to the script.
|
1530
|
-
self.script_data_in =
|
1573
|
+
self.script_data_in: dict[str, ScriptData] | None = None
|
1574
|
+
self._script_data_in = script_data_in
|
1531
1575
|
#: Information about data output from the script.
|
1532
|
-
self.script_data_out =
|
1576
|
+
self.script_data_out: dict[str, ScriptData] | None = None
|
1577
|
+
self._script_data_out = script_data_out
|
1533
1578
|
#: If True, script data input and output file paths will be passed to the script
|
1534
1579
|
#: execution command line with an option like `--input-json` or `--output-hdf5`
|
1535
1580
|
#: etc. If False, the file paths will be passed on their own. For Python scripts,
|
@@ -1544,7 +1589,7 @@ class Action(JSONLike):
|
|
1544
1589
|
self.script_pass_env_spec = script_pass_env_spec
|
1545
1590
|
#: The environments in which this action can run.
|
1546
1591
|
self.environments = environments or [
|
1547
|
-
self.
|
1592
|
+
self._app.ActionEnvironment(environment="null_env")
|
1548
1593
|
]
|
1549
1594
|
#: Whether this action can be aborted.
|
1550
1595
|
self.abortable = abortable
|
@@ -1553,9 +1598,9 @@ class Action(JSONLike):
|
|
1553
1598
|
#: Any applicable output file parsers.
|
1554
1599
|
self.output_file_parsers = output_file_parsers or []
|
1555
1600
|
#: The input files to the action's commands.
|
1556
|
-
self.input_files = self.
|
1601
|
+
self.input_files = self.__resolve_input_files(input_files or [])
|
1557
1602
|
#: The output files from the action's commands.
|
1558
|
-
self.output_files = self.
|
1603
|
+
self.output_files = self.__resolve_output_files(output_files or [])
|
1559
1604
|
#: How to determine whether to run the action.
|
1560
1605
|
self.rules = rules or []
|
1561
1606
|
#: The names of files to be explicitly saved after each step.
|
@@ -1563,125 +1608,130 @@ class Action(JSONLike):
|
|
1563
1608
|
#: The names of files to be deleted after each step.
|
1564
1609
|
self.clean_up = clean_up or []
|
1565
1610
|
|
1566
|
-
self._task_schema = None # assigned by parent TaskSchema
|
1611
|
+
self._task_schema: TaskSchema | None = None # assigned by parent TaskSchema
|
1567
1612
|
self._from_expand = False # assigned on creation of new Action by `expand`
|
1568
1613
|
|
1569
1614
|
self._set_parent_refs()
|
1570
1615
|
|
1571
|
-
def process_script_data_formats(self):
|
1616
|
+
def process_script_data_formats(self) -> None:
|
1572
1617
|
"""
|
1573
1618
|
Convert script data information into standard form.
|
1574
1619
|
"""
|
1575
|
-
self.script_data_in = self.
|
1576
|
-
self.script_data_out = self.
|
1620
|
+
self.script_data_in = self.__process_script_data(self._script_data_in, "inputs")
|
1621
|
+
self.script_data_out = self.__process_script_data(
|
1622
|
+
self._script_data_out, "outputs"
|
1623
|
+
)
|
1577
1624
|
|
1578
|
-
def
|
1579
|
-
self, data_fmt:
|
1580
|
-
) ->
|
1581
|
-
|
1582
|
-
|
1625
|
+
def __process_script_data_str(
|
1626
|
+
self, data_fmt: str, param_names: Iterable[str]
|
1627
|
+
) -> dict[str, ScriptData]:
|
1628
|
+
# include all input parameters, using specified data format
|
1629
|
+
data_fmt = data_fmt.lower()
|
1630
|
+
return {k: {"format": data_fmt} for k in param_names}
|
1583
1631
|
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1607
|
-
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1632
|
+
def __process_script_data_dict(
|
1633
|
+
self,
|
1634
|
+
data_fmt: Mapping[str, str | ScriptData],
|
1635
|
+
prefix: str,
|
1636
|
+
param_names: Iterable[str],
|
1637
|
+
) -> dict[str, ScriptData]:
|
1638
|
+
all_params: dict[str, ScriptData] = {}
|
1639
|
+
for nm, v in data_fmt.items():
|
1640
|
+
# values might be strings, or dicts with "format" and potentially other
|
1641
|
+
# kwargs:
|
1642
|
+
if isinstance(v, dict):
|
1643
|
+
# Make sure format is first key
|
1644
|
+
v2: ScriptData = {
|
1645
|
+
"format": v["format"],
|
1646
|
+
}
|
1647
|
+
all_params[nm] = v2
|
1648
|
+
v2.update(v)
|
1649
|
+
else:
|
1650
|
+
all_params[nm] = {"format": v.lower()}
|
1651
|
+
|
1652
|
+
if prefix == "inputs":
|
1653
|
+
# expand unlabelled-multiple inputs to multiple labelled inputs:
|
1654
|
+
multi_types = set(self.task_schema.multi_input_types)
|
1655
|
+
multis: dict[str, ScriptData] = {}
|
1656
|
+
for nm in tuple(all_params):
|
1657
|
+
if nm in multi_types:
|
1658
|
+
k_fmt = all_params.pop(nm)
|
1659
|
+
for name in param_names:
|
1660
|
+
if name.startswith(nm):
|
1661
|
+
multis[name] = copy.deepcopy(k_fmt)
|
1662
|
+
if multis:
|
1615
1663
|
all_params = {
|
1616
1664
|
**multis,
|
1617
1665
|
**all_params,
|
1618
1666
|
}
|
1619
1667
|
|
1620
|
-
|
1621
|
-
|
1622
|
-
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
1668
|
+
if _ALL_OTHER_SYM in all_params:
|
1669
|
+
# replace catch-all with all other input/output names:
|
1670
|
+
other_fmt = all_params[_ALL_OTHER_SYM]
|
1671
|
+
all_params = {k: v for k, v in all_params.items() if k != _ALL_OTHER_SYM}
|
1672
|
+
for name in set(param_names).difference(all_params):
|
1673
|
+
all_params[name] = copy.deepcopy(other_fmt)
|
1674
|
+
return all_params
|
1675
|
+
|
1676
|
+
def __process_script_data(
|
1677
|
+
self, data_fmt: str | Mapping[str, str | ScriptData] | None, prefix: str
|
1678
|
+
) -> dict[str, ScriptData]:
|
1679
|
+
if not data_fmt:
|
1680
|
+
return {}
|
1681
|
+
|
1682
|
+
param_names = self.get_parameter_names(prefix)
|
1683
|
+
if isinstance(data_fmt, str):
|
1684
|
+
all_params = self.__process_script_data_str(data_fmt, param_names)
|
1685
|
+
else:
|
1686
|
+
all_params = self.__process_script_data_dict(data_fmt, prefix, param_names)
|
1627
1687
|
|
1628
1688
|
# validation:
|
1629
1689
|
allowed_keys = ("format", "all_iterations")
|
1630
1690
|
for k, v in all_params.items():
|
1631
1691
|
# validate parameter name (sub-parameters are allowed):
|
1632
1692
|
if k.split(".")[0] not in param_names:
|
1633
|
-
raise UnknownScriptDataParameter(
|
1634
|
-
f"Script data parameter {k!r} is not a known parameter of the "
|
1635
|
-
f"action. Parameters ({prefix}) are: {param_names!r}."
|
1636
|
-
)
|
1693
|
+
raise UnknownScriptDataParameter(k, prefix, param_names)
|
1637
1694
|
# validate format:
|
1638
1695
|
if v["format"] not in self._script_data_formats:
|
1639
1696
|
raise UnsupportedScriptDataFormat(
|
1640
|
-
|
1641
|
-
f"understood. Available script data formats are: "
|
1642
|
-
f"{self._script_data_formats!r}."
|
1697
|
+
v, prefix[:-1], k, self._script_data_formats
|
1643
1698
|
)
|
1644
|
-
|
1645
|
-
|
1646
|
-
if k2 not in allowed_keys:
|
1647
|
-
raise UnknownScriptDataKey(
|
1648
|
-
f"Script data key {k2!r} is not understood. Allowed keys are: "
|
1649
|
-
f"{allowed_keys!r}."
|
1650
|
-
)
|
1699
|
+
if any((bad_key := k2) for k2 in v if k2 not in allowed_keys):
|
1700
|
+
raise UnknownScriptDataKey(bad_key, allowed_keys)
|
1651
1701
|
|
1652
1702
|
return all_params
|
1653
1703
|
|
1654
|
-
def _process_script_data_in(
|
1655
|
-
self, data_fmt: Union[str, Dict[str, str]]
|
1656
|
-
) -> Dict[str, str]:
|
1657
|
-
return self._process_script_data_format(data_fmt, "inputs")
|
1658
|
-
|
1659
|
-
def _process_script_data_out(
|
1660
|
-
self, data_fmt: Union[str, Dict[str, str]]
|
1661
|
-
) -> Dict[str, str]:
|
1662
|
-
return self._process_script_data_format(data_fmt, "outputs")
|
1663
|
-
|
1664
1704
|
@property
|
1665
|
-
def script_data_in_grouped(self) ->
|
1705
|
+
def script_data_in_grouped(self) -> Mapping[str, Mapping[str, Mapping[str, str]]]:
|
1666
1706
|
"""Get input parameter types by script data-in format."""
|
1667
|
-
|
1707
|
+
if self.script_data_in is None:
|
1708
|
+
self.process_script_data_formats()
|
1709
|
+
assert self.script_data_in is not None
|
1710
|
+
return swap_nested_dict_keys(
|
1711
|
+
dct=cast("dict", self.script_data_in), inner_key="format"
|
1712
|
+
)
|
1668
1713
|
|
1669
1714
|
@property
|
1670
|
-
def script_data_out_grouped(self) ->
|
1715
|
+
def script_data_out_grouped(self) -> Mapping[str, Mapping[str, Mapping[str, str]]]:
|
1671
1716
|
"""Get output parameter types by script data-out format."""
|
1672
|
-
|
1717
|
+
if self.script_data_out is None:
|
1718
|
+
self.process_script_data_formats()
|
1719
|
+
assert self.script_data_out is not None
|
1720
|
+
return swap_nested_dict_keys(
|
1721
|
+
dct=cast("dict", self.script_data_out), inner_key="format"
|
1722
|
+
)
|
1673
1723
|
|
1674
1724
|
@property
|
1675
1725
|
def script_data_in_has_files(self) -> bool:
|
1676
1726
|
"""Return True if the script requires some inputs to be passed via an
|
1677
1727
|
intermediate file format."""
|
1678
|
-
return bool(set(self.script_data_in_grouped
|
1728
|
+
return bool(set(self.script_data_in_grouped) - {"direct"}) # TODO: test
|
1679
1729
|
|
1680
1730
|
@property
|
1681
1731
|
def script_data_out_has_files(self) -> bool:
|
1682
1732
|
"""Return True if the script produces some outputs via an intermediate file
|
1683
1733
|
format."""
|
1684
|
-
return bool(set(self.script_data_out_grouped
|
1734
|
+
return bool(set(self.script_data_out_grouped) - {"direct"}) # TODO: test
|
1685
1735
|
|
1686
1736
|
@property
|
1687
1737
|
def script_data_in_has_direct(self) -> bool:
|
@@ -1699,12 +1749,18 @@ class Action(JSONLike):
|
|
1699
1749
|
def script_is_python(self) -> bool:
|
1700
1750
|
"""Return True if the script is a Python script (determined by the file
|
1701
1751
|
extension)"""
|
1702
|
-
if self.script:
|
1703
|
-
snip_path
|
1704
|
-
|
1705
|
-
|
1752
|
+
if self.script and (snip_path := self.get_snippet_script_path(self.script)):
|
1753
|
+
return snip_path.suffix == ".py"
|
1754
|
+
return False
|
1755
|
+
|
1756
|
+
@override
|
1757
|
+
def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
|
1758
|
+
d = super()._postprocess_to_dict(d)
|
1759
|
+
d["script_data_in"] = d.pop("_script_data_in")
|
1760
|
+
d["script_data_out"] = d.pop("_script_data_out")
|
1761
|
+
return d
|
1706
1762
|
|
1707
|
-
def __deepcopy__(self, memo):
|
1763
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
|
1708
1764
|
kwargs = self.to_dict()
|
1709
1765
|
_from_expand = kwargs.pop("_from_expand")
|
1710
1766
|
_task_schema = kwargs.pop("_task_schema", None)
|
@@ -1714,41 +1770,41 @@ class Action(JSONLike):
|
|
1714
1770
|
return obj
|
1715
1771
|
|
1716
1772
|
@property
|
1717
|
-
def task_schema(self):
|
1773
|
+
def task_schema(self) -> TaskSchema:
|
1718
1774
|
"""
|
1719
1775
|
The task schema that this action came from.
|
1720
1776
|
"""
|
1777
|
+
assert self._task_schema is not None
|
1721
1778
|
return self._task_schema
|
1722
1779
|
|
1723
|
-
def
|
1780
|
+
def __resolve_input_files(self, input_files: list[FileSpec]) -> list[FileSpec]:
|
1724
1781
|
in_files = input_files
|
1725
|
-
for
|
1726
|
-
if
|
1727
|
-
in_files.append(
|
1782
|
+
for ifg in self.input_file_generators:
|
1783
|
+
if ifg.input_file not in in_files:
|
1784
|
+
in_files.append(ifg.input_file)
|
1728
1785
|
return in_files
|
1729
1786
|
|
1730
|
-
def
|
1787
|
+
def __resolve_output_files(self, output_files: list[FileSpec]) -> list[FileSpec]:
|
1731
1788
|
out_files = output_files
|
1732
|
-
for
|
1733
|
-
for
|
1734
|
-
if
|
1735
|
-
out_files.append(
|
1789
|
+
for ofp in self.output_file_parsers:
|
1790
|
+
for out_file in ofp.output_files:
|
1791
|
+
if out_file not in out_files:
|
1792
|
+
out_files.append(out_file)
|
1736
1793
|
return out_files
|
1737
1794
|
|
1738
1795
|
def __repr__(self) -> str:
|
1739
1796
|
IFGs = {
|
1740
|
-
|
1741
|
-
for
|
1797
|
+
ifg.input_file.label: [inp.typ for inp in ifg.inputs]
|
1798
|
+
for ifg in self.input_file_generators
|
1799
|
+
}
|
1800
|
+
OFPs = {
|
1801
|
+
ofp.output.typ
|
1802
|
+
if ofp.output
|
1803
|
+
else f"OFP_{idx}": [out_file.label for out_file in ofp.output_files]
|
1804
|
+
for idx, ofp in enumerate(self.output_file_parsers)
|
1742
1805
|
}
|
1743
|
-
OFPs = {}
|
1744
|
-
for idx, i in enumerate(self.output_file_parsers):
|
1745
|
-
if i.output is not None:
|
1746
|
-
key = i.output.typ
|
1747
|
-
else:
|
1748
|
-
key = f"OFP_{idx}"
|
1749
|
-
OFPs[key] = [j.label for j in i.output_files]
|
1750
1806
|
|
1751
|
-
out = []
|
1807
|
+
out: list[str] = []
|
1752
1808
|
if self.commands:
|
1753
1809
|
out.append(f"commands={self.commands!r}")
|
1754
1810
|
if self.script:
|
@@ -1764,10 +1820,10 @@ class Action(JSONLike):
|
|
1764
1820
|
|
1765
1821
|
return f"{self.__class__.__name__}({', '.join(out)})"
|
1766
1822
|
|
1767
|
-
def __eq__(self, other):
|
1768
|
-
if
|
1823
|
+
def __eq__(self, other: Any) -> bool:
|
1824
|
+
if not isinstance(other, self.__class__):
|
1769
1825
|
return False
|
1770
|
-
|
1826
|
+
return (
|
1771
1827
|
self.commands == other.commands
|
1772
1828
|
and self.script == other.script
|
1773
1829
|
and self.environments == other.environments
|
@@ -1775,63 +1831,64 @@ class Action(JSONLike):
|
|
1775
1831
|
and self.input_file_generators == other.input_file_generators
|
1776
1832
|
and self.output_file_parsers == other.output_file_parsers
|
1777
1833
|
and self.rules == other.rules
|
1778
|
-
)
|
1779
|
-
return True
|
1780
|
-
return False
|
1834
|
+
)
|
1781
1835
|
|
1782
1836
|
@classmethod
|
1783
|
-
def _json_like_constructor(cls, json_like):
|
1837
|
+
def _json_like_constructor(cls, json_like) -> Self:
|
1784
1838
|
"""Invoked by `JSONLike.from_json_like` instead of `__init__`."""
|
1785
1839
|
_from_expand = json_like.pop("_from_expand", None)
|
1786
1840
|
obj = cls(**json_like)
|
1787
1841
|
obj._from_expand = _from_expand
|
1788
1842
|
return obj
|
1789
1843
|
|
1790
|
-
def get_parameter_dependence(self, parameter:
|
1844
|
+
def get_parameter_dependence(self, parameter: SchemaParameter) -> ParameterDependence:
|
1791
1845
|
"""Find if/where a given parameter is used by the action."""
|
1846
|
+
# names of input files whose generation requires this parameter
|
1792
1847
|
writer_files = [
|
1793
|
-
|
1794
|
-
for
|
1795
|
-
if parameter.parameter in
|
1796
|
-
]
|
1797
|
-
|
1798
|
-
|
1799
|
-
return
|
1848
|
+
ifg.input_file
|
1849
|
+
for ifg in self.input_file_generators
|
1850
|
+
if parameter.parameter in ifg.inputs
|
1851
|
+
]
|
1852
|
+
# TODO: indices of commands in which this parameter appears
|
1853
|
+
commands: list[int] = []
|
1854
|
+
return {"input_file_writers": writer_files, "commands": commands}
|
1800
1855
|
|
1801
|
-
def
|
1856
|
+
def __get_resolved_action_env(
|
1802
1857
|
self,
|
1803
|
-
relevant_scopes:
|
1804
|
-
input_file_generator:
|
1805
|
-
output_file_parser:
|
1806
|
-
commands:
|
1807
|
-
):
|
1808
|
-
possible = [
|
1858
|
+
relevant_scopes: tuple[ActionScopeType, ...],
|
1859
|
+
input_file_generator: InputFileGenerator | None = None,
|
1860
|
+
output_file_parser: OutputFileParser | None = None,
|
1861
|
+
commands: list[Command] | None = None,
|
1862
|
+
) -> ActionEnvironment:
|
1863
|
+
possible = [
|
1864
|
+
env
|
1865
|
+
for env in self.environments
|
1866
|
+
if env.scope and env.scope.typ in relevant_scopes
|
1867
|
+
]
|
1809
1868
|
if not possible:
|
1810
1869
|
if input_file_generator:
|
1811
|
-
|
1870
|
+
raise MissingCompatibleActionEnvironment(
|
1871
|
+
f"input file generator {input_file_generator.input_file.label!r}"
|
1872
|
+
)
|
1812
1873
|
elif output_file_parser:
|
1813
1874
|
if output_file_parser.output is not None:
|
1814
1875
|
ofp_id = output_file_parser.output.typ
|
1815
1876
|
else:
|
1816
1877
|
ofp_id = "<unnamed>"
|
1817
|
-
|
1878
|
+
raise MissingCompatibleActionEnvironment(f"output file parser {ofp_id!r}")
|
1818
1879
|
else:
|
1819
|
-
|
1820
|
-
raise MissingCompatibleActionEnvironment(
|
1821
|
-
f"No compatible environment is specified for the {msg}."
|
1822
|
-
)
|
1880
|
+
raise MissingCompatibleActionEnvironment(f"commands {commands!r}")
|
1823
1881
|
|
1824
|
-
#
|
1825
|
-
|
1826
|
-
return possible_srt[0]
|
1882
|
+
# get max by scope type specificity:
|
1883
|
+
return max(possible, key=lambda i: i.scope.typ.value)
|
1827
1884
|
|
1828
1885
|
def get_input_file_generator_action_env(
|
1829
|
-
self, input_file_generator:
|
1830
|
-
):
|
1886
|
+
self, input_file_generator: InputFileGenerator
|
1887
|
+
) -> ActionEnvironment:
|
1831
1888
|
"""
|
1832
1889
|
Get the actual environment to use for an input file generator.
|
1833
1890
|
"""
|
1834
|
-
return self.
|
1891
|
+
return self.__get_resolved_action_env(
|
1835
1892
|
relevant_scopes=(
|
1836
1893
|
ActionScopeType.ANY,
|
1837
1894
|
ActionScopeType.PROCESSING,
|
@@ -1840,11 +1897,13 @@ class Action(JSONLike):
|
|
1840
1897
|
input_file_generator=input_file_generator,
|
1841
1898
|
)
|
1842
1899
|
|
1843
|
-
def get_output_file_parser_action_env(
|
1900
|
+
def get_output_file_parser_action_env(
|
1901
|
+
self, output_file_parser: OutputFileParser
|
1902
|
+
) -> ActionEnvironment:
|
1844
1903
|
"""
|
1845
1904
|
Get the actual environment to use for an output file parser.
|
1846
1905
|
"""
|
1847
|
-
return self.
|
1906
|
+
return self.__get_resolved_action_env(
|
1848
1907
|
relevant_scopes=(
|
1849
1908
|
ActionScopeType.ANY,
|
1850
1909
|
ActionScopeType.PROCESSING,
|
@@ -1853,11 +1912,11 @@ class Action(JSONLike):
|
|
1853
1912
|
output_file_parser=output_file_parser,
|
1854
1913
|
)
|
1855
1914
|
|
1856
|
-
def get_commands_action_env(self):
|
1915
|
+
def get_commands_action_env(self) -> ActionEnvironment:
|
1857
1916
|
"""
|
1858
1917
|
Get the actual environment to use for the action commands.
|
1859
1918
|
"""
|
1860
|
-
return self.
|
1919
|
+
return self.__get_resolved_action_env(
|
1861
1920
|
relevant_scopes=(ActionScopeType.ANY, ActionScopeType.MAIN),
|
1862
1921
|
commands=self.commands,
|
1863
1922
|
)
|
@@ -1868,43 +1927,51 @@ class Action(JSONLike):
|
|
1868
1927
|
"""
|
1869
1928
|
return self.get_environment_spec()["name"]
|
1870
1929
|
|
1871
|
-
def get_environment_spec(self) ->
|
1930
|
+
def get_environment_spec(self) -> Mapping[str, Any]:
|
1872
1931
|
"""
|
1873
1932
|
Get the specification for the primary envionment, assuming it has been expanded.
|
1874
1933
|
"""
|
1875
1934
|
if not self._from_expand:
|
1876
1935
|
raise RuntimeError(
|
1877
|
-
|
1878
|
-
|
1936
|
+
"Cannot choose a single environment from this action because it is not "
|
1937
|
+
"expanded, meaning multiple action environments might exist."
|
1879
1938
|
)
|
1880
1939
|
return self.environments[0].environment
|
1881
1940
|
|
1882
|
-
def get_environment(self) ->
|
1941
|
+
def get_environment(self) -> Environment:
|
1883
1942
|
"""
|
1884
1943
|
Get the primary environment.
|
1885
1944
|
"""
|
1886
|
-
return self.
|
1945
|
+
return self._app.envs.get(**self.get_environment_spec())
|
1887
1946
|
|
1888
1947
|
@staticmethod
|
1889
|
-
def is_snippet_script(script: str) -> bool:
|
1948
|
+
def is_snippet_script(script: str | None) -> bool:
|
1890
1949
|
"""Returns True if the provided script string represents a script snippets that is
|
1891
1950
|
to be modified before execution (e.g. to receive and provide parameter data)."""
|
1951
|
+
if script is None:
|
1952
|
+
return False
|
1892
1953
|
return script.startswith("<<script:")
|
1893
1954
|
|
1955
|
+
__SCRIPT_NAME_RE: ClassVar[Pattern] = re.compile(
|
1956
|
+
r"\<\<script:(?:.*(?:\/|\\))*(.*)\>\>"
|
1957
|
+
)
|
1958
|
+
|
1894
1959
|
@classmethod
|
1895
1960
|
def get_script_name(cls, script: str) -> str:
|
1896
1961
|
"""Return the script name."""
|
1897
1962
|
if cls.is_snippet_script(script):
|
1898
|
-
|
1899
|
-
|
1900
|
-
return match_obj
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1963
|
+
if not (match_obj := cls.__SCRIPT_NAME_RE.match(script)):
|
1964
|
+
raise ValueError("incomplete <<script:>>")
|
1965
|
+
return match_obj[1]
|
1966
|
+
# a script we can expect in the working directory:
|
1967
|
+
return script
|
1968
|
+
|
1969
|
+
__SCRIPT_RE: ClassVar[Pattern] = re.compile(r"\<\<script:(.*:?)\>\>")
|
1970
|
+
__ENV_RE: ClassVar[Pattern] = re.compile(r"\<\<env:(.*?)\>\>")
|
1904
1971
|
|
1905
1972
|
@classmethod
|
1906
1973
|
def get_snippet_script_str(
|
1907
|
-
cls, script, env_spec:
|
1974
|
+
cls, script: str, env_spec: Mapping[str, Any] | None = None
|
1908
1975
|
) -> str:
|
1909
1976
|
"""
|
1910
1977
|
Get the substituted script snippet path as a string.
|
@@ -1914,67 +1981,72 @@ class Action(JSONLike):
|
|
1914
1981
|
f"Must be an app-data script name (e.g. "
|
1915
1982
|
f"<<script:path/to/app/data/script.py>>), but received {script}"
|
1916
1983
|
)
|
1917
|
-
|
1918
|
-
|
1919
|
-
out = match_obj
|
1984
|
+
if not (match_obj := cls.__SCRIPT_RE.match(script)):
|
1985
|
+
raise ValueError("incomplete <<script:>>")
|
1986
|
+
out: str = match_obj[1]
|
1920
1987
|
|
1921
1988
|
if env_spec:
|
1922
|
-
out =
|
1923
|
-
|
1924
|
-
repl=lambda match_obj: env_spec[match_obj.group(1)],
|
1989
|
+
out = cls.__ENV_RE.sub(
|
1990
|
+
repl=lambda match_obj: env_spec[match_obj[1]],
|
1925
1991
|
string=out,
|
1926
1992
|
)
|
1927
1993
|
return out
|
1928
1994
|
|
1929
1995
|
@classmethod
|
1930
1996
|
def get_snippet_script_path(
|
1931
|
-
cls, script_path, env_spec:
|
1932
|
-
) -> Path:
|
1997
|
+
cls, script_path: str | None, env_spec: Mapping[str, Any] | None = None
|
1998
|
+
) -> Path | None:
|
1933
1999
|
"""
|
1934
2000
|
Get the substituted script snippet path, or False if there is no snippet.
|
1935
2001
|
"""
|
1936
2002
|
if not cls.is_snippet_script(script_path):
|
1937
|
-
return
|
2003
|
+
return None
|
1938
2004
|
|
2005
|
+
assert script_path is not None
|
1939
2006
|
path = cls.get_snippet_script_str(script_path, env_spec)
|
1940
|
-
|
1941
|
-
path = cls.app.scripts.get(path)
|
1942
|
-
|
1943
|
-
return Path(path)
|
2007
|
+
return Path(cls._app.scripts.get(path, path))
|
1944
2008
|
|
1945
2009
|
@staticmethod
|
1946
|
-
def __get_param_dump_file_stem(js_idx: int, js_act_idx: int):
|
2010
|
+
def __get_param_dump_file_stem(js_idx: int | str, js_act_idx: int | str) -> str:
|
1947
2011
|
return RunDirAppFiles.get_run_param_dump_file_prefix(js_idx, js_act_idx)
|
1948
2012
|
|
1949
2013
|
@staticmethod
|
1950
|
-
def __get_param_load_file_stem(js_idx: int, js_act_idx: int):
|
2014
|
+
def __get_param_load_file_stem(js_idx: int | str, js_act_idx: int | str) -> str:
|
1951
2015
|
return RunDirAppFiles.get_run_param_load_file_prefix(js_idx, js_act_idx)
|
1952
2016
|
|
1953
|
-
def get_param_dump_file_path_JSON(
|
2017
|
+
def get_param_dump_file_path_JSON(
|
2018
|
+
self, js_idx: int | str, js_act_idx: int | str
|
2019
|
+
) -> Path:
|
1954
2020
|
"""
|
1955
2021
|
Get the path of the JSON dump file.
|
1956
2022
|
"""
|
1957
2023
|
return Path(self.__get_param_dump_file_stem(js_idx, js_act_idx) + ".json")
|
1958
2024
|
|
1959
|
-
def get_param_dump_file_path_HDF5(
|
2025
|
+
def get_param_dump_file_path_HDF5(
|
2026
|
+
self, js_idx: int | str, js_act_idx: int | str
|
2027
|
+
) -> Path:
|
1960
2028
|
"""
|
1961
2029
|
Get the path of the HDF56 dump file.
|
1962
2030
|
"""
|
1963
2031
|
return Path(self.__get_param_dump_file_stem(js_idx, js_act_idx) + ".h5")
|
1964
2032
|
|
1965
|
-
def get_param_load_file_path_JSON(
|
2033
|
+
def get_param_load_file_path_JSON(
|
2034
|
+
self, js_idx: int | str, js_act_idx: int | str
|
2035
|
+
) -> Path:
|
1966
2036
|
"""
|
1967
2037
|
Get the path of the JSON load file.
|
1968
2038
|
"""
|
1969
2039
|
return Path(self.__get_param_load_file_stem(js_idx, js_act_idx) + ".json")
|
1970
2040
|
|
1971
|
-
def get_param_load_file_path_HDF5(
|
2041
|
+
def get_param_load_file_path_HDF5(
|
2042
|
+
self, js_idx: int | str, js_act_idx: int | str
|
2043
|
+
) -> Path:
|
1972
2044
|
"""
|
1973
2045
|
Get the path of the HDF5 load file.
|
1974
2046
|
"""
|
1975
2047
|
return Path(self.__get_param_load_file_stem(js_idx, js_act_idx) + ".h5")
|
1976
2048
|
|
1977
|
-
def expand(self):
|
2049
|
+
def expand(self) -> Sequence[Action]:
|
1978
2050
|
"""
|
1979
2051
|
Expand this action into a list of actions if necessary.
|
1980
2052
|
This converts input file generators and output file parsers into their own actions.
|
@@ -1983,154 +2055,159 @@ class Action(JSONLike):
|
|
1983
2055
|
# already expanded
|
1984
2056
|
return [self]
|
1985
2057
|
|
1986
|
-
|
1987
|
-
|
1988
|
-
|
1989
|
-
|
1990
|
-
|
1991
|
-
|
1992
|
-
# always run OPs, for now
|
1993
|
-
|
1994
|
-
main_rules = self.rules + [
|
1995
|
-
self.app.ActionRule.check_missing(f"output_files.{i.label}")
|
1996
|
-
for i in self.output_files
|
1997
|
-
]
|
2058
|
+
# run main if:
|
2059
|
+
# - one or more output files are not passed
|
2060
|
+
# run IFG if:
|
2061
|
+
# - one or more output files are not passed
|
2062
|
+
# - AND input file is not passed
|
2063
|
+
# always run OPs, for now
|
1998
2064
|
|
1999
|
-
|
2000
|
-
|
2065
|
+
main_rules = self.rules + [
|
2066
|
+
self._app.ActionRule.check_missing(f"output_files.{of.label}")
|
2067
|
+
for of in self.output_files
|
2068
|
+
]
|
2001
2069
|
|
2002
|
-
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
|
2018
|
-
|
2019
|
-
|
2020
|
-
|
2021
|
-
|
2022
|
-
|
2023
|
-
|
2024
|
-
|
2025
|
-
|
2026
|
-
|
2027
|
-
|
2028
|
-
)
|
2029
|
-
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
2034
|
-
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2038
|
-
|
2039
|
-
|
2040
|
-
|
2041
|
-
|
2042
|
-
|
2043
|
-
|
2044
|
-
|
2045
|
-
|
2046
|
-
|
2047
|
-
|
2048
|
-
|
2049
|
-
|
2050
|
-
|
2051
|
-
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
)
|
2061
|
-
|
2062
|
-
|
2063
|
-
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
|
2070
|
+
# note we keep the IFG/OPs in the new actions, so we can check the parameters
|
2071
|
+
# used/produced.
|
2072
|
+
|
2073
|
+
args: list[str]
|
2074
|
+
inp_files = []
|
2075
|
+
inp_acts: list[Action] = []
|
2076
|
+
for ifg in self.input_file_generators:
|
2077
|
+
exe = "<<executable:python_script>>"
|
2078
|
+
args = [
|
2079
|
+
'"$WK_PATH"',
|
2080
|
+
"$EAR_ID",
|
2081
|
+
] # WK_PATH could have a space in it
|
2082
|
+
if ifg.script:
|
2083
|
+
script_name = self.get_script_name(ifg.script)
|
2084
|
+
variables = {
|
2085
|
+
"script_name": script_name,
|
2086
|
+
"script_name_no_ext": str(Path(script_name).stem),
|
2087
|
+
}
|
2088
|
+
else:
|
2089
|
+
variables = {}
|
2090
|
+
act_i = self._app.Action(
|
2091
|
+
commands=[
|
2092
|
+
self._app.Command(executable=exe, arguments=args, variables=variables)
|
2093
|
+
],
|
2094
|
+
input_file_generators=[ifg],
|
2095
|
+
environments=[self.get_input_file_generator_action_env(ifg)],
|
2096
|
+
rules=main_rules + ifg.get_action_rules(),
|
2097
|
+
script_pass_env_spec=ifg.script_pass_env_spec,
|
2098
|
+
abortable=ifg.abortable,
|
2099
|
+
# TODO: add script_data_in etc? and to OFP?
|
2100
|
+
)
|
2101
|
+
act_i._task_schema = self.task_schema
|
2102
|
+
if ifg.input_file not in inp_files:
|
2103
|
+
inp_files.append(ifg.input_file)
|
2104
|
+
act_i._from_expand = True
|
2105
|
+
inp_acts.append(act_i)
|
2106
|
+
|
2107
|
+
out_files: list[FileSpec] = []
|
2108
|
+
out_acts: list[Action] = []
|
2109
|
+
for ofp in self.output_file_parsers:
|
2110
|
+
exe = "<<executable:python_script>>"
|
2111
|
+
args = [
|
2112
|
+
'"$WK_PATH"',
|
2113
|
+
"$EAR_ID",
|
2114
|
+
] # WK_PATH could have a space in it
|
2115
|
+
if ofp.script:
|
2116
|
+
script_name = self.get_script_name(ofp.script)
|
2117
|
+
variables = {
|
2118
|
+
"script_name": script_name,
|
2119
|
+
"script_name_no_ext": str(Path(script_name).stem),
|
2120
|
+
}
|
2121
|
+
else:
|
2122
|
+
variables = {}
|
2123
|
+
act_i = self._app.Action(
|
2124
|
+
commands=[
|
2125
|
+
self._app.Command(executable=exe, arguments=args, variables=variables)
|
2126
|
+
],
|
2127
|
+
output_file_parsers=[ofp],
|
2128
|
+
environments=[self.get_output_file_parser_action_env(ofp)],
|
2129
|
+
rules=[*self.rules, *ofp.get_action_rules()],
|
2130
|
+
script_pass_env_spec=ofp.script_pass_env_spec,
|
2131
|
+
abortable=ofp.abortable,
|
2132
|
+
)
|
2133
|
+
act_i._task_schema = self.task_schema
|
2134
|
+
for out_f in ofp.output_files:
|
2135
|
+
if out_f not in out_files:
|
2136
|
+
out_files.append(out_f)
|
2137
|
+
act_i._from_expand = True
|
2138
|
+
out_acts.append(act_i)
|
2139
|
+
|
2140
|
+
commands = self.commands
|
2141
|
+
if self.script:
|
2142
|
+
exe = f"<<executable:{self.script_exe}>>"
|
2143
|
+
args = []
|
2069
2144
|
if self.script:
|
2070
|
-
|
2071
|
-
|
2072
|
-
|
2073
|
-
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
2081
|
-
|
2082
|
-
|
2083
|
-
|
2084
|
-
|
2085
|
-
|
2086
|
-
|
2087
|
-
|
2088
|
-
|
2089
|
-
|
2090
|
-
args.append(
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
-
args.append(
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
commands += [
|
2107
|
-
self.app.Command(executable=exe, arguments=args, variables=variables)
|
2108
|
-
]
|
2109
|
-
|
2110
|
-
# TODO: store script_args? and build command with executable syntax?
|
2111
|
-
main_act = self.app.Action(
|
2112
|
-
commands=commands,
|
2113
|
-
script=self.script,
|
2114
|
-
script_data_in=self.script_data_in,
|
2115
|
-
script_data_out=self.script_data_out,
|
2116
|
-
script_exe=self.script_exe,
|
2117
|
-
script_pass_env_spec=self.script_pass_env_spec,
|
2118
|
-
environments=[self.get_commands_action_env()],
|
2119
|
-
abortable=self.abortable,
|
2120
|
-
rules=main_rules,
|
2121
|
-
input_files=inp_files,
|
2122
|
-
output_files=out_files,
|
2123
|
-
save_files=self.save_files,
|
2124
|
-
clean_up=self.clean_up,
|
2145
|
+
script_name = self.get_script_name(self.script)
|
2146
|
+
variables = {
|
2147
|
+
"script_name": script_name,
|
2148
|
+
"script_name_no_ext": str(Path(script_name).stem),
|
2149
|
+
}
|
2150
|
+
else:
|
2151
|
+
variables = {}
|
2152
|
+
if self.script_data_in_has_direct or self.script_data_out_has_direct:
|
2153
|
+
# WK_PATH could have a space in it:
|
2154
|
+
args.extend(("--wk-path", '"$WK_PATH"', "--run-id", "$EAR_ID"))
|
2155
|
+
|
2156
|
+
fn_args = {"js_idx": "${JS_IDX}", "js_act_idx": "${JS_act_idx}"}
|
2157
|
+
|
2158
|
+
for fmt in self.script_data_in_grouped:
|
2159
|
+
if fmt == "json":
|
2160
|
+
if self.script_data_files_use_opt:
|
2161
|
+
args.append("--inputs-json")
|
2162
|
+
args.append(str(self.get_param_dump_file_path_JSON(**fn_args)))
|
2163
|
+
elif fmt == "hdf5":
|
2164
|
+
if self.script_data_files_use_opt:
|
2165
|
+
args.append("--inputs-hdf5")
|
2166
|
+
args.append(str(self.get_param_dump_file_path_HDF5(**fn_args)))
|
2167
|
+
|
2168
|
+
for fmt in self.script_data_out_grouped:
|
2169
|
+
if fmt == "json":
|
2170
|
+
if self.script_data_files_use_opt:
|
2171
|
+
args.append("--outputs-json")
|
2172
|
+
args.append(str(self.get_param_load_file_path_JSON(**fn_args)))
|
2173
|
+
elif fmt == "hdf5":
|
2174
|
+
if self.script_data_files_use_opt:
|
2175
|
+
args.append("--outputs-hdf5")
|
2176
|
+
args.append(str(self.get_param_load_file_path_HDF5(**fn_args)))
|
2177
|
+
|
2178
|
+
commands.append(
|
2179
|
+
self._app.Command(executable=exe, arguments=args, variables=variables)
|
2125
2180
|
)
|
2126
|
-
main_act._task_schema = self.task_schema
|
2127
|
-
main_act._from_expand = True
|
2128
2181
|
|
2129
|
-
|
2182
|
+
# TODO: store script_args? and build command with executable syntax?
|
2183
|
+
main_act = self._app.Action(
|
2184
|
+
commands=commands,
|
2185
|
+
script=self.script,
|
2186
|
+
script_data_in=self.script_data_in,
|
2187
|
+
script_data_out=self.script_data_out,
|
2188
|
+
script_exe=self.script_exe,
|
2189
|
+
script_pass_env_spec=self.script_pass_env_spec,
|
2190
|
+
environments=[self.get_commands_action_env()],
|
2191
|
+
abortable=self.abortable,
|
2192
|
+
rules=main_rules,
|
2193
|
+
input_files=inp_files,
|
2194
|
+
output_files=out_files,
|
2195
|
+
save_files=self.save_files,
|
2196
|
+
clean_up=self.clean_up,
|
2197
|
+
)
|
2198
|
+
main_act._task_schema = self.task_schema
|
2199
|
+
main_act._from_expand = True
|
2200
|
+
main_act.process_script_data_formats()
|
2130
2201
|
|
2131
|
-
|
2202
|
+
return [*inp_acts, main_act, *out_acts]
|
2132
2203
|
|
2133
|
-
|
2204
|
+
# note: we use "parameter" rather than "input", because it could be a schema input
|
2205
|
+
# or schema output.
|
2206
|
+
__PARAMS_RE: ClassVar[Pattern] = re.compile(
|
2207
|
+
r"\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(.*?)\)?\>\>"
|
2208
|
+
)
|
2209
|
+
|
2210
|
+
def get_command_input_types(self, sub_parameters: bool = False) -> tuple[str, ...]:
|
2134
2211
|
"""Get parameter types from commands.
|
2135
2212
|
|
2136
2213
|
Parameters
|
@@ -2140,49 +2217,44 @@ class Action(JSONLike):
|
|
2140
2217
|
untouched. If False (default), only return the root parameter type and
|
2141
2218
|
disregard the sub-parameter part.
|
2142
2219
|
"""
|
2143
|
-
params =
|
2144
|
-
# note: we use "parameter" rather than "input", because it could be a schema input
|
2145
|
-
# or schema output.
|
2146
|
-
vars_regex = r"\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(.*?)\)?\>\>"
|
2220
|
+
params: set[str] = set()
|
2147
2221
|
for command in self.commands:
|
2148
|
-
|
2149
|
-
if
|
2150
|
-
|
2151
|
-
|
2152
|
-
for arg in command.arguments or
|
2153
|
-
|
2154
|
-
if
|
2155
|
-
|
2156
|
-
|
2222
|
+
params.update(
|
2223
|
+
val[1] if sub_parameters else val[1].split(".")[0]
|
2224
|
+
for val in self.__PARAMS_RE.finditer(command.command or "")
|
2225
|
+
)
|
2226
|
+
for arg in command.arguments or ():
|
2227
|
+
params.update(
|
2228
|
+
val[1] if sub_parameters else val[1].split(".")[0]
|
2229
|
+
for val in self.__PARAMS_RE.finditer(arg)
|
2230
|
+
)
|
2157
2231
|
# TODO: consider stdin?
|
2158
|
-
return tuple(
|
2232
|
+
return tuple(params)
|
2233
|
+
|
2234
|
+
__FILES_RE: ClassVar[Pattern] = re.compile(r"\<\<file:(.*?)\>\>")
|
2159
2235
|
|
2160
|
-
def get_command_input_file_labels(self) ->
|
2236
|
+
def get_command_input_file_labels(self) -> tuple[str, ...]:
|
2161
2237
|
"""Get input files types from commands."""
|
2162
|
-
files =
|
2163
|
-
vars_regex = r"\<\<file:(.*?)\>\>"
|
2238
|
+
files: set[str] = set()
|
2164
2239
|
for command in self.commands:
|
2165
|
-
|
2166
|
-
|
2167
|
-
|
2168
|
-
for val in re.findall(vars_regex, arg):
|
2169
|
-
files.append(val)
|
2240
|
+
files.update(self.__FILES_RE.findall(command.command or ""))
|
2241
|
+
for arg in command.arguments or ():
|
2242
|
+
files.update(self.__FILES_RE.findall(arg))
|
2170
2243
|
# TODO: consider stdin?
|
2171
|
-
return tuple(
|
2244
|
+
return tuple(files)
|
2172
2245
|
|
2173
|
-
def get_command_output_types(self) ->
|
2246
|
+
def get_command_output_types(self) -> tuple[str, ...]:
|
2174
2247
|
"""Get parameter types from command stdout and stderr arguments."""
|
2175
|
-
params =
|
2248
|
+
params: set[str] = set()
|
2176
2249
|
for command in self.commands:
|
2177
2250
|
out_params = command.get_output_types()
|
2178
2251
|
if out_params["stdout"]:
|
2179
|
-
params.
|
2252
|
+
params.add(out_params["stdout"])
|
2180
2253
|
if out_params["stderr"]:
|
2181
|
-
params.
|
2254
|
+
params.add(out_params["stderr"])
|
2255
|
+
return tuple(params)
|
2182
2256
|
|
2183
|
-
|
2184
|
-
|
2185
|
-
def get_input_types(self, sub_parameters: bool = False) -> Tuple[str]:
|
2257
|
+
def get_input_types(self, sub_parameters: bool = False) -> tuple[str, ...]:
|
2186
2258
|
"""Get the input types that are consumed by commands and input file generators of
|
2187
2259
|
this action.
|
2188
2260
|
|
@@ -2193,80 +2265,75 @@ class Action(JSONLike):
|
|
2193
2265
|
inputs will be returned untouched. If False (default), only return the root
|
2194
2266
|
parameter type and disregard the sub-parameter part.
|
2195
2267
|
"""
|
2196
|
-
|
2268
|
+
if (
|
2197
2269
|
self.script
|
2198
2270
|
and not self.input_file_generators
|
2199
2271
|
and not self.output_file_parsers
|
2200
|
-
)
|
2201
|
-
|
2202
|
-
params = self.task_schema.input_types
|
2272
|
+
):
|
2273
|
+
params = set(self.task_schema.input_types)
|
2203
2274
|
else:
|
2204
|
-
params =
|
2205
|
-
for
|
2206
|
-
params.
|
2207
|
-
for
|
2208
|
-
params.
|
2209
|
-
return tuple(
|
2210
|
-
|
2211
|
-
def get_output_types(self) ->
|
2275
|
+
params = set(self.get_command_input_types(sub_parameters))
|
2276
|
+
for ifg in self.input_file_generators:
|
2277
|
+
params.update(inp.typ for inp in ifg.inputs)
|
2278
|
+
for ofp in self.output_file_parsers:
|
2279
|
+
params.update(ofp.inputs or ())
|
2280
|
+
return tuple(params)
|
2281
|
+
|
2282
|
+
def get_output_types(self) -> tuple[str, ...]:
|
2212
2283
|
"""Get the output types that are produced by command standard outputs and errors,
|
2213
2284
|
and by output file parsers of this action."""
|
2214
|
-
|
2285
|
+
if (
|
2215
2286
|
self.script
|
2216
2287
|
and not self.input_file_generators
|
2217
2288
|
and not self.output_file_parsers
|
2218
|
-
)
|
2219
|
-
|
2220
|
-
params = self.task_schema.output_types
|
2289
|
+
):
|
2290
|
+
params = set(self.task_schema.output_types)
|
2221
2291
|
else:
|
2222
|
-
params =
|
2223
|
-
for
|
2224
|
-
if
|
2225
|
-
params.
|
2226
|
-
params.
|
2227
|
-
return tuple(
|
2292
|
+
params = set(self.get_command_output_types())
|
2293
|
+
for ofp in self.output_file_parsers:
|
2294
|
+
if ofp.output is not None:
|
2295
|
+
params.add(ofp.output.typ)
|
2296
|
+
params.update(ofp.outputs or ())
|
2297
|
+
return tuple(params)
|
2228
2298
|
|
2229
|
-
def get_input_file_labels(self):
|
2299
|
+
def get_input_file_labels(self) -> tuple[str, ...]:
|
2230
2300
|
"""
|
2231
2301
|
Get the labels from the input files.
|
2232
2302
|
"""
|
2233
|
-
return tuple(
|
2303
|
+
return tuple(in_f.label for in_f in self.input_files)
|
2234
2304
|
|
2235
|
-
def get_output_file_labels(self):
|
2305
|
+
def get_output_file_labels(self) -> tuple[str, ...]:
|
2236
2306
|
"""
|
2237
2307
|
Get the labels from the output files.
|
2238
2308
|
"""
|
2239
|
-
return tuple(
|
2309
|
+
return tuple(out_f.label for out_f in self.output_files)
|
2240
2310
|
|
2241
2311
|
@TimeIt.decorator
|
2242
2312
|
def generate_data_index(
|
2243
2313
|
self,
|
2244
|
-
act_idx,
|
2245
|
-
EAR_ID,
|
2246
|
-
schema_data_idx,
|
2247
|
-
all_data_idx,
|
2248
|
-
workflow,
|
2249
|
-
param_source,
|
2250
|
-
) ->
|
2314
|
+
act_idx: int,
|
2315
|
+
EAR_ID: int,
|
2316
|
+
schema_data_idx: DataIndex,
|
2317
|
+
all_data_idx: dict[tuple[int, int], DataIndex],
|
2318
|
+
workflow: Workflow,
|
2319
|
+
param_source: ParamSource,
|
2320
|
+
) -> list[int | list[int]]:
|
2251
2321
|
"""Generate the data index for this action of an element iteration whose overall
|
2252
2322
|
data index is passed.
|
2253
2323
|
|
2254
2324
|
This mutates `all_data_idx`.
|
2255
|
-
|
2256
2325
|
"""
|
2257
2326
|
|
2258
2327
|
# output keys must be processed first for this to work, since when processing an
|
2259
2328
|
# output key, we may need to update the index of an output in a previous action's
|
2260
2329
|
# data index, which could affect the data index in an input of this action.
|
2261
|
-
keys = [f"outputs.{
|
2262
|
-
keys
|
2263
|
-
for
|
2264
|
-
|
2265
|
-
for i in self.output_files:
|
2266
|
-
keys.append(f"output_files.{i.label}")
|
2330
|
+
keys = [f"outputs.{typ}" for typ in self.get_output_types()]
|
2331
|
+
keys.extend(f"inputs.{typ}" for typ in self.get_input_types())
|
2332
|
+
keys.extend(f"input_files.{file.label}" for file in self.input_files)
|
2333
|
+
keys.extend(f"output_files.{file.label}" for file in self.output_files)
|
2267
2334
|
|
2268
2335
|
# these are consumed by the OFP, so should not be considered to generate new data:
|
2269
|
-
OFP_outs =
|
2336
|
+
OFP_outs = {j for ofp in self.output_file_parsers for j in ofp.outputs or ()}
|
2270
2337
|
|
2271
2338
|
# keep all resources and repeats data:
|
2272
2339
|
sub_data_idx = {
|
@@ -2274,37 +2341,40 @@ class Action(JSONLike):
|
|
2274
2341
|
for k, v in schema_data_idx.items()
|
2275
2342
|
if ("resources" in k or "repeats" in k)
|
2276
2343
|
}
|
2277
|
-
param_src_update = []
|
2344
|
+
param_src_update: list[int | list[int]] = []
|
2278
2345
|
for key in keys:
|
2279
|
-
sub_param_idx = {}
|
2346
|
+
sub_param_idx: dict[str, int | list[int]] = {}
|
2280
2347
|
if (
|
2281
2348
|
key.startswith("input_files")
|
2282
2349
|
or key.startswith("output_files")
|
2283
2350
|
or key.startswith("inputs")
|
2284
|
-
or (
|
2351
|
+
or (
|
2352
|
+
key.startswith("outputs") and key.removeprefix("outputs.") in OFP_outs
|
2353
|
+
)
|
2285
2354
|
):
|
2286
2355
|
# look for an index in previous data indices (where for inputs we look
|
2287
2356
|
# for *output* parameters of the same name):
|
2288
|
-
k_idx = None
|
2357
|
+
k_idx: int | list[int] | None = None
|
2289
2358
|
for prev_data_idx in all_data_idx.values():
|
2290
2359
|
if key.startswith("inputs"):
|
2291
|
-
k_param = key.
|
2360
|
+
k_param = key.removeprefix("inputs.")
|
2292
2361
|
k_out = f"outputs.{k_param}"
|
2293
2362
|
if k_out in prev_data_idx:
|
2294
2363
|
k_idx = prev_data_idx[k_out]
|
2295
|
-
|
2296
|
-
|
2297
|
-
if key in prev_data_idx:
|
2298
|
-
k_idx = prev_data_idx[key]
|
2364
|
+
elif key in prev_data_idx:
|
2365
|
+
k_idx = prev_data_idx[key]
|
2299
2366
|
|
2300
2367
|
if k_idx is None:
|
2301
2368
|
# otherwise take from the schema_data_idx:
|
2302
2369
|
if key in schema_data_idx:
|
2303
2370
|
k_idx = schema_data_idx[key]
|
2371
|
+
prefix = f"{key}." # sub-parameter (note dot)
|
2304
2372
|
# add any associated sub-parameters:
|
2305
|
-
|
2306
|
-
|
2307
|
-
|
2373
|
+
sub_param_idx.update(
|
2374
|
+
(k, v)
|
2375
|
+
for k, v in schema_data_idx.items()
|
2376
|
+
if k.startswith(prefix)
|
2377
|
+
)
|
2308
2378
|
else:
|
2309
2379
|
# otherwise we need to allocate a new parameter datum:
|
2310
2380
|
# (for input/output_files keys)
|
@@ -2313,13 +2383,12 @@ class Action(JSONLike):
|
|
2313
2383
|
else:
|
2314
2384
|
# outputs
|
2315
2385
|
k_idx = None
|
2316
|
-
for (
|
2386
|
+
for (_, EAR_ID_i), prev_data_idx in all_data_idx.items():
|
2317
2387
|
if key in prev_data_idx:
|
2318
2388
|
k_idx = prev_data_idx[key]
|
2319
2389
|
|
2320
2390
|
# allocate a new parameter datum for this intermediate output:
|
2321
|
-
param_source_i = copy.
|
2322
|
-
# param_source_i["action_idx"] = act_idx_i
|
2391
|
+
param_source_i = copy.copy(param_source)
|
2323
2392
|
param_source_i["EAR_ID"] = EAR_ID_i
|
2324
2393
|
new_k_idx = workflow._add_unset_parameter_data(param_source_i)
|
2325
2394
|
|
@@ -2336,36 +2405,48 @@ class Action(JSONLike):
|
|
2336
2405
|
sub_data_idx[key] = k_idx
|
2337
2406
|
sub_data_idx.update(sub_param_idx)
|
2338
2407
|
|
2339
|
-
all_data_idx[
|
2408
|
+
all_data_idx[act_idx, EAR_ID] = sub_data_idx
|
2340
2409
|
|
2341
2410
|
return param_src_update
|
2342
2411
|
|
2343
|
-
def get_possible_scopes(self) ->
|
2412
|
+
def get_possible_scopes(self) -> tuple[ActionScope, ...]:
|
2344
2413
|
"""Get the action scopes that are inclusive of this action, ordered by decreasing
|
2345
2414
|
specificity."""
|
2346
2415
|
|
2347
2416
|
scope = self.get_precise_scope()
|
2348
|
-
|
2349
2417
|
if self.input_file_generators:
|
2350
|
-
|
2418
|
+
return (
|
2351
2419
|
scope,
|
2352
|
-
self.
|
2353
|
-
self.
|
2354
|
-
self.
|
2420
|
+
self._app.ActionScope.input_file_generator(),
|
2421
|
+
self._app.ActionScope.processing(),
|
2422
|
+
self._app.ActionScope.any(),
|
2355
2423
|
)
|
2356
2424
|
elif self.output_file_parsers:
|
2357
|
-
|
2425
|
+
return (
|
2358
2426
|
scope,
|
2359
|
-
self.
|
2360
|
-
self.
|
2361
|
-
self.
|
2427
|
+
self._app.ActionScope.output_file_parser(),
|
2428
|
+
self._app.ActionScope.processing(),
|
2429
|
+
self._app.ActionScope.any(),
|
2362
2430
|
)
|
2363
2431
|
else:
|
2364
|
-
|
2432
|
+
return (scope, self._app.ActionScope.any())
|
2433
|
+
|
2434
|
+
def _get_possible_scopes_reversed(self) -> Iterator[ActionScope]:
|
2435
|
+
"""Get the action scopes that are inclusive of this action, ordered by increasing
|
2436
|
+
specificity."""
|
2365
2437
|
|
2366
|
-
|
2438
|
+
# Fail early if a failure is possible
|
2439
|
+
precise_scope = self.get_precise_scope()
|
2440
|
+
yield self._app.ActionScope.any()
|
2441
|
+
if self.input_file_generators:
|
2442
|
+
yield self._app.ActionScope.processing()
|
2443
|
+
yield self._app.ActionScope.input_file_generator()
|
2444
|
+
elif self.output_file_parsers:
|
2445
|
+
yield self._app.ActionScope.processing()
|
2446
|
+
yield self._app.ActionScope.output_file_parser()
|
2447
|
+
yield precise_scope
|
2367
2448
|
|
2368
|
-
def get_precise_scope(self) ->
|
2449
|
+
def get_precise_scope(self) -> ActionScope:
|
2369
2450
|
"""
|
2370
2451
|
Get the exact scope of this action.
|
2371
2452
|
The action must have been expanded prior to calling this.
|
@@ -2377,21 +2458,21 @@ class Action(JSONLike):
|
|
2377
2458
|
)
|
2378
2459
|
|
2379
2460
|
if self.input_file_generators:
|
2380
|
-
return self.
|
2461
|
+
return self._app.ActionScope.input_file_generator(
|
2381
2462
|
file=self.input_file_generators[0].input_file.label
|
2382
2463
|
)
|
2383
2464
|
elif self.output_file_parsers:
|
2384
2465
|
if self.output_file_parsers[0].output is not None:
|
2385
|
-
return self.
|
2466
|
+
return self._app.ActionScope.output_file_parser(
|
2386
2467
|
output=self.output_file_parsers[0].output
|
2387
2468
|
)
|
2388
2469
|
else:
|
2389
|
-
return self.
|
2470
|
+
return self._app.ActionScope.output_file_parser()
|
2390
2471
|
else:
|
2391
|
-
return self.
|
2472
|
+
return self._app.ActionScope.main()
|
2392
2473
|
|
2393
2474
|
def is_input_type_required(
|
2394
|
-
self, typ: str, provided_files:
|
2475
|
+
self, typ: str, provided_files: Container[FileSpec]
|
2395
2476
|
) -> bool:
|
2396
2477
|
"""
|
2397
2478
|
Determine if the given input type is required by this action.
|
@@ -2410,38 +2491,30 @@ class Action(JSONLike):
|
|
2410
2491
|
|
2411
2492
|
# typ is required if used in any input file generators and input file is not
|
2412
2493
|
# provided:
|
2413
|
-
for
|
2414
|
-
if typ in (
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2418
|
-
# typ is required if used in any output file parser
|
2419
|
-
for OFP in self.output_file_parsers:
|
2420
|
-
if typ in (OFP.inputs or []):
|
2494
|
+
for ifg in self.input_file_generators:
|
2495
|
+
if typ in (inp.typ for inp in ifg.inputs) and (
|
2496
|
+
ifg.input_file not in provided_files
|
2497
|
+
):
|
2421
2498
|
return True
|
2422
2499
|
|
2423
|
-
#
|
2424
|
-
return
|
2500
|
+
# typ is required if used in any output file parser
|
2501
|
+
return any(typ in (ofp.inputs or ()) for ofp in self.output_file_parsers)
|
2425
2502
|
|
2426
2503
|
@TimeIt.decorator
|
2427
|
-
def test_rules(self, element_iter) ->
|
2504
|
+
def test_rules(self, element_iter: ElementIteration) -> tuple[bool, list[int]]:
|
2428
2505
|
"""Test all rules against the specified element iteration."""
|
2429
|
-
|
2430
|
-
|
2431
|
-
|
2432
|
-
|
2433
|
-
for cmd_idx, cmd in enumerate(self.commands)
|
2434
|
-
|
2435
|
-
|
2436
|
-
|
2437
|
-
|
2438
|
-
|
2439
|
-
def get_required_executables(self) -> Tuple[str]:
|
2506
|
+
if any(not rule.test(element_iteration=element_iter) for rule in self.rules):
|
2507
|
+
return False, []
|
2508
|
+
return True, [
|
2509
|
+
cmd_idx
|
2510
|
+
for cmd_idx, cmd in enumerate(self.commands)
|
2511
|
+
if all(rule.test(element_iteration=element_iter) for rule in cmd.rules)
|
2512
|
+
]
|
2513
|
+
|
2514
|
+
def get_required_executables(self) -> Iterator[str]:
|
2440
2515
|
"""Return executable labels required by this action."""
|
2441
|
-
exec_labs = []
|
2442
2516
|
for command in self.commands:
|
2443
|
-
|
2444
|
-
return tuple(set(exec_labs))
|
2517
|
+
yield from command.get_required_executables()
|
2445
2518
|
|
2446
2519
|
def compose_source(self, snip_path: Path) -> str:
|
2447
2520
|
"""Generate the file contents of this source."""
|
@@ -2453,8 +2526,7 @@ class Action(JSONLike):
|
|
2453
2526
|
if not self.script_is_python:
|
2454
2527
|
return script_str
|
2455
2528
|
|
2456
|
-
py_imports =
|
2457
|
-
"""\
|
2529
|
+
py_imports = """
|
2458
2530
|
import argparse, sys
|
2459
2531
|
from pathlib import Path
|
2460
2532
|
|
@@ -2466,29 +2538,25 @@ class Action(JSONLike):
|
|
2466
2538
|
parser.add_argument("--outputs-json")
|
2467
2539
|
parser.add_argument("--outputs-hdf5")
|
2468
2540
|
args = parser.parse_args()
|
2469
|
-
|
2470
|
-
"""
|
2471
|
-
)
|
2541
|
+
"""
|
2472
2542
|
|
2473
2543
|
# if any direct inputs/outputs, we must load the workflow (must be python):
|
2474
2544
|
if self.script_data_in_has_direct or self.script_data_out_has_direct:
|
2475
|
-
py_main_block_workflow_load =
|
2476
|
-
|
2477
|
-
|
2478
|
-
|
2479
|
-
|
2480
|
-
|
2481
|
-
|
2482
|
-
|
2483
|
-
|
2484
|
-
|
2485
|
-
|
2486
|
-
|
2487
|
-
|
2488
|
-
|
2489
|
-
|
2490
|
-
cfg_dir=self.app.config.config_directory,
|
2491
|
-
cfg_invoc_key=self.app.config.config_key,
|
2545
|
+
py_main_block_workflow_load = """
|
2546
|
+
import {app_module} as app
|
2547
|
+
app.load_config(
|
2548
|
+
log_file_path=Path("{run_log_file}").resolve(),
|
2549
|
+
config_dir=r"{cfg_dir}",
|
2550
|
+
config_key=r"{cfg_invoc_key}",
|
2551
|
+
)
|
2552
|
+
wk_path, EAR_ID = args.wk_path, args.run_id
|
2553
|
+
wk = app.Workflow(wk_path)
|
2554
|
+
EAR = wk.get_EARs_from_IDs([EAR_ID])[0]
|
2555
|
+
""".format(
|
2556
|
+
run_log_file=self._app.RunDirAppFiles.get_log_file_name(),
|
2557
|
+
app_module=self._app.module,
|
2558
|
+
cfg_dir=self._app.config.config_directory,
|
2559
|
+
cfg_invoc_key=self._app.config.config_key,
|
2492
2560
|
)
|
2493
2561
|
else:
|
2494
2562
|
py_main_block_workflow_load = ""
|
@@ -2496,40 +2564,33 @@ class Action(JSONLike):
|
|
2496
2564
|
func_kwargs_lst = []
|
2497
2565
|
if "direct" in self.script_data_in_grouped:
|
2498
2566
|
direct_ins_str = "direct_ins = EAR.get_input_values_direct()"
|
2499
|
-
|
2500
|
-
func_kwargs_lst.append(direct_ins_arg_str)
|
2567
|
+
func_kwargs_lst.append("**direct_ins")
|
2501
2568
|
else:
|
2502
2569
|
direct_ins_str = ""
|
2503
2570
|
|
2504
2571
|
if self.script_data_in_has_files:
|
2505
2572
|
# need to pass "_input_files" keyword argument to script main function:
|
2506
|
-
input_files_str =
|
2507
|
-
"""\
|
2573
|
+
input_files_str = """
|
2508
2574
|
inp_files = {}
|
2509
2575
|
if args.inputs_json:
|
2510
2576
|
inp_files["json"] = Path(args.inputs_json)
|
2511
2577
|
if args.inputs_hdf5:
|
2512
2578
|
inp_files["hdf5"] = Path(args.inputs_hdf5)
|
2513
2579
|
"""
|
2514
|
-
)
|
2515
|
-
input_files_arg_str = "_input_files=inp_files"
|
2516
|
-
func_kwargs_lst.append(input_files_arg_str)
|
2580
|
+
func_kwargs_lst.append("_input_files=inp_files")
|
2517
2581
|
else:
|
2518
2582
|
input_files_str = ""
|
2519
2583
|
|
2520
2584
|
if self.script_data_out_has_files:
|
2521
2585
|
# need to pass "_output_files" keyword argument to script main function:
|
2522
|
-
output_files_str =
|
2523
|
-
"""\
|
2586
|
+
output_files_str = """
|
2524
2587
|
out_files = {}
|
2525
2588
|
if args.outputs_json:
|
2526
2589
|
out_files["json"] = Path(args.outputs_json)
|
2527
2590
|
if args.outputs_hdf5:
|
2528
2591
|
out_files["hdf5"] = Path(args.outputs_hdf5)
|
2529
2592
|
"""
|
2530
|
-
)
|
2531
|
-
output_files_arg_str = "_output_files=out_files"
|
2532
|
-
func_kwargs_lst.append(output_files_arg_str)
|
2593
|
+
func_kwargs_lst.append("_output_files=out_files")
|
2533
2594
|
|
2534
2595
|
else:
|
2535
2596
|
output_files_str = ""
|
@@ -2538,13 +2599,11 @@ class Action(JSONLike):
|
|
2538
2599
|
func_invoke_str = f"{script_main_func}({', '.join(func_kwargs_lst)})"
|
2539
2600
|
if "direct" in self.script_data_out_grouped:
|
2540
2601
|
py_main_block_invoke = f"outputs = {func_invoke_str}"
|
2541
|
-
py_main_block_outputs =
|
2542
|
-
"""\
|
2602
|
+
py_main_block_outputs = """
|
2543
2603
|
outputs = {"outputs." + k: v for k, v in outputs.items()}
|
2544
2604
|
for name_i, out_i in outputs.items():
|
2545
2605
|
wk.set_parameter_value(param_id=EAR.data_idx[name_i], value=out_i)
|
2546
|
-
|
2547
|
-
)
|
2606
|
+
"""
|
2548
2607
|
else:
|
2549
2608
|
py_main_block_invoke = func_invoke_str
|
2550
2609
|
py_main_block_outputs = ""
|
@@ -2562,13 +2621,13 @@ class Action(JSONLike):
|
|
2562
2621
|
{outputs}
|
2563
2622
|
"""
|
2564
2623
|
).format(
|
2565
|
-
py_imports=indent(py_imports, tab_indent),
|
2566
|
-
wk_load=indent(py_main_block_workflow_load, tab_indent),
|
2624
|
+
py_imports=indent(dedent(py_imports), tab_indent),
|
2625
|
+
wk_load=indent(dedent(py_main_block_workflow_load), tab_indent),
|
2567
2626
|
direct_ins=indent(direct_ins_str, tab_indent),
|
2568
|
-
in_files=indent(input_files_str, tab_indent),
|
2569
|
-
out_files=indent(output_files_str, tab_indent),
|
2627
|
+
in_files=indent(dedent(input_files_str), tab_indent),
|
2628
|
+
out_files=indent(dedent(output_files_str), tab_indent),
|
2570
2629
|
invoke=indent(py_main_block_invoke, tab_indent),
|
2571
|
-
outputs=indent(py_main_block_outputs, tab_indent),
|
2630
|
+
outputs=indent(dedent(py_main_block_outputs), tab_indent),
|
2572
2631
|
)
|
2573
2632
|
|
2574
2633
|
out = dedent(
|
@@ -2583,7 +2642,7 @@ class Action(JSONLike):
|
|
2583
2642
|
|
2584
2643
|
return out
|
2585
2644
|
|
2586
|
-
def get_parameter_names(self, prefix: str) ->
|
2645
|
+
def get_parameter_names(self, prefix: str) -> list[str]:
|
2587
2646
|
"""Get parameter types associated with a given prefix.
|
2588
2647
|
|
2589
2648
|
For example, with the prefix "inputs", this would return `['p1', 'p2']` for an
|
@@ -2604,11 +2663,12 @@ class Action(JSONLike):
|
|
2604
2663
|
"""
|
2605
2664
|
if prefix == "inputs":
|
2606
2665
|
single_lab_lookup = self.task_schema._get_single_label_lookup()
|
2607
|
-
|
2666
|
+
return [single_lab_lookup.get(i, i) for i in self.get_input_types()]
|
2608
2667
|
elif prefix == "outputs":
|
2609
|
-
|
2668
|
+
return list(self.get_output_types())
|
2610
2669
|
elif prefix == "input_files":
|
2611
|
-
|
2670
|
+
return list(self.get_input_file_labels())
|
2612
2671
|
elif prefix == "output_files":
|
2613
|
-
|
2614
|
-
|
2672
|
+
return list(self.get_output_file_labels())
|
2673
|
+
else:
|
2674
|
+
raise ValueError(f"unexpected prefix: {prefix}")
|