hpcflow 0.1.9__py3-none-any.whl → 0.2.0a271__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. hpcflow/__init__.py +2 -11
  2. hpcflow/__pyinstaller/__init__.py +5 -0
  3. hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
  4. hpcflow/_version.py +1 -1
  5. hpcflow/app.py +43 -0
  6. hpcflow/cli.py +2 -462
  7. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  8. hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
  9. hpcflow/data/jinja_templates/test/test_template.txt +8 -0
  10. hpcflow/data/programs/hello_world/README.md +1 -0
  11. hpcflow/data/programs/hello_world/hello_world.c +87 -0
  12. hpcflow/data/programs/hello_world/linux/hello_world +0 -0
  13. hpcflow/data/programs/hello_world/macos/hello_world +0 -0
  14. hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
  15. hpcflow/data/scripts/__init__.py +1 -0
  16. hpcflow/data/scripts/bad_script.py +2 -0
  17. hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
  18. hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
  19. hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
  20. hpcflow/data/scripts/do_nothing.py +2 -0
  21. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  22. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  23. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  24. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  25. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  26. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  27. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  28. hpcflow/data/scripts/generate_t1_file_01.py +7 -0
  29. hpcflow/data/scripts/import_future_script.py +7 -0
  30. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  31. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  32. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  33. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  34. hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
  35. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  36. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  37. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  38. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  39. hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
  40. hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
  41. hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
  42. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  43. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  44. hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
  45. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
  46. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  47. hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
  48. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
  49. hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
  50. hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
  51. hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
  52. hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
  53. hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
  54. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  55. hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
  56. hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
  57. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  58. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  59. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  60. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  61. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  62. hpcflow/data/scripts/parse_t1_file_01.py +4 -0
  63. hpcflow/data/scripts/script_exit_test.py +5 -0
  64. hpcflow/data/template_components/__init__.py +1 -0
  65. hpcflow/data/template_components/command_files.yaml +26 -0
  66. hpcflow/data/template_components/environments.yaml +13 -0
  67. hpcflow/data/template_components/parameters.yaml +14 -0
  68. hpcflow/data/template_components/task_schemas.yaml +139 -0
  69. hpcflow/data/workflows/workflow_1.yaml +5 -0
  70. hpcflow/examples.ipynb +1037 -0
  71. hpcflow/sdk/__init__.py +149 -0
  72. hpcflow/sdk/app.py +4266 -0
  73. hpcflow/sdk/cli.py +1479 -0
  74. hpcflow/sdk/cli_common.py +385 -0
  75. hpcflow/sdk/config/__init__.py +5 -0
  76. hpcflow/sdk/config/callbacks.py +246 -0
  77. hpcflow/sdk/config/cli.py +388 -0
  78. hpcflow/sdk/config/config.py +1410 -0
  79. hpcflow/sdk/config/config_file.py +501 -0
  80. hpcflow/sdk/config/errors.py +272 -0
  81. hpcflow/sdk/config/types.py +150 -0
  82. hpcflow/sdk/core/__init__.py +38 -0
  83. hpcflow/sdk/core/actions.py +3857 -0
  84. hpcflow/sdk/core/app_aware.py +25 -0
  85. hpcflow/sdk/core/cache.py +224 -0
  86. hpcflow/sdk/core/command_files.py +814 -0
  87. hpcflow/sdk/core/commands.py +424 -0
  88. hpcflow/sdk/core/element.py +2071 -0
  89. hpcflow/sdk/core/enums.py +221 -0
  90. hpcflow/sdk/core/environment.py +256 -0
  91. hpcflow/sdk/core/errors.py +1043 -0
  92. hpcflow/sdk/core/execute.py +207 -0
  93. hpcflow/sdk/core/json_like.py +809 -0
  94. hpcflow/sdk/core/loop.py +1320 -0
  95. hpcflow/sdk/core/loop_cache.py +282 -0
  96. hpcflow/sdk/core/object_list.py +933 -0
  97. hpcflow/sdk/core/parameters.py +3371 -0
  98. hpcflow/sdk/core/rule.py +196 -0
  99. hpcflow/sdk/core/run_dir_files.py +57 -0
  100. hpcflow/sdk/core/skip_reason.py +7 -0
  101. hpcflow/sdk/core/task.py +3792 -0
  102. hpcflow/sdk/core/task_schema.py +993 -0
  103. hpcflow/sdk/core/test_utils.py +538 -0
  104. hpcflow/sdk/core/types.py +447 -0
  105. hpcflow/sdk/core/utils.py +1207 -0
  106. hpcflow/sdk/core/validation.py +87 -0
  107. hpcflow/sdk/core/values.py +477 -0
  108. hpcflow/sdk/core/workflow.py +4820 -0
  109. hpcflow/sdk/core/zarr_io.py +206 -0
  110. hpcflow/sdk/data/__init__.py +13 -0
  111. hpcflow/sdk/data/config_file_schema.yaml +34 -0
  112. hpcflow/sdk/data/config_schema.yaml +260 -0
  113. hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
  114. hpcflow/sdk/data/files_spec_schema.yaml +5 -0
  115. hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
  116. hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
  117. hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
  118. hpcflow/sdk/demo/__init__.py +3 -0
  119. hpcflow/sdk/demo/cli.py +242 -0
  120. hpcflow/sdk/helper/__init__.py +3 -0
  121. hpcflow/sdk/helper/cli.py +137 -0
  122. hpcflow/sdk/helper/helper.py +300 -0
  123. hpcflow/sdk/helper/watcher.py +192 -0
  124. hpcflow/sdk/log.py +288 -0
  125. hpcflow/sdk/persistence/__init__.py +18 -0
  126. hpcflow/sdk/persistence/base.py +2817 -0
  127. hpcflow/sdk/persistence/defaults.py +6 -0
  128. hpcflow/sdk/persistence/discovery.py +39 -0
  129. hpcflow/sdk/persistence/json.py +954 -0
  130. hpcflow/sdk/persistence/pending.py +948 -0
  131. hpcflow/sdk/persistence/store_resource.py +203 -0
  132. hpcflow/sdk/persistence/types.py +309 -0
  133. hpcflow/sdk/persistence/utils.py +73 -0
  134. hpcflow/sdk/persistence/zarr.py +2388 -0
  135. hpcflow/sdk/runtime.py +320 -0
  136. hpcflow/sdk/submission/__init__.py +3 -0
  137. hpcflow/sdk/submission/enums.py +70 -0
  138. hpcflow/sdk/submission/jobscript.py +2379 -0
  139. hpcflow/sdk/submission/schedulers/__init__.py +281 -0
  140. hpcflow/sdk/submission/schedulers/direct.py +233 -0
  141. hpcflow/sdk/submission/schedulers/sge.py +376 -0
  142. hpcflow/sdk/submission/schedulers/slurm.py +598 -0
  143. hpcflow/sdk/submission/schedulers/utils.py +25 -0
  144. hpcflow/sdk/submission/shells/__init__.py +52 -0
  145. hpcflow/sdk/submission/shells/base.py +229 -0
  146. hpcflow/sdk/submission/shells/bash.py +504 -0
  147. hpcflow/sdk/submission/shells/os_version.py +115 -0
  148. hpcflow/sdk/submission/shells/powershell.py +352 -0
  149. hpcflow/sdk/submission/submission.py +1402 -0
  150. hpcflow/sdk/submission/types.py +140 -0
  151. hpcflow/sdk/typing.py +194 -0
  152. hpcflow/sdk/utils/arrays.py +69 -0
  153. hpcflow/sdk/utils/deferred_file.py +55 -0
  154. hpcflow/sdk/utils/hashing.py +16 -0
  155. hpcflow/sdk/utils/patches.py +31 -0
  156. hpcflow/sdk/utils/strings.py +69 -0
  157. hpcflow/tests/api/test_api.py +32 -0
  158. hpcflow/tests/conftest.py +123 -0
  159. hpcflow/tests/data/__init__.py +0 -0
  160. hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
  161. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  162. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  163. hpcflow/tests/data/workflow_1.json +10 -0
  164. hpcflow/tests/data/workflow_1.yaml +5 -0
  165. hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
  166. hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
  167. hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
  168. hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
  169. hpcflow/tests/programs/test_programs.py +180 -0
  170. hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
  171. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  172. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
  173. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  174. hpcflow/tests/scripts/test_main_scripts.py +1361 -0
  175. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  176. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  177. hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
  178. hpcflow/tests/unit/test_action.py +1066 -0
  179. hpcflow/tests/unit/test_action_rule.py +24 -0
  180. hpcflow/tests/unit/test_app.py +132 -0
  181. hpcflow/tests/unit/test_cache.py +46 -0
  182. hpcflow/tests/unit/test_cli.py +172 -0
  183. hpcflow/tests/unit/test_command.py +377 -0
  184. hpcflow/tests/unit/test_config.py +195 -0
  185. hpcflow/tests/unit/test_config_file.py +162 -0
  186. hpcflow/tests/unit/test_element.py +666 -0
  187. hpcflow/tests/unit/test_element_iteration.py +88 -0
  188. hpcflow/tests/unit/test_element_set.py +158 -0
  189. hpcflow/tests/unit/test_group.py +115 -0
  190. hpcflow/tests/unit/test_input_source.py +1479 -0
  191. hpcflow/tests/unit/test_input_value.py +398 -0
  192. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  193. hpcflow/tests/unit/test_json_like.py +1247 -0
  194. hpcflow/tests/unit/test_loop.py +2674 -0
  195. hpcflow/tests/unit/test_meta_task.py +325 -0
  196. hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
  197. hpcflow/tests/unit/test_object_list.py +116 -0
  198. hpcflow/tests/unit/test_parameter.py +243 -0
  199. hpcflow/tests/unit/test_persistence.py +664 -0
  200. hpcflow/tests/unit/test_resources.py +243 -0
  201. hpcflow/tests/unit/test_run.py +286 -0
  202. hpcflow/tests/unit/test_run_directories.py +29 -0
  203. hpcflow/tests/unit/test_runtime.py +9 -0
  204. hpcflow/tests/unit/test_schema_input.py +372 -0
  205. hpcflow/tests/unit/test_shell.py +129 -0
  206. hpcflow/tests/unit/test_slurm.py +39 -0
  207. hpcflow/tests/unit/test_submission.py +502 -0
  208. hpcflow/tests/unit/test_task.py +2560 -0
  209. hpcflow/tests/unit/test_task_schema.py +182 -0
  210. hpcflow/tests/unit/test_utils.py +616 -0
  211. hpcflow/tests/unit/test_value_sequence.py +549 -0
  212. hpcflow/tests/unit/test_values.py +91 -0
  213. hpcflow/tests/unit/test_workflow.py +827 -0
  214. hpcflow/tests/unit/test_workflow_template.py +186 -0
  215. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  216. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  217. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  218. hpcflow/tests/unit/utils/test_patches.py +5 -0
  219. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  220. hpcflow/tests/unit/utils/test_strings.py +97 -0
  221. hpcflow/tests/workflows/__init__.py +0 -0
  222. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  223. hpcflow/tests/workflows/test_jobscript.py +355 -0
  224. hpcflow/tests/workflows/test_run_status.py +198 -0
  225. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  226. hpcflow/tests/workflows/test_submission.py +140 -0
  227. hpcflow/tests/workflows/test_workflows.py +564 -0
  228. hpcflow/tests/workflows/test_zip.py +18 -0
  229. hpcflow/viz_demo.ipynb +6794 -0
  230. hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
  231. hpcflow-0.2.0a271.dist-info/METADATA +65 -0
  232. hpcflow-0.2.0a271.dist-info/RECORD +237 -0
  233. {hpcflow-0.1.9.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
  234. hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
  235. hpcflow/api.py +0 -458
  236. hpcflow/archive/archive.py +0 -308
  237. hpcflow/archive/cloud/cloud.py +0 -47
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -432
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -232
  243. hpcflow/copytree.py +0 -66
  244. hpcflow/data/examples/_config.yml +0 -14
  245. hpcflow/data/examples/damask/demo/1.run.yml +0 -4
  246. hpcflow/data/examples/damask/demo/2.process.yml +0 -29
  247. hpcflow/data/examples/damask/demo/geom.geom +0 -2052
  248. hpcflow/data/examples/damask/demo/load.load +0 -1
  249. hpcflow/data/examples/damask/demo/material.config +0 -185
  250. hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
  251. hpcflow/data/examples/damask/inputs/load.load +0 -1
  252. hpcflow/data/examples/damask/inputs/material.config +0 -185
  253. hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
  254. hpcflow/data/examples/damask/profiles/damask.yml +0 -4
  255. hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
  256. hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
  257. hpcflow/data/examples/damask/profiles/default.yml +0 -6
  258. hpcflow/data/examples/thinking.yml +0 -177
  259. hpcflow/errors.py +0 -2
  260. hpcflow/init_db.py +0 -37
  261. hpcflow/models.py +0 -2549
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -323
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -167
  268. hpcflow/variables.py +0 -544
  269. hpcflow-0.1.9.dist-info/METADATA +0 -168
  270. hpcflow-0.1.9.dist-info/RECORD +0 -45
  271. hpcflow-0.1.9.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.9.dist-info/top_level.txt +0 -1
  273. /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
  274. /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
  275. /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
@@ -0,0 +1,424 @@
1
+ """
2
+ Model of a command run in an action.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from dataclasses import dataclass, field
7
+ from functools import partial
8
+ from pathlib import Path
9
+ import re
10
+ from typing import Any, ClassVar, TYPE_CHECKING
11
+
12
+ import numpy as np
13
+
14
+ from hpcflow.sdk.log import TimeIt
15
+ from hpcflow.sdk.typing import hydrate
16
+ from hpcflow.sdk.core.element import ElementResources
17
+ from hpcflow.sdk.core.errors import NoCLIFormatMethodError
18
+ from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
19
+ from hpcflow.sdk.core.parameters import ParameterValue
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable, Iterable, Mapping, Sequence
23
+ from re import Pattern
24
+ from .actions import ActionRule, Action
25
+ from .element import ElementActionRun
26
+ from .environment import Environment
27
+ from ..submission.shells import Shell
28
+
29
+
30
+ @dataclass
31
+ @hydrate
32
+ class Command(JSONLike):
33
+ """
34
+ A command that may be run within a workflow action.
35
+
36
+ Parameters
37
+ ----------
38
+ command: str
39
+ The actual command.
40
+ executable: str
41
+ The executable to run,
42
+ from the set of executable managed by the environment.
43
+ arguments: list[str]
44
+ The arguments to pass in.
45
+ variables: dict[str, str]
46
+ Values that may be substituted when preparing the arguments.
47
+ stdout: str
48
+ The name of a file to write standard output to.
49
+ stderr: str
50
+ The name of a file to write standard error to.
51
+ stdin: str
52
+ The name of a file to read standard input from.
53
+ rules: list[~hpcflow.app.ActionRule]
54
+ Rules that state whether this command is eligible to run.
55
+ """
56
+
57
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
58
+ ChildObjectSpec(
59
+ name="rules",
60
+ class_name="ActionRule",
61
+ is_multiple=True,
62
+ parent_ref="command",
63
+ ),
64
+ )
65
+
66
+ #: The actual command.
67
+ #: Overrides :py:attr:`executable`.
68
+ command: str | None = None
69
+ #: The executable to run,
70
+ #: from the set of executable managed by the environment.
71
+ executable: str | None = None
72
+ #: The arguments to pass in.
73
+ arguments: list[str] | None = None
74
+ #: Values that may be substituted when preparing the arguments.
75
+ variables: dict[str, str] | None = None
76
+ #: The name of a file to write standard output to.
77
+ stdout: str | None = None
78
+ #: The name of a file to write standard error to.
79
+ stderr: str | None = None
80
+ #: The name of a file to read standard input from.
81
+ stdin: str | None = None
82
+ #: Rules that state whether this command is eligible to run.
83
+ rules: list[ActionRule] = field(default_factory=list)
84
+
85
+ action: Action | None = None # assigned by parent Action
86
+
87
+ def __post_init__(self):
88
+ self._set_parent_refs()
89
+
90
+ def __repr__(self) -> str:
91
+ out = []
92
+ if self.command:
93
+ out.append(f"command={self.command!r}")
94
+ if self.executable:
95
+ out.append(f"executable={self.executable!r}")
96
+ if self.arguments:
97
+ out.append(f"arguments={self.arguments!r}")
98
+ if self.variables:
99
+ out.append(f"variables={self.variables!r}")
100
+ if self.stdout:
101
+ out.append(f"stdout={self.stdout!r}")
102
+ if self.stderr:
103
+ out.append(f"stderr={self.stderr!r}")
104
+ if self.stdin:
105
+ out.append(f"stdin={self.stdin!r}")
106
+ if self.rules:
107
+ out.append(f"rules={self.rules!r}")
108
+
109
+ return f"{self.__class__.__name__}({', '.join(out)})"
110
+
111
+ def __eq__(self, other):
112
+ if not isinstance(other, self.__class__):
113
+ return False
114
+ return (
115
+ self.command == other.command
116
+ and self.executable == other.executable
117
+ and self.arguments == other.arguments
118
+ and self.variables == other.variables
119
+ and self.stdout == other.stdout
120
+ and self.stderr == other.stderr
121
+ and self.stdin == other.stdin
122
+ and self.rules == other.rules
123
+ )
124
+
125
+ def __get_initial_command_line(self) -> str:
126
+ if self.command:
127
+ return self.command
128
+ else:
129
+ return self.executable or ""
130
+
131
+ __EXE_SCRIPT_RE: ClassVar[Pattern] = re.compile(r"\<\<(executable|script):(.*?)\>\>")
132
+ __ENV_SPEC_RE: ClassVar[Pattern] = re.compile(r"\<\<env:(.*?)\>\>")
133
+
134
+ def get_command_line(
135
+ self, EAR: ElementActionRun, shell: Shell, env: Environment
136
+ ) -> tuple[str, list[tuple[str, ...]]]:
137
+ """Return the resolved command line.
138
+
139
+ This is ordinarily called at run-time by `Workflow.write_commands`.
140
+ """
141
+
142
+ self._app.persistence_logger.debug("Command.get_command_line")
143
+ cmd_str = self.__get_initial_command_line()
144
+
145
+ def _format_sum(iterable: Iterable) -> str:
146
+ return str(sum(iterable))
147
+
148
+ def _join(iterable: Iterable, delim: str) -> str:
149
+ return delim.join(map(str, iterable))
150
+
151
+ parse_types: dict[str, Callable[..., str]] = {
152
+ "sum": _format_sum,
153
+ "join": _join,
154
+ }
155
+
156
+ def exec_script_repl(match_obj: re.Match[str]) -> str:
157
+ typ, val = match_obj.groups()
158
+ if typ == "executable":
159
+ executable = env.executables.get(val)
160
+ filterable = ElementResources.get_env_instance_filterable_attributes()
161
+ filter_exec = {attr: EAR.get_resources().get(attr) for attr in filterable}
162
+ exec_cmd = executable.filter_instances(**filter_exec)[0].command
163
+ return exec_cmd.replace("<<num_cores>>", str(EAR.resources.num_cores))
164
+ elif typ == "script":
165
+ # TODO: is this needed? we have <<script_name>> <<script_path>> etc as command variables
166
+ return EAR.action.get_script_name(val)
167
+ else:
168
+ raise ValueError("impossible match occurred")
169
+
170
+ def input_param_repl(match_obj: re.Match[str], inp_val) -> str:
171
+ _, func, func_kwargs, method, method_kwargs = match_obj.groups()
172
+
173
+ if isinstance(inp_val, ParameterValue):
174
+ if not method:
175
+ method = "CLI_format"
176
+ if not hasattr(inp_val, method):
177
+ raise NoCLIFormatMethodError(method, inp_val)
178
+ kwargs = self.__prepare_kwargs_from_string(args_str=method_kwargs)
179
+ inp_val = getattr(inp_val, method)(**kwargs)
180
+
181
+ if func:
182
+ kwargs = self.__prepare_kwargs_from_string(
183
+ args_str=func_kwargs,
184
+ doubled_quoted_args=["delim"],
185
+ )
186
+ inp_val = parse_types[func](inp_val, **kwargs)
187
+
188
+ return str(inp_val)
189
+
190
+ file_regex = r"(\<\<file:{}\>\>?)"
191
+
192
+ # substitute executables:
193
+ cmd_str = self.__EXE_SCRIPT_RE.sub(
194
+ repl=exec_script_repl,
195
+ string=cmd_str,
196
+ )
197
+
198
+ # executable command might itself contain variables defined in `variables`, and/or
199
+ # an `<<args>>` variable::
200
+ for var_key, var_val in (self.variables or {}).items():
201
+ # substitute any `<<env:>>` specifiers
202
+ var_val = self.__ENV_SPEC_RE.sub(
203
+ repl=lambda match_obj: EAR.env_spec[match_obj[1]],
204
+ string=var_val,
205
+ )
206
+ cmd_str = cmd_str.replace(f"<<{var_key}>>", var_val)
207
+ if "<<args>>" in cmd_str:
208
+ args_str = " ".join(self.arguments or ())
209
+ ends_in_args = cmd_str.endswith("<<args>>")
210
+ cmd_str = cmd_str.replace("<<args>>", args_str)
211
+ if ends_in_args and not args_str:
212
+ cmd_str = cmd_str.rstrip()
213
+
214
+ # remove any left over "<<args>>" and "<<script_name>>"s:
215
+ cmd_str = (
216
+ cmd_str.replace("<<args>>", "")
217
+ .replace("<<script_name>>", "")
218
+ .replace("<<script_path>>", "")
219
+ )
220
+
221
+ # substitute input parameters in command:
222
+ types_pattern = "|".join(parse_types)
223
+ pattern = (
224
+ r"(\<\<(?:({types_pattern})(?:\[(.*)\])?\()?parameter:{name}(?:\.(\w+)"
225
+ r"\((.*?)\))?\)?\>\>?)"
226
+ )
227
+
228
+ for cmd_inp_full in EAR.action.get_command_input_types(sub_parameters=True):
229
+ # remove any CLI formatting method, which will be the final component and will
230
+ # include parentheses:
231
+ cmd_inp_parts = cmd_inp_full.split(".")
232
+ if "(" in cmd_inp_parts[-1]:
233
+ cmd_inp = ".".join(cmd_inp_parts[:-1])
234
+ else:
235
+ cmd_inp = cmd_inp_full
236
+ inp_val = EAR.get(
237
+ f"inputs.{cmd_inp}",
238
+ raise_on_unset=True,
239
+ ) # TODO: what if schema output?
240
+ pattern_i = pattern.format(
241
+ types_pattern=types_pattern,
242
+ name=re.escape(cmd_inp),
243
+ )
244
+ cmd_str = re.sub(
245
+ pattern=pattern_i,
246
+ repl=partial(input_param_repl, inp_val=inp_val),
247
+ string=cmd_str,
248
+ )
249
+
250
+ # substitute input/output files in command:
251
+ for cmd_file in EAR.action.get_command_file_labels():
252
+ file_path = EAR.get(
253
+ f"input_files.{cmd_file}", raise_on_unset=True
254
+ ) or EAR.get(f"output_files.{cmd_file}", raise_on_unset=True)
255
+ # assuming we have copied this file to the EAR directory, then we just
256
+ # need the file name:
257
+ file_name = Path(file_path).name
258
+ cmd_str = re.sub(
259
+ pattern=file_regex.format(cmd_file),
260
+ repl=file_name,
261
+ string=cmd_str,
262
+ )
263
+
264
+ shell_vars: list[tuple[str, ...]] = []
265
+ out_types = self.get_output_types()
266
+ if out_types["stdout"]:
267
+ # TODO: also map stderr/both if possible
268
+ # assign stdout to a shell variable if required:
269
+ param_name = f"outputs.{out_types['stdout']}"
270
+ shell_var_name = f"parameter_{out_types['stdout']}"
271
+ shell_vars.append((param_name, shell_var_name, "stdout"))
272
+ cmd_str = shell.format_stream_assignment(
273
+ shell_var_name=shell_var_name,
274
+ command=cmd_str,
275
+ )
276
+ elif self.stdout:
277
+ cmd_str += f" 1>> {self.stdout}"
278
+
279
+ if self.stderr:
280
+ cmd_str += f" 2>> {self.stderr}"
281
+
282
+ return cmd_str, shell_vars
283
+
284
+ # note: we use "parameter" rather than "output", because it could be a schema
285
+ # output or schema input.
286
+ __PARAM_RE: ClassVar[Pattern] = re.compile(
287
+ r"(?:\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(\w+)"
288
+ r"(?:\.(?:\w+)\((?:.*?)\))?\)?\>\>?)"
289
+ )
290
+
291
+ def get_output_types(self) -> Mapping[str, str | None]:
292
+ """
293
+ Get whether stdout and stderr are workflow parameters.
294
+ """
295
+ out: dict[str, str | None] = {"stdout": None, "stderr": None}
296
+ for i, label in zip((self.stdout, self.stderr), ("stdout", "stderr")):
297
+ if i and (match := self.__PARAM_RE.search(i)):
298
+ param_typ: str = match[1]
299
+ if match.span(0) != (0, len(i)):
300
+ raise ValueError(
301
+ f"If specified as a parameter, `{label}` must not include"
302
+ f" any characters other than the parameter "
303
+ f"specification, but this was given: {i!r}."
304
+ )
305
+ out[label] = param_typ
306
+ return out
307
+
308
+ @staticmethod
309
+ def __prepare_kwargs_from_string(
310
+ args_str: str | None, doubled_quoted_args: list[str] | None = None
311
+ ) -> dict[str, str]:
312
+ if args_str is None:
313
+ return {}
314
+
315
+ kwargs: dict[str, str] = {}
316
+ # deal with specified double-quoted arguments first if it exists:
317
+ for quote_arg in doubled_quoted_args or ():
318
+ quote_pat = r'.*({quote_arg}="(.*)").*'.format(quote_arg=quote_arg)
319
+ if match := re.match(quote_pat, args_str):
320
+ quote_str, quote_contents = match.groups()
321
+ args_str = args_str.replace(quote_str, "")
322
+ kwargs[quote_arg] = quote_contents
323
+
324
+ if args_str := args_str.strip().strip(","):
325
+ for arg_part in args_str.split(","):
326
+ name_i, value_i = map(str.strip, arg_part.split("="))
327
+ kwargs[name_i] = value_i
328
+ return kwargs
329
+
330
+ def process_std_stream(self, name: str, value: str, stderr: bool) -> Any:
331
+ """
332
+ Process a description of a standard stread from a command to get how it becomes
333
+ a workflow parameter for later actions.
334
+
335
+ Parameters
336
+ ---------
337
+ name:
338
+ The name of the output, describing how to process things.
339
+ value:
340
+ The actual value read from the stream.
341
+ stderr:
342
+ If true, this is handling the stderr stream. If false, the stdout stream.
343
+ """
344
+
345
+ def _parse_list(
346
+ lst_str: str, item_type: str = "str", delim: str = " "
347
+ ) -> list[Any]:
348
+ return [parse_types[item_type](i) for i in lst_str.split(delim)]
349
+
350
+ def _parse_array(
351
+ arr_str: str, item_type: str = "float", delim: str = " "
352
+ ) -> np.ndarray[Any, np.dtype[Any]]:
353
+ return np.array(
354
+ _parse_list(lst_str=arr_str, item_type=item_type, delim=delim)
355
+ )
356
+
357
+ def _parse_bool(bool_str: str) -> bool:
358
+ bool_str = bool_str.lower()
359
+ if bool_str in ("true", "1"):
360
+ return True
361
+ elif bool_str in ("false", "0"):
362
+ return False
363
+ else:
364
+ raise ValueError(
365
+ f"Cannot parse value {bool_str!r} as a boolean in command "
366
+ f"{'stderr' if stderr else 'stdout'}: "
367
+ f"{self.stderr if stderr else self.stdout!r}."
368
+ )
369
+
370
+ parse_types: dict[str, Callable[[str], Any]] = {
371
+ "str": str,
372
+ "int": int,
373
+ "float": float,
374
+ "bool": _parse_bool,
375
+ "list": _parse_list,
376
+ "array": _parse_array,
377
+ }
378
+ types_pattern = "|".join(parse_types)
379
+
380
+ # TODO: use str.removeprefix in 3.9 onwards
381
+ out_name = name.replace("outputs.", "")
382
+ pattern = (
383
+ r"(\<\<(?:({types_pattern})(?:\[(.*)\])?\()?parameter:{name}(?:\.(\w+)"
384
+ r"\((.*?)\))?\)?\>\>?)"
385
+ )
386
+ pattern = pattern.format(types_pattern=types_pattern, name=out_name)
387
+ spec = self.stderr if stderr else self.stdout
388
+ assert spec is not None
389
+ self._app.submission_logger.info(
390
+ f"processing shell standard stream according to spec: {spec!r}"
391
+ )
392
+ param = self._app.Parameter(out_name)
393
+ if (match := re.match(pattern, spec)) is None:
394
+ return value
395
+ groups = match.groups()
396
+ parse_type, parse_args_str = groups[1:3]
397
+ parse_args = self.__prepare_kwargs_from_string(
398
+ args_str=parse_args_str,
399
+ doubled_quoted_args=["delim"],
400
+ )
401
+ if param._value_class:
402
+ method, method_args_str = groups[3:5]
403
+ method_args = self.__prepare_kwargs_from_string(
404
+ args_str=method_args_str,
405
+ doubled_quoted_args=["delim"],
406
+ )
407
+ method = method or "CLI_parse"
408
+ value = getattr(param._value_class, method)(value, **method_args)
409
+ if parse_type:
410
+ value = parse_types[parse_type](value, **parse_args)
411
+
412
+ return value
413
+
414
+ __EXE_RE: ClassVar[Pattern] = re.compile(r"\<\<(?:executable):(.*?)\>\>")
415
+
416
+ @classmethod
417
+ def _extract_executable_labels(cls, cmd_str: str) -> Sequence[str]:
418
+ return cls.__EXE_RE.findall(cmd_str)
419
+
420
+ @TimeIt.decorator
421
+ def get_required_executables(self) -> Sequence[str]:
422
+ """Return executable labels required by this command."""
423
+ # an executable label might appear in the `command` or `executable` attribute:
424
+ return self._extract_executable_labels(self.__get_initial_command_line())