hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a199__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +9 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/bad_script.py +2 -0
  5. hpcflow/data/scripts/do_nothing.py +2 -0
  6. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  7. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  8. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  11. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  12. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  13. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  15. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  16. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  23. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  24. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  25. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  26. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  27. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  28. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  29. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  30. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  31. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  32. hpcflow/data/scripts/script_exit_test.py +5 -0
  33. hpcflow/data/template_components/environments.yaml +1 -1
  34. hpcflow/sdk/__init__.py +26 -15
  35. hpcflow/sdk/app.py +2192 -768
  36. hpcflow/sdk/cli.py +506 -296
  37. hpcflow/sdk/cli_common.py +105 -7
  38. hpcflow/sdk/config/__init__.py +1 -1
  39. hpcflow/sdk/config/callbacks.py +115 -43
  40. hpcflow/sdk/config/cli.py +126 -103
  41. hpcflow/sdk/config/config.py +674 -318
  42. hpcflow/sdk/config/config_file.py +131 -95
  43. hpcflow/sdk/config/errors.py +125 -84
  44. hpcflow/sdk/config/types.py +148 -0
  45. hpcflow/sdk/core/__init__.py +25 -1
  46. hpcflow/sdk/core/actions.py +1771 -1059
  47. hpcflow/sdk/core/app_aware.py +24 -0
  48. hpcflow/sdk/core/cache.py +139 -79
  49. hpcflow/sdk/core/command_files.py +263 -287
  50. hpcflow/sdk/core/commands.py +145 -112
  51. hpcflow/sdk/core/element.py +828 -535
  52. hpcflow/sdk/core/enums.py +192 -0
  53. hpcflow/sdk/core/environment.py +74 -93
  54. hpcflow/sdk/core/errors.py +455 -52
  55. hpcflow/sdk/core/execute.py +207 -0
  56. hpcflow/sdk/core/json_like.py +540 -272
  57. hpcflow/sdk/core/loop.py +751 -347
  58. hpcflow/sdk/core/loop_cache.py +164 -47
  59. hpcflow/sdk/core/object_list.py +370 -207
  60. hpcflow/sdk/core/parameters.py +1100 -627
  61. hpcflow/sdk/core/rule.py +59 -41
  62. hpcflow/sdk/core/run_dir_files.py +21 -37
  63. hpcflow/sdk/core/skip_reason.py +7 -0
  64. hpcflow/sdk/core/task.py +1649 -1339
  65. hpcflow/sdk/core/task_schema.py +308 -196
  66. hpcflow/sdk/core/test_utils.py +191 -114
  67. hpcflow/sdk/core/types.py +440 -0
  68. hpcflow/sdk/core/utils.py +485 -309
  69. hpcflow/sdk/core/validation.py +82 -9
  70. hpcflow/sdk/core/workflow.py +2544 -1178
  71. hpcflow/sdk/core/zarr_io.py +98 -137
  72. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  73. hpcflow/sdk/demo/cli.py +53 -33
  74. hpcflow/sdk/helper/cli.py +18 -15
  75. hpcflow/sdk/helper/helper.py +75 -63
  76. hpcflow/sdk/helper/watcher.py +61 -28
  77. hpcflow/sdk/log.py +122 -71
  78. hpcflow/sdk/persistence/__init__.py +8 -31
  79. hpcflow/sdk/persistence/base.py +1360 -606
  80. hpcflow/sdk/persistence/defaults.py +6 -0
  81. hpcflow/sdk/persistence/discovery.py +38 -0
  82. hpcflow/sdk/persistence/json.py +568 -188
  83. hpcflow/sdk/persistence/pending.py +382 -179
  84. hpcflow/sdk/persistence/store_resource.py +39 -23
  85. hpcflow/sdk/persistence/types.py +318 -0
  86. hpcflow/sdk/persistence/utils.py +14 -11
  87. hpcflow/sdk/persistence/zarr.py +1337 -433
  88. hpcflow/sdk/runtime.py +44 -41
  89. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  90. hpcflow/sdk/submission/jobscript.py +1651 -692
  91. hpcflow/sdk/submission/schedulers/__init__.py +167 -39
  92. hpcflow/sdk/submission/schedulers/direct.py +121 -81
  93. hpcflow/sdk/submission/schedulers/sge.py +170 -129
  94. hpcflow/sdk/submission/schedulers/slurm.py +291 -268
  95. hpcflow/sdk/submission/schedulers/utils.py +12 -2
  96. hpcflow/sdk/submission/shells/__init__.py +14 -15
  97. hpcflow/sdk/submission/shells/base.py +150 -29
  98. hpcflow/sdk/submission/shells/bash.py +283 -173
  99. hpcflow/sdk/submission/shells/os_version.py +31 -30
  100. hpcflow/sdk/submission/shells/powershell.py +228 -170
  101. hpcflow/sdk/submission/submission.py +1014 -335
  102. hpcflow/sdk/submission/types.py +140 -0
  103. hpcflow/sdk/typing.py +182 -12
  104. hpcflow/sdk/utils/arrays.py +71 -0
  105. hpcflow/sdk/utils/deferred_file.py +55 -0
  106. hpcflow/sdk/utils/hashing.py +16 -0
  107. hpcflow/sdk/utils/patches.py +12 -0
  108. hpcflow/sdk/utils/strings.py +33 -0
  109. hpcflow/tests/api/test_api.py +32 -0
  110. hpcflow/tests/conftest.py +27 -6
  111. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  112. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  113. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  114. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  115. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  116. hpcflow/tests/scripts/test_main_scripts.py +866 -85
  117. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  118. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  119. hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
  120. hpcflow/tests/unit/test_action.py +262 -75
  121. hpcflow/tests/unit/test_action_rule.py +9 -4
  122. hpcflow/tests/unit/test_app.py +33 -6
  123. hpcflow/tests/unit/test_cache.py +46 -0
  124. hpcflow/tests/unit/test_cli.py +134 -1
  125. hpcflow/tests/unit/test_command.py +71 -54
  126. hpcflow/tests/unit/test_config.py +142 -16
  127. hpcflow/tests/unit/test_config_file.py +21 -18
  128. hpcflow/tests/unit/test_element.py +58 -62
  129. hpcflow/tests/unit/test_element_iteration.py +50 -1
  130. hpcflow/tests/unit/test_element_set.py +29 -19
  131. hpcflow/tests/unit/test_group.py +4 -2
  132. hpcflow/tests/unit/test_input_source.py +116 -93
  133. hpcflow/tests/unit/test_input_value.py +29 -24
  134. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  135. hpcflow/tests/unit/test_json_like.py +44 -35
  136. hpcflow/tests/unit/test_loop.py +1396 -84
  137. hpcflow/tests/unit/test_meta_task.py +325 -0
  138. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  139. hpcflow/tests/unit/test_object_list.py +17 -12
  140. hpcflow/tests/unit/test_parameter.py +29 -7
  141. hpcflow/tests/unit/test_persistence.py +237 -42
  142. hpcflow/tests/unit/test_resources.py +20 -18
  143. hpcflow/tests/unit/test_run.py +117 -6
  144. hpcflow/tests/unit/test_run_directories.py +29 -0
  145. hpcflow/tests/unit/test_runtime.py +2 -1
  146. hpcflow/tests/unit/test_schema_input.py +23 -15
  147. hpcflow/tests/unit/test_shell.py +23 -2
  148. hpcflow/tests/unit/test_slurm.py +8 -7
  149. hpcflow/tests/unit/test_submission.py +38 -89
  150. hpcflow/tests/unit/test_task.py +352 -247
  151. hpcflow/tests/unit/test_task_schema.py +33 -20
  152. hpcflow/tests/unit/test_utils.py +9 -11
  153. hpcflow/tests/unit/test_value_sequence.py +15 -12
  154. hpcflow/tests/unit/test_workflow.py +114 -83
  155. hpcflow/tests/unit/test_workflow_template.py +0 -1
  156. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  157. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  158. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  159. hpcflow/tests/unit/utils/test_patches.py +5 -0
  160. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  161. hpcflow/tests/workflows/__init__.py +0 -0
  162. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  163. hpcflow/tests/workflows/test_jobscript.py +334 -1
  164. hpcflow/tests/workflows/test_run_status.py +198 -0
  165. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  166. hpcflow/tests/workflows/test_submission.py +140 -0
  167. hpcflow/tests/workflows/test_workflows.py +160 -15
  168. hpcflow/tests/workflows/test_zip.py +18 -0
  169. hpcflow/viz_demo.ipynb +6587 -3
  170. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
  171. hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
  172. hpcflow/sdk/core/parallel.py +0 -21
  173. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  174. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
  175. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
  176. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
@@ -2,16 +2,17 @@
2
2
  Operating system information discovery helpers.
3
3
  """
4
4
 
5
- import os
5
+ from __future__ import annotations
6
+ from collections.abc import Mapping
6
7
  import platform
7
8
  import re
8
9
  import subprocess
9
- from typing import Dict, List, Optional
10
+ from typing import Final
10
11
 
11
- DEFAULT_LINUX_RELEASE_FILE = "/etc/os-release"
12
+ _DEFAULT_LINUX_RELEASE_FILE: Final = "/etc/os-release"
12
13
 
13
14
 
14
- def get_OS_info() -> Dict:
15
+ def get_OS_info() -> Mapping[str, str]:
15
16
  """
16
17
  Get basic operating system version info.
17
18
  """
@@ -23,7 +24,7 @@ def get_OS_info() -> Dict:
23
24
  }
24
25
 
25
26
 
26
- def get_OS_info_windows() -> Dict:
27
+ def get_OS_info_windows() -> Mapping[str, str]:
27
28
  """
28
29
  Get operating system version info: Windows version.
29
30
  """
@@ -31,10 +32,10 @@ def get_OS_info_windows() -> Dict:
31
32
 
32
33
 
33
34
  def get_OS_info_POSIX(
34
- WSL_executable: Optional[List[str]] = None,
35
- use_py: Optional[bool] = True,
36
- linux_release_file: Optional[str] = None,
37
- ) -> Dict:
35
+ WSL_executable: list[str] | None = None,
36
+ use_py: bool = True,
37
+ linux_release_file: str | None = None,
38
+ ) -> Mapping[str, str]:
38
39
  """
39
40
  Get operating system version info: POSIX version.
40
41
 
@@ -48,11 +49,11 @@ def get_OS_info_POSIX(
48
49
  when getting OS info in WSL on Windows, since we need to call the WSL executable.
49
50
  linux_release_file:
50
51
  If on Linux, record the name and version fields from this file.
51
-
52
52
  """
53
53
 
54
- def try_subprocess_call(command):
54
+ def try_subprocess_call(*args: str) -> str:
55
55
  exc = None
56
+ command = [*WSL_exe, *args]
56
57
  try:
57
58
  proc = subprocess.run(
58
59
  args=command,
@@ -63,23 +64,21 @@ def get_OS_info_POSIX(
63
64
  except Exception as err:
64
65
  exc = err
65
66
 
66
- if proc.returncode != 0 or exc:
67
+ if proc.returncode or exc:
67
68
  raise RuntimeError(
68
69
  f"Failed to get POSIX OS info. Command was: {command!r}. Subprocess "
69
70
  f"exception was: {exc!r}. Stderr was: {proc.stderr!r}."
70
71
  )
71
- else:
72
- return proc.stdout
72
+ return proc.stdout
73
73
 
74
- WSL_executable = WSL_executable or []
75
- out = {}
74
+ WSL_exe = WSL_executable or []
75
+ out: dict[str, str] = {}
76
76
  if use_py:
77
77
  out.update(**get_OS_info())
78
-
79
78
  else:
80
- OS_name = try_subprocess_call(WSL_executable + ["uname", "-s"]).strip()
81
- OS_release = try_subprocess_call(WSL_executable + ["uname", "-r"]).strip()
82
- OS_version = try_subprocess_call(WSL_executable + ["uname", "-v"]).strip()
79
+ OS_name = try_subprocess_call("uname", "-s").strip()
80
+ OS_release = try_subprocess_call("uname", "-r").strip()
81
+ OS_version = try_subprocess_call("uname", "-v").strip()
83
82
 
84
83
  out["OS_name"] = OS_name
85
84
  out["OS_release"] = OS_release
@@ -87,28 +86,30 @@ def get_OS_info_POSIX(
87
86
 
88
87
  if out["OS_name"] == "Linux":
89
88
  # get linux distribution name and version:
90
- linux_release_file = linux_release_file or DEFAULT_LINUX_RELEASE_FILE
91
- release_out = try_subprocess_call(WSL_executable + ["cat", linux_release_file])
89
+ linux_release_file = linux_release_file or _DEFAULT_LINUX_RELEASE_FILE
90
+ release_out = try_subprocess_call("cat", linux_release_file)
92
91
 
93
- name_match = re.search(r"^NAME=\"(.*)\"", release_out, flags=re.MULTILINE)
94
- if name_match:
95
- lin_name = name_match.group(1)
96
- else:
92
+ name_match = _NAME_RE.search(release_out)
93
+ if not name_match:
97
94
  raise RuntimeError(
98
95
  f"Failed to get Linux distribution name from file `{linux_release_file}`."
99
96
  )
97
+ lin_name: str = name_match[1]
100
98
 
101
- version_match = re.search(r"^VERSION=\"(.*)\"", release_out, flags=re.MULTILINE)
102
- if version_match:
103
- lin_version = version_match.group(1)
104
- else:
99
+ version_match = _VERSION_RE.search(release_out)
100
+ if not version_match:
105
101
  raise RuntimeError(
106
102
  f"Failed to get Linux distribution version from file "
107
103
  f"`{linux_release_file}`."
108
104
  )
105
+ lin_version: str = version_match[1]
109
106
 
110
107
  out["linux_release_file"] = linux_release_file
111
108
  out["linux_distribution_name"] = lin_name
112
109
  out["linux_distribution_version"] = lin_version
113
110
 
114
111
  return out
112
+
113
+
114
+ _NAME_RE: Final = re.compile(r"^NAME=\"(.*)\"", flags=re.MULTILINE)
115
+ _VERSION_RE: Final = re.compile(r"^VERSION=\"(.*)\"", flags=re.MULTILINE)
@@ -2,37 +2,44 @@
2
2
  Shell models based on Microsoft PowerShell.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  import subprocess
6
- from textwrap import dedent, indent
7
- from typing import Dict, List, Optional
8
- from hpcflow.sdk.core import ABORT_EXIT_CODE
9
- from hpcflow.sdk.submission.shells import Shell
7
+ from textwrap import dedent
8
+ from typing import TYPE_CHECKING
9
+ from typing_extensions import override
10
+ from hpcflow.sdk.typing import hydrate
11
+ from hpcflow.sdk.submission.shells.base import Shell
10
12
  from hpcflow.sdk.submission.shells.os_version import get_OS_info_windows
11
13
 
14
+ if TYPE_CHECKING:
15
+ from typing import ClassVar
16
+ from .base import VersionInfo
12
17
 
18
+
19
+ @hydrate
13
20
  class WindowsPowerShell(Shell):
14
21
  """Class to represent using PowerShell on Windows to generate and submit a jobscript."""
15
22
 
16
23
  # TODO: add snippets that can be used in demo task schemas?
17
24
 
18
25
  #: Default for executable name.
19
- DEFAULT_EXE = "powershell.exe"
26
+ DEFAULT_EXE: ClassVar[str] = "powershell.exe"
20
27
 
21
28
  #: File extension for jobscripts.
22
- JS_EXT = ".ps1"
29
+ JS_EXT: ClassVar[str] = ".ps1"
23
30
  #: Basic indent.
24
- JS_INDENT = " "
31
+ JS_INDENT: ClassVar[str] = " "
25
32
  #: Indent for environment setup.
26
- JS_ENV_SETUP_INDENT = 2 * JS_INDENT
33
+ JS_ENV_SETUP_INDENT: ClassVar[str] = 2 * JS_INDENT
27
34
  #: Template for the jobscript shebang line.
28
- JS_SHEBANG = ""
29
- #: Template for the common part of the jobscript header.
30
- JS_HEADER = dedent(
35
+ JS_SHEBANG: ClassVar[str] = ""
36
+ #: Template for the jobscript functions file.
37
+ JS_FUNCS: ClassVar[str] = dedent(
31
38
  """\
32
39
  function {workflow_app_alias} {{
33
40
  & {{
34
41
  {env_setup}{app_invoc} `
35
- --with-config log_file_path "$pwd/{run_log_file}" `
42
+ --with-config log_file_path "$env:{app_caps}_LOG_PATH" `
36
43
  --config-dir "{config_dir}" `
37
44
  --config-key "{config_invoc_key}" `
38
45
  $args
@@ -42,6 +49,12 @@ class WindowsPowerShell(Shell):
42
49
  function get_nth_line($file, $line) {{
43
50
  Get-Content $file | Select-Object -Skip $line -First 1
44
51
  }}
52
+ """
53
+ )
54
+ #: Template for the common part of the jobscript header.
55
+ JS_HEADER: ClassVar[str] = dedent(
56
+ """\
57
+ $ErrorActionPreference = 'Stop'
45
58
 
46
59
  function JoinMultiPath {{
47
60
  $numArgs = $args.Length
@@ -50,103 +63,156 @@ class WindowsPowerShell(Shell):
50
63
  $path = Join-Path $path $args[$i]
51
64
  }}
52
65
  return $path
53
- }}
54
-
55
- function StartJobHere($block) {{
56
- $jobInitBlock = [scriptblock]::Create(@"
57
- Function wkflow_app {{ $function:wkflow_app }}
58
- Function get_nth_line {{ $function:get_nth_line }}
59
- Function JoinMultiPath {{ $function:JoinMultiPath }}
60
- Set-Location '$pwd'
61
- "@)
62
- Start-Job -InitializationScript $jobInitBlock -Script $block
63
- }}
66
+ }}
64
67
 
65
68
  $WK_PATH = $(Get-Location)
66
69
  $WK_PATH_ARG = $WK_PATH
67
70
  $SUB_IDX = {sub_idx}
68
71
  $JS_IDX = {js_idx}
69
- $EAR_ID_FILE = JoinMultiPath $WK_PATH artifacts submissions $SUB_IDX {EAR_file_name}
70
- $ELEM_RUN_DIR_FILE = JoinMultiPath $WK_PATH artifacts submissions $SUB_IDX {element_run_dirs_file_path}
72
+
73
+ $SUB_DIR = JoinMultiPath $WK_PATH artifacts submissions $SUB_IDX
74
+ $JS_FUNCS_PATH = JoinMultiPath $SUB_DIR {jobscript_functions_dir} {jobscript_functions_name}
75
+ . $JS_FUNCS_PATH
76
+
77
+ $EAR_ID_FILE = JoinMultiPath $SUB_DIR {run_IDs_file_dir} {run_IDs_file_name}
78
+ $SUB_TMP_DIR = Join-Path $SUB_DIR {tmp_dir_name}
79
+ $SUB_LOG_DIR = Join-Path $SUB_DIR {log_dir_name}
80
+ $SUB_STD_DIR = Join-Path $SUB_DIR {app_std_dir_name}
81
+ $SUB_SCRIPTS_DIR = Join-Path $SUB_DIR {scripts_dir_name}
82
+
83
+ $env:{app_caps}_WK_PATH = $WK_PATH
84
+ $env:{app_caps}_WK_PATH_ARG = $WK_PATH_ARG
85
+ $env:{app_caps}_SUB_IDX = {sub_idx}
86
+ $env:{app_caps}_SUB_SCRIPTS_DIR = $SUB_SCRIPTS_DIR
87
+ $env:{app_caps}_SUB_TMP_DIR = $SUB_TMP_DIR
88
+ $env:{app_caps}_SUB_LOG_DIR = $SUB_LOG_DIR
89
+ $env:{app_caps}_SUB_STD_DIR = $SUB_STD_DIR
90
+ $env:{app_caps}_LOG_PATH = Join-Path $SUB_LOG_DIR "js_$JS_IDX.log"
91
+ $env:{app_caps}_JS_FUNCS_PATH = $JS_FUNCS_PATH
92
+ $env:{app_caps}_JS_IDX = {js_idx}
93
+ $env:{app_caps}_RUN_ID_FILE = $EAR_ID_FILE
71
94
  """
72
95
  )
73
96
  #: Template for the jobscript header when directly executed.
74
- JS_DIRECT_HEADER = dedent(
97
+ JS_DIRECT_HEADER: ClassVar[str] = dedent(
75
98
  """\
76
99
  {shebang}
77
-
78
100
  {header}
79
101
  {wait_command}
80
102
  """
81
103
  )
82
- #: Template for the jobscript body.
83
- JS_MAIN = dedent(
104
+ #: Template for enabling writing of the app log.
105
+ JS_RUN_LOG_PATH_ENABLE: ClassVar[str] = 'Join-Path $SUB_LOG_DIR "{run_log_file_name}"'
106
+ #: Template for disabling writing of the app log.
107
+ JS_RUN_LOG_PATH_DISABLE: ClassVar[str] = '" "'
108
+ #: Template for the run execution command.
109
+ JS_RUN_CMD: ClassVar[str] = (
110
+ "{workflow_app_alias} internal workflow $WK_PATH execute-run "
111
+ "$SUB_IDX $JS_IDX $block_idx $block_act_idx $EAR_ID\n"
112
+ )
113
+ #: Template for the execution command for multiple combined runs.
114
+ JS_RUN_CMD_COMBINED: ClassVar[str] = (
115
+ "{workflow_app_alias} internal workflow $WK_PATH execute-combined-runs "
116
+ "$SUB_IDX $JS_IDX\n"
117
+ )
118
+ #: Template for setting up run environment variables and executing the run.
119
+ JS_RUN: ClassVar[str] = dedent(
84
120
  """\
85
- $elem_EAR_IDs = get_nth_line $EAR_ID_FILE $JS_elem_idx
86
- $elem_run_dirs = get_nth_line $ELEM_RUN_DIR_FILE $JS_elem_idx
87
-
88
- for ($JS_act_idx = 0; $JS_act_idx -lt {num_actions}; $JS_act_idx += 1) {{
89
-
90
- $EAR_ID = ($elem_EAR_IDs -split "{EAR_files_delimiter}")[$JS_act_idx]
91
- if ($EAR_ID -eq -1) {{
92
- continue
93
- }}
121
+ $EAR_ID = ($elem_EAR_IDs -split "{EAR_files_delimiter}")[$block_act_idx]
122
+ if ($EAR_ID -eq -1) {{
123
+ continue
124
+ }}
94
125
 
95
- $run_dir = ($elem_run_dirs -split "{EAR_files_delimiter}")[$JS_act_idx]
96
- $run_dir_abs = "$WK_PATH\\$run_dir"
97
- Set-Location $run_dir_abs
98
- $app_stream_file = "$pwd/{run_stream_file}"
99
-
100
- $skip = {workflow_app_alias} internal workflow $WK_PATH get-ear-skipped $EAR_ID 2>> $app_stream_file
101
- $exc_sk = $LASTEXITCODE
102
-
103
- if ($exc_sk -eq 0) {{
104
-
105
- if ($skip -eq "1") {{
106
- continue
107
- }}
108
-
109
- {workflow_app_alias} internal workflow $WK_PATH write-commands $SUB_IDX $JS_IDX $JS_act_idx $EAR_ID 2>&1 >> $app_stream_file
110
- $exc_wc = $LASTEXITCODE
111
-
112
- {workflow_app_alias} internal workflow $WK_PATH set-ear-start $EAR_ID 2>&1 >> $app_stream_file
113
- $exc_se = $LASTEXITCODE
114
-
115
- if (($exc_wc -eq 0) -and ($exc_se -eq 0)) {{
116
- . (Join-Path $run_dir_abs "{commands_file_name}")
117
- $exit_code = $LASTEXITCODE
118
- }}
119
- else {{
120
- $exit_code = If ($exc_wc -ne 0) {{$exc_wc}} Else {{$exc_se}}
121
- }}
122
- }}
123
- else {{
124
- $exit_code = $exc_sk
125
- }}
126
- $global:LASTEXITCODE = $null
127
- {workflow_app_alias} internal workflow $WK_PATH set-ear-end $JS_IDX $JS_act_idx $EAR_ID "--" "$exit_code" 2>&1 >> $app_stream_file
126
+ $env:{app_caps}_RUN_ID = $EAR_ID
127
+ $env:{app_caps}_RUN_LOG_PATH = {run_log_enable_disable}
128
+ $env:{app_caps}_LOG_PATH = $env:{app_caps}_RUN_LOG_PATH
129
+ $env:{app_caps}_RUN_STD_PATH = Join-Path $SUB_STD_DIR "$env:{app_caps}_RUN_ID.txt"
130
+ $env:{app_caps}_BLOCK_ACT_IDX = $block_act_idx
128
131
 
132
+ Set-Location $SUB_TMP_DIR
133
+
134
+ {run_cmd}
135
+ """
136
+ )
137
+ #: Template for the action-run processing loop in a jobscript.
138
+ JS_ACT_MULTI: ClassVar[str] = dedent(
139
+ """\
140
+ for ($block_act_idx = 0; $block_act_idx -lt {num_actions}; $block_act_idx += 1) {{
141
+ {run_block}
129
142
  }}
143
+ """
144
+ )
145
+ #: Template for the single-action-run execution in a jobscript.
146
+ JS_ACT_SINGLE: ClassVar[str] = dedent(
147
+ """\
148
+ $block_act_idx = 0
149
+ {run_block}
150
+ """
151
+ )
152
+ #: Template for setting up environment variables and running one or more action-runs.
153
+ JS_MAIN: ClassVar[str] = dedent(
154
+ """\
155
+ $block_elem_idx = ($JS_elem_idx - {block_start_elem_idx})
156
+ $elem_EAR_IDs = get_nth_line $EAR_ID_FILE $JS_elem_idx
157
+ $env:{app_caps}_JS_ELEM_IDX = $JS_elem_idx
158
+ $env:{app_caps}_BLOCK_ELEM_IDX = $block_elem_idx
159
+
160
+ {action}
161
+ """
162
+ )
163
+ #: Template for a jobscript-block header.
164
+ JS_BLOCK_HEADER: ClassVar[str] = dedent( # for single-block jobscripts only
165
+ """\
166
+ $block_idx = 0
167
+ $env:{app_caps}_BLOCK_IDX = 0
168
+ """
169
+ )
170
+ #: Template for single-element execution.
171
+ JS_ELEMENT_SINGLE: ClassVar[str] = dedent(
172
+ """\
173
+ $JS_elem_idx = {block_start_elem_idx}
174
+ {main}
130
175
  """
131
176
  )
132
177
  #: Template for the element processing loop in a jobscript.
133
- JS_ELEMENT_LOOP = dedent(
178
+ JS_ELEMENT_MULTI_LOOP: ClassVar[str] = dedent(
134
179
  """\
135
- for ($JS_elem_idx = 0; $JS_elem_idx -lt {num_elements}; $JS_elem_idx += 1) {{
180
+ for ($JS_elem_idx = {block_start_elem_idx}; $JS_elem_idx -lt ({block_start_elem_idx} + {num_elements}); $JS_elem_idx += 1) {{
136
181
  {main}
137
182
  }}
183
+ """
184
+ )
185
+ #: Template for the jobscript block loop in a jobscript.
186
+ JS_BLOCK_LOOP: ClassVar[str] = dedent(
187
+ """\
188
+ $num_elements = {num_elements}
189
+ $num_actions = {num_actions}
190
+ $block_start_elem_idx = 0
191
+ for ($block_idx = 0; $block_idx -lt {num_blocks}; $block_idx += 1 ) {{
192
+ $env:{app_caps}_BLOCK_IDX = $block_idx
193
+ {element_loop}
194
+ $block_start_elem_idx += $num_elements[$block_idx]
195
+ }}
196
+ """
197
+ )
198
+ #: Template for the jobscript footer.
199
+ JS_FOOTER: ClassVar[str] = dedent(
200
+ """\
138
201
  Set-Location $WK_PATH
139
202
  """
140
203
  )
141
204
 
142
- def __init__(self, *args, **kwargs):
143
- super().__init__(*args, **kwargs)
144
-
145
- def get_direct_submit_command(self, js_path) -> List[str]:
205
+ def get_direct_submit_command(self, js_path: str) -> list[str]:
146
206
  """Get the command for submitting a non-scheduled jobscript."""
147
- return self.executable + ["-File", js_path]
207
+ return [*self.executable, "-File", js_path]
148
208
 
149
- def get_version_info(self, exclude_os: Optional[bool] = False) -> Dict:
209
+ def get_command_file_launch_command(self, cmd_file_path: str) -> list[str]:
210
+ """Get the command for launching the commands file for a given run."""
211
+ # note the "-File" argument is required for the correct exit code to be recorded.
212
+ return [*self.executable, "-File", cmd_file_path]
213
+
214
+ @override
215
+ def get_version_info(self, exclude_os: bool = False) -> VersionInfo:
150
216
  """Get powershell version information.
151
217
 
152
218
  Parameters
@@ -157,7 +223,7 @@ class WindowsPowerShell(Shell):
157
223
  """
158
224
 
159
225
  proc = subprocess.run(
160
- args=self.executable + ["$PSVersionTable.PSVersion.ToString()"],
226
+ args=self.executable + ["-Command", "$PSVersionTable.PSVersion.ToString()"],
161
227
  stdout=subprocess.PIPE,
162
228
  text=True,
163
229
  )
@@ -166,125 +232,117 @@ class WindowsPowerShell(Shell):
166
232
  else:
167
233
  raise RuntimeError("Failed to parse PowerShell version information.")
168
234
 
169
- out = {
235
+ osinfo = {} if exclude_os else get_OS_info_windows()
236
+ return {
170
237
  "shell_name": "powershell",
171
238
  "shell_executable": self.executable,
172
239
  "shell_version": PS_version,
240
+ **osinfo,
173
241
  }
174
242
 
175
- if not exclude_os:
176
- out.update(**get_OS_info_windows())
177
-
178
- return out
179
-
180
243
  @staticmethod
181
- def process_app_invoc_executable(app_invoc_exe):
244
+ def process_app_invoc_executable(app_invoc_exe: str) -> str:
182
245
  if " " in app_invoc_exe:
183
246
  # use call operator and single-quote the executable path:
184
247
  app_invoc_exe = f"& '{app_invoc_exe}'"
185
248
  return app_invoc_exe
186
249
 
187
- def format_stream_assignment(self, shell_var_name, command):
250
+ @override
251
+ def format_env_var_get(self, var: str) -> str:
252
+ """
253
+ Format retrieval of a shell environment variable.
254
+ """
255
+ return f"$env:{var}"
256
+
257
+ @override
258
+ def format_array(self, lst: list) -> str:
259
+ """
260
+ Format construction of a shell array.
261
+ """
262
+ return "@(" + ", ".join(str(i) for i in lst) + ")"
263
+
264
+ @override
265
+ def format_array_get_item(self, arr_name: str, index: int | str) -> str:
266
+ """
267
+ Format retrieval of a shell array item at a specified index.
268
+ """
269
+ return f"${arr_name}[{index}]"
270
+
271
+ @override
272
+ def format_stream_assignment(self, shell_var_name: str, command: str) -> str:
188
273
  """
189
274
  Produce code to assign the output of the command to a shell variable.
190
275
  """
191
276
  return f"${shell_var_name} = {command}"
192
277
 
278
+ @override
279
+ def format_source_functions_file(self, app_name: str, commands: str) -> str:
280
+ """
281
+ Format sourcing (i.e. invocation) of the jobscript functions file.
282
+ """
283
+ app_caps = app_name.upper()
284
+ out = dedent(
285
+ """\
286
+ . $env:{app_name}_JS_FUNCS_PATH
287
+
288
+ """
289
+ ).format(app_name=app_caps)
290
+
291
+ var_strings = (
292
+ f"{app_caps}_WK_PATH",
293
+ f"{app_caps}_SUB_SCRIPTS_DIR",
294
+ f"{app_caps}_JS_IDX",
295
+ f"{app_caps}_BLOCK_IDX",
296
+ f"{app_caps}_BLOCK_ACT_IDX",
297
+ f"{app_caps}_RUN_ID",
298
+ f"{app_caps}_RUN_STD_PATH",
299
+ f"{app_caps}_RUN_SCRIPT_NAME",
300
+ f"{app_caps}_RUN_SCRIPT_NAME_NO_EXT",
301
+ f"{app_caps}_RUN_SCRIPT_DIR",
302
+ f"{app_caps}_RUN_SCRIPT_PATH",
303
+ )
304
+ add = False
305
+ for i in var_strings:
306
+ if i in commands:
307
+ add = True
308
+ out += f"${i} = $env:{i}\n"
309
+
310
+ if add:
311
+ out += "\n"
312
+
313
+ return out
314
+
315
+ @override
316
+ def format_commands_file(self, app_name: str, commands: str) -> str:
317
+ """
318
+ Format the commands file.
319
+ """
320
+ return (
321
+ self.format_source_functions_file(app_name, commands)
322
+ + commands
323
+ + "\nexit $LASTEXITCODE\n"
324
+ )
325
+
326
+ @override
193
327
  def format_save_parameter(
194
328
  self,
195
329
  workflow_app_alias: str,
196
330
  param_name: str,
197
331
  shell_var_name: str,
198
- EAR_ID: int,
199
332
  cmd_idx: int,
200
333
  stderr: bool,
201
- ):
334
+ app_name: str,
335
+ ) -> str:
202
336
  """
203
337
  Produce code to save a parameter's value into the workflow persistent store.
204
338
  """
205
339
  # TODO: quote shell_var_name as well? e.g. if it's a white-space delimited list?
206
340
  # and test.
207
341
  stderr_str = " --stderr" if stderr else ""
342
+ app_caps = app_name.upper()
208
343
  return (
209
- f"{workflow_app_alias} "
210
- f"internal workflow $WK_PATH save-parameter "
211
- f"{param_name} ${shell_var_name} {EAR_ID} {cmd_idx}{stderr_str} "
212
- f"2>&1 >> $app_stream_file"
344
+ f'{workflow_app_alias} --std-stream "${app_caps}_RUN_STD_PATH" '
345
+ f'internal workflow "${app_caps}_WK_PATH" save-parameter {stderr_str}'
346
+ f'"--" {param_name} ${shell_var_name} ${app_caps}_RUN_ID {cmd_idx}'
213
347
  f"\n"
214
348
  )
215
-
216
- def format_loop_check(self, workflow_app_alias: str, loop_name: str, run_ID: int):
217
- """
218
- Produce code to check the looping status of part of a workflow.
219
- """
220
- return (
221
- f"{workflow_app_alias} "
222
- f"internal workflow $WK_PATH check-loop "
223
- f"{loop_name} {run_ID} "
224
- f"2>&1 >> $app_stream_file"
225
- f"\n"
226
- )
227
-
228
- def wrap_in_subshell(self, commands: str, abortable: bool) -> str:
229
- """Format commands to run within a child scope.
230
-
231
- This assumes `commands` ends in a newline.
232
-
233
- """
234
- commands = indent(commands, self.JS_INDENT)
235
- if abortable:
236
- # run commands as a background job, and poll a file to check for abort
237
- # requests:
238
- return dedent(
239
- """\
240
- $job = StartJobHere {{
241
- $WK_PATH = $using:WK_PATH
242
- $SUB_IDX = $using:SUB_IDX
243
- $JS_IDX = $using:JS_IDX
244
- $EAR_ID = $using:EAR_ID
245
- $app_stream_file= $using:app_stream_file
246
-
247
- {commands}
248
- if ($LASTEXITCODE -ne 0) {{
249
- throw
250
- }}
251
- }}
252
-
253
- $is_abort = $null
254
- $abort_file = JoinMultiPath $WK_PATH artifacts submissions $SUB_IDX abort_EARs.txt
255
- while ($true) {{
256
- $is_abort = get_nth_line $abort_file $EAR_ID
257
- if ($job.State -ne "Running") {{
258
- break
259
- }}
260
- elseif ($is_abort -eq "1") {{
261
- Add-Content -Path $app_stream_file -Value "Abort instruction received; stopping commands..."
262
- Stop-Job -Job $job
263
- Wait-Job -Job $job
264
- break
265
- }}
266
- else {{
267
- Receive-Job -job $job | Write-Output
268
- Start-Sleep 1 # TODO: TEMP: increase for production
269
- }}
270
- }}
271
- Receive-Job -job $job | Write-Output
272
- if ($job.state -eq "Completed") {{
273
- exit 0
274
- }}
275
- elseif ($is_abort -eq "1") {{
276
- exit {abort_exit_code}
277
- }}
278
- else {{
279
- exit 1
280
- }}
281
- """
282
- ).format(commands=commands, abort_exit_code=ABORT_EXIT_CODE)
283
- else:
284
- # run commands in "foreground":
285
- return dedent(
286
- """\
287
- & {{
288
- {commands}}}
289
- """
290
- ).format(commands=commands)