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
hpcflow/sdk/cli.py CHANGED
@@ -2,12 +2,15 @@
2
2
  Command line interface implementation.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+ import contextlib
5
7
  import json
6
8
  import os
7
- from typing import Dict, List
9
+ import time
8
10
  import click
9
11
  from colorama import init as colorama_init
10
- from termcolor import colored
12
+ from termcolor import colored # type: ignore
13
+ from typing import TYPE_CHECKING
11
14
  from rich.pretty import pprint
12
15
 
13
16
  from hpcflow import __version__, _app_name
@@ -31,7 +34,9 @@ from hpcflow.sdk.cli_common import (
31
34
  tasks_opt,
32
35
  cancel_opt,
33
36
  submit_status_opt,
37
+ force_arr_opt,
34
38
  make_status_opt,
39
+ add_sub_opt,
35
40
  zip_path_opt,
36
41
  zip_overwrite_opt,
37
42
  zip_log_opt,
@@ -42,11 +47,27 @@ from hpcflow.sdk.cli_common import (
42
47
  rechunk_backup_opt,
43
48
  rechunk_chunk_size_opt,
44
49
  rechunk_status_opt,
50
+ cancel_status_opt,
51
+ list_js_max_js_opt,
52
+ list_js_jobscripts_opt,
53
+ list_task_js_max_js_opt,
54
+ list_task_js_task_names_opt,
55
+ list_js_width_opt,
56
+ jobscript_std_array_idx_opt,
45
57
  _add_doc_from_help,
46
58
  )
47
59
  from hpcflow.sdk.helper.cli import get_helper_CLI
48
60
  from hpcflow.sdk.log import TimeIt
61
+ from hpcflow.sdk.core.workflow import Workflow
49
62
  from hpcflow.sdk.submission.shells import ALL_SHELLS
63
+ from hpcflow.sdk.submission.jobscript import Jobscript
64
+ from hpcflow.sdk.submission.submission import Submission
65
+ from hpcflow.sdk.submission.schedulers.sge import SGEPosix
66
+
67
+ if TYPE_CHECKING:
68
+ from pathlib import Path
69
+ from typing import Literal
70
+ from .app import BaseApp
50
71
 
51
72
  #: Standard option
52
73
  string_option = click.option(
@@ -59,16 +80,61 @@ string_option = click.option(
59
80
  workflow_ref_type_opt = click.option(
60
81
  "--ref-type",
61
82
  "-r",
62
- type=click.Choice(["assume-id", "id", "path"]),
83
+ type=click.Choice(("assume-id", "id", "path")),
63
84
  default="assume-id",
64
85
  help="How to interpret a reference, as an ID, a path, or to guess.",
65
86
  )
66
87
 
88
+ #: Get the current workflow from the context.
89
+ _pass_workflow = click.make_pass_decorator(Workflow)
90
+ #: Get the current submission from the context.
91
+ _pass_submission = click.make_pass_decorator(Submission)
92
+ #: Get the current jobscript from the context.
93
+ _pass_js = click.make_pass_decorator(Jobscript)
67
94
 
68
95
  _add_doc_from_help(string_option, workflow_ref_type_opt)
69
96
 
70
97
 
71
- def parse_jobscript_wait_spec(jobscripts: str) -> Dict[int, List[int]]:
98
+ class ErrorPropagatingClickContext(click.Context):
99
+ """A click Context class that passes on exception information.
100
+
101
+ Using the standard `click.Context` class, exceptions raised when using a resource specified
102
+ with `ctx.with_resource(my_ctx_manager())` are not passed on to the `__exit__` method of
103
+ `my_ctx_manager`. See: https://github.com/pallets/click/issues/2447.
104
+
105
+ Examples
106
+ --------
107
+ >>> @click.group()
108
+ ... @click.pass_context
109
+ ... def cli(ctx):
110
+ ... ctx.with_resource(my_context_manager())
111
+ ... cli.context_class = ErrorPropagatingClickContext
112
+
113
+ """
114
+
115
+ def __exit__(self, exc_type, exc_value, tb):
116
+ self._depth -= 1
117
+ if self._depth == 0:
118
+ self._exit_stack.__exit__(exc_type, exc_value, tb)
119
+ self._exit_stack = contextlib.ExitStack()
120
+ click.core.pop_context()
121
+
122
+
123
+ @contextlib.contextmanager
124
+ def redirect_std_to_file_click(file, mode: str = "a"):
125
+ def ignore(exc):
126
+ """Do not intercept Click's `Exit` exception when the exit code is zero."""
127
+ if type(exc) is click.exceptions.Exit:
128
+ if exc.exit_code == 0:
129
+ return True
130
+ return exc.exit_code
131
+ return 1 # default exit code
132
+
133
+ with utils.redirect_std_to_file(file=file, ignore=ignore):
134
+ yield
135
+
136
+
137
+ def parse_jobscript_wait_spec(jobscripts: str) -> dict[int, list[int]]:
72
138
  """
73
139
  Parse a jobscript wait specification.
74
140
  """
@@ -79,7 +145,15 @@ def parse_jobscript_wait_spec(jobscripts: str) -> Dict[int, List[int]]:
79
145
  return sub_js_idx_dct
80
146
 
81
147
 
82
- def _make_API_CLI(app):
148
+ def _set_help_name(cmd: click.Group | click.Command, app: BaseApp):
149
+ """
150
+ Update the help string of the command to contain the name of the application.
151
+ """
152
+ if cmd.help:
153
+ cmd.help = cmd.help.format(app_name=app.name)
154
+
155
+
156
+ def _make_API_CLI(app: BaseApp):
83
157
  """Generate the CLI for the main functionality."""
84
158
 
85
159
  @click.command(name="make")
@@ -94,18 +168,20 @@ def _make_API_CLI(app):
94
168
  @ts_name_fmt_option
95
169
  @variables_option
96
170
  @make_status_opt
171
+ @add_sub_opt
97
172
  def make_workflow(
98
- template_file_or_str,
99
- string,
100
- format,
101
- path,
102
- name,
103
- overwrite,
104
- store,
105
- ts_fmt=None,
106
- ts_name_fmt=None,
107
- variables=None,
108
- status=True,
173
+ template_file_or_str: str,
174
+ string: bool,
175
+ format: Literal["json", "yaml"] | None,
176
+ path: Path | None,
177
+ name: str | None,
178
+ overwrite: bool,
179
+ store: str,
180
+ ts_fmt: str | None = None,
181
+ ts_name_fmt: str | None = None,
182
+ variables: list[tuple[str, str]] | None = None,
183
+ status: bool = True,
184
+ add_submission: bool = False,
109
185
  ):
110
186
  """Generate a new {app_name} workflow.
111
187
 
@@ -123,9 +199,11 @@ def _make_API_CLI(app):
123
199
  store=store,
124
200
  ts_fmt=ts_fmt,
125
201
  ts_name_fmt=ts_name_fmt,
126
- variables=dict(variables),
202
+ variables=dict(variables) if variables is not None else None,
127
203
  status=status,
204
+ add_submission=add_submission,
128
205
  )
206
+ assert isinstance(wk, Workflow)
129
207
  click.echo(wk.path)
130
208
 
131
209
  @click.command(name="go")
@@ -147,23 +225,23 @@ def _make_API_CLI(app):
147
225
  @cancel_opt
148
226
  @submit_status_opt
149
227
  def make_and_submit_workflow(
150
- template_file_or_str,
151
- string,
152
- format,
153
- path,
154
- name,
155
- overwrite,
156
- store,
157
- ts_fmt=None,
158
- ts_name_fmt=None,
159
- variables=None,
160
- js_parallelism=None,
161
- wait=False,
162
- add_to_known=True,
163
- print_idx=False,
164
- tasks=None,
165
- cancel=False,
166
- status=True,
228
+ template_file_or_str: str,
229
+ string: bool,
230
+ format: Literal["json", "yaml"] | None,
231
+ path: Path | None,
232
+ name: str | None,
233
+ overwrite: bool,
234
+ store: str,
235
+ ts_fmt: str | None = None,
236
+ ts_name_fmt: str | None = None,
237
+ variables: list[tuple[str, str]] | None = None,
238
+ js_parallelism: bool | None = None,
239
+ wait: bool = False,
240
+ add_to_known: bool = True,
241
+ print_idx: bool = False,
242
+ tasks: list[int] | None = None,
243
+ cancel: bool = False,
244
+ status: bool = True,
167
245
  ):
168
246
  """Generate and submit a new {app_name} workflow.
169
247
 
@@ -182,7 +260,7 @@ def _make_API_CLI(app):
182
260
  store=store,
183
261
  ts_fmt=ts_fmt,
184
262
  ts_name_fmt=ts_name_fmt,
185
- variables=dict(variables),
263
+ variables=dict(variables) if variables is not None else None,
186
264
  JS_parallelism=js_parallelism,
187
265
  wait=wait,
188
266
  add_to_known=add_to_known,
@@ -192,12 +270,13 @@ def _make_API_CLI(app):
192
270
  status=status,
193
271
  )
194
272
  if print_idx:
273
+ assert isinstance(out, tuple)
195
274
  click.echo(out[1])
196
275
 
197
276
  @click.command(context_settings={"ignore_unknown_options": True})
198
277
  @click.argument("py_test_args", nargs=-1, type=click.UNPROCESSED)
199
278
  @click.pass_context
200
- def test(ctx, py_test_args):
279
+ def test(ctx: click.Context, py_test_args: list[str]):
201
280
  """Run {app_name} test suite.
202
281
 
203
282
  PY_TEST_ARGS are arguments passed on to Pytest.
@@ -208,7 +287,7 @@ def _make_API_CLI(app):
208
287
  @click.command(context_settings={"ignore_unknown_options": True})
209
288
  @click.argument("py_test_args", nargs=-1, type=click.UNPROCESSED)
210
289
  @click.pass_context
211
- def test_hpcflow(ctx, py_test_args):
290
+ def test_hpcflow(ctx: click.Context, py_test_args: list[str]):
212
291
  """Run hpcFlow test suite.
213
292
 
214
293
  PY_TEST_ARGS are arguments passed on to Pytest.
@@ -222,8 +301,7 @@ def _make_API_CLI(app):
222
301
  test,
223
302
  ]
224
303
  for cmd in commands:
225
- if cmd.help:
226
- cmd.help = cmd.help.format(app_name=app.name)
304
+ _set_help_name(cmd, app)
227
305
 
228
306
  if app.name != "hpcFlow":
229
307
  # `test_hpcflow` is the same as `test` for the hpcflow app no need to add both:
@@ -232,118 +310,173 @@ def _make_API_CLI(app):
232
310
  return commands
233
311
 
234
312
 
235
- def _make_workflow_submission_jobscript_CLI(app):
313
+ def _make_workflow_submission_jobscript_CLI(app: BaseApp):
236
314
  """Generate the CLI for interacting with existing workflow submission
237
315
  jobscripts."""
238
316
 
239
317
  @click.group(name="js")
318
+ @_pass_submission
240
319
  @click.pass_context
241
320
  @click.argument("js_idx", type=click.INT)
242
- def jobscript(ctx, js_idx):
321
+ def jobscript(ctx: click.Context, sb: Submission, js_idx: int):
243
322
  """Interact with existing {app_name} workflow submission jobscripts.
244
323
 
245
324
  JS_IDX is the jobscript index within the submission object.
246
325
 
247
326
  """
248
- ctx.obj["jobscript"] = ctx.obj["submission"].jobscripts[js_idx]
327
+ ctx.obj = sb.jobscripts[js_idx]
249
328
 
250
329
  @jobscript.command(name="res")
251
- @click.pass_context
252
- def resources(ctx):
330
+ @_pass_js
331
+ def resources(job: Jobscript):
253
332
  """Get resources associated with this jobscript."""
254
- click.echo(ctx.obj["jobscript"].resources.__dict__)
333
+ click.echo(job.resources.__dict__)
255
334
 
256
335
  @jobscript.command(name="deps")
257
- @click.pass_context
258
- def dependencies(ctx):
336
+ @_pass_js
337
+ def dependencies(job: Jobscript):
259
338
  """Get jobscript dependencies."""
260
- click.echo(ctx.obj["jobscript"].dependencies)
339
+ click.echo(job.dependencies)
261
340
 
262
341
  @jobscript.command()
263
- @click.pass_context
264
- def path(ctx):
342
+ @_pass_js
343
+ def path(job: Jobscript):
265
344
  """Get the file path to the jobscript."""
266
- click.echo(ctx.obj["jobscript"].jobscript_path)
345
+ click.echo(job.jobscript_path)
267
346
 
268
347
  @jobscript.command()
269
- @click.pass_context
270
- def show(ctx):
348
+ @_pass_js
349
+ def show(job: Jobscript):
271
350
  """Show the jobscript file."""
272
- with ctx.obj["jobscript"].jobscript_path.open("rt") as fp:
351
+ with job.jobscript_path.open("rt") as fp:
273
352
  click.echo(fp.read())
274
353
 
275
- jobscript.help = jobscript.help.format(app_name=app.name)
354
+ @jobscript.command()
355
+ @jobscript_std_array_idx_opt
356
+ @_pass_js
357
+ def stdout(job: Jobscript, array_idx: int):
358
+ """Print the contents of the standard output stream file."""
359
+ job.print_stdout(array_idx=array_idx)
360
+
361
+ @jobscript.command()
362
+ @jobscript_std_array_idx_opt
363
+ @_pass_js
364
+ def stderr(job: Jobscript, array_idx: int):
365
+ """Print the contents of the standard error stream file."""
366
+ job.print_stderr(array_idx=array_idx)
276
367
 
368
+ _set_help_name(jobscript, app)
277
369
  return jobscript
278
370
 
279
371
 
280
- def _make_workflow_submission_CLI(app):
372
+ def _make_workflow_submission_CLI(app: BaseApp):
281
373
  """Generate the CLI for interacting with existing workflow submissions."""
282
374
 
283
375
  @click.group(name="sub")
376
+ @_pass_workflow
284
377
  @click.pass_context
285
378
  @click.argument("sub_idx", type=click.INT)
286
- def submission(ctx, sub_idx):
379
+ def submission(ctx: click.Context, wf: Workflow, sub_idx: int):
287
380
  """Interact with existing {app_name} workflow submissions.
288
381
 
289
382
  SUB_IDX is the submission index.
290
383
 
291
384
  """
292
- ctx.obj["submission"] = ctx.obj["workflow"].submissions[sub_idx]
385
+ ctx.obj = wf.submissions[sub_idx]
293
386
 
294
387
  @submission.command("status")
295
- @click.pass_context
296
- def status(ctx):
388
+ @_pass_submission
389
+ def status(sb: Submission):
297
390
  """Get the submission status."""
298
- click.echo(ctx.obj["submission"].status.name.lower())
391
+ click.echo(sb.status.name.lower())
299
392
 
300
393
  @submission.command("submitted-js")
301
- @click.pass_context
302
- def submitted_JS(ctx):
394
+ @_pass_submission
395
+ def submitted_JS(sb: Submission):
303
396
  """Get a list of jobscript indices that have been submitted."""
304
- click.echo(ctx.obj["submission"].submitted_jobscripts)
397
+ click.echo(sb.submitted_jobscripts)
305
398
 
306
399
  @submission.command("outstanding-js")
307
- @click.pass_context
308
- def outstanding_JS(ctx):
400
+ @_pass_submission
401
+ def outstanding_JS(sb: Submission):
309
402
  """Get a list of jobscript indices that have not yet been submitted."""
310
- click.echo(ctx.obj["submission"].outstanding_jobscripts)
403
+ click.echo(sb.outstanding_jobscripts)
311
404
 
312
405
  @submission.command("needs-submit")
313
- @click.pass_context
314
- def needs_submit(ctx):
406
+ @_pass_submission
407
+ def needs_submit(sb: Submission):
315
408
  """Check if this submission needs submitting."""
316
- click.echo(ctx.obj["submission"].needs_submit)
409
+ click.echo(sb.needs_submit)
317
410
 
318
411
  @submission.command("get-active-jobscripts")
319
- @click.pass_context
320
- def get_active_jobscripts(ctx):
412
+ @_pass_submission
413
+ def get_active_jobscripts(sb: Submission):
321
414
  """Show active jobscripts and their jobscript-element states."""
322
- pprint(ctx.obj["submission"].get_active_jobscripts(as_json=True))
415
+ pprint(sb.get_active_jobscripts(as_json=True))
323
416
 
324
- submission.help = submission.help.format(app_name=app.name)
325
- submission.add_command(_make_workflow_submission_jobscript_CLI(app))
417
+ @submission.command()
418
+ @_pass_submission
419
+ def get_scheduler_job_IDs(sb: Submission):
420
+ """Print jobscript scheduler job IDs."""
421
+ job_IDs = sb.get_scheduler_job_IDs()
422
+ if job_IDs:
423
+ print("\n".join(job_IDs))
424
+
425
+ @submission.command()
426
+ @_pass_submission
427
+ def get_process_IDs(sb: Submission):
428
+ """Print jobscript process IDs."""
429
+ proc_IDs = sb.get_process_IDs()
430
+ if proc_IDs:
431
+ print("\n".join(str(i) for i in proc_IDs))
432
+
433
+ @submission.command()
434
+ @list_js_max_js_opt
435
+ @list_js_jobscripts_opt
436
+ @list_js_width_opt
437
+ @_pass_submission
438
+ def list_jobscripts(
439
+ sb: Submission, max_js: int | None, jobscripts: str | None, width: int | None
440
+ ):
441
+ """Print a table listing jobscripts and associated information."""
442
+ jobscripts_ = [int(i) for i in jobscripts.split(",")] if jobscripts else None
443
+ sb.list_jobscripts(max_js=max_js, jobscripts=jobscripts_, width=width)
444
+
445
+ @submission.command()
446
+ @list_task_js_max_js_opt
447
+ @list_task_js_task_names_opt
448
+ @list_js_width_opt
449
+ @_pass_submission
450
+ def list_task_jobscripts(
451
+ sb: Submission,
452
+ max_js: int | None,
453
+ task_names: str | None,
454
+ width: int | None,
455
+ ):
456
+ """Print a table listing tasks and their associated jobscripts."""
457
+ task_names_ = list(task_names.split(",")) if task_names else None
458
+ sb.list_task_jobscripts(task_names=task_names_, max_js=max_js, width=width)
326
459
 
460
+ _set_help_name(submission, app)
461
+ submission.add_command(_make_workflow_submission_jobscript_CLI(app))
327
462
  return submission
328
463
 
329
464
 
330
- def _make_workflow_CLI(app):
465
+ def _make_workflow_CLI(app: BaseApp):
331
466
  """Generate the CLI for interacting with existing workflows."""
332
467
 
333
468
  @click.group()
334
469
  @click.argument("workflow_ref")
335
470
  @workflow_ref_type_opt
336
471
  @click.pass_context
337
- def workflow(ctx, workflow_ref, ref_type):
472
+ def workflow(ctx: click.Context, workflow_ref: str, ref_type: str | None):
338
473
  """Interact with existing {app_name} workflows.
339
474
 
340
475
  WORKFLOW_REF is the path to, or local ID of, an existing workflow.
341
476
 
342
477
  """
343
478
  workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
344
- wk = app.Workflow(workflow_path)
345
- ctx.ensure_object(dict)
346
- ctx.obj["workflow"] = wk
479
+ ctx.obj = app.Workflow(workflow_path)
347
480
 
348
481
  @workflow.command(name="submit")
349
482
  @js_parallelism_option
@@ -353,23 +486,23 @@ def _make_workflow_CLI(app):
353
486
  @tasks_opt
354
487
  @cancel_opt
355
488
  @submit_status_opt
356
- @click.pass_context
489
+ @_pass_workflow
357
490
  def submit_workflow(
358
- ctx,
359
- js_parallelism=None,
360
- wait=False,
361
- add_to_known=True,
362
- print_idx=False,
363
- tasks=None,
364
- cancel=False,
365
- status=True,
491
+ wf: Workflow,
492
+ js_parallelism: bool | None = None,
493
+ wait: bool = False,
494
+ add_to_known: bool = True,
495
+ print_idx: bool = False,
496
+ tasks: list[int] | None = None,
497
+ cancel: bool = False,
498
+ status: bool = True,
366
499
  ):
367
500
  """Submit the workflow."""
368
- out = ctx.obj["workflow"].submit(
501
+ out = wf.submit(
369
502
  JS_parallelism=js_parallelism,
370
503
  wait=wait,
371
504
  add_to_known=add_to_known,
372
- return_idx=print_idx,
505
+ return_idx=True,
373
506
  tasks=tasks,
374
507
  cancel=cancel,
375
508
  status=status,
@@ -377,6 +510,27 @@ def _make_workflow_CLI(app):
377
510
  if print_idx:
378
511
  click.echo(out)
379
512
 
513
+ @workflow.command(name="add-submission")
514
+ @js_parallelism_option
515
+ @tasks_opt
516
+ @force_arr_opt
517
+ @submit_status_opt
518
+ @click.pass_context
519
+ def add_submission(
520
+ ctx,
521
+ js_parallelism=None,
522
+ tasks=None,
523
+ force_array=False,
524
+ status=True,
525
+ ):
526
+ """Add a new submission to the workflow, but do not submit."""
527
+ ctx.obj["workflow"].add_submission(
528
+ JS_parallelism=js_parallelism,
529
+ tasks=tasks,
530
+ force_array=force_array,
531
+ status=status,
532
+ )
533
+
380
534
  @workflow.command(name="wait")
381
535
  @click.option(
382
536
  "-j",
@@ -389,19 +543,19 @@ def _make_workflow_CLI(app):
389
543
  "separate patterns like these."
390
544
  ),
391
545
  )
392
- @click.pass_context
393
- def wait(ctx, jobscripts):
546
+ @_pass_workflow
547
+ def wait(wf: Workflow, jobscripts: str | None):
394
548
  js_spec = parse_jobscript_wait_spec(jobscripts) if jobscripts else None
395
- ctx.obj["workflow"].wait(sub_js=js_spec)
549
+ wf.wait(sub_js=js_spec)
396
550
 
397
551
  @workflow.command(name="abort-run")
398
552
  @click.option("--submission", type=click.INT, default=-1)
399
553
  @click.option("--task", type=click.INT)
400
554
  @click.option("--element", type=click.INT)
401
- @click.pass_context
402
- def abort_run(ctx, submission, task, element):
555
+ @_pass_workflow
556
+ def abort_run(wf: Workflow, submission: int, task: int, element: int):
403
557
  """Abort the specified run."""
404
- ctx.obj["workflow"].abort_run(
558
+ wf.abort_run(
405
559
  submission_idx=submission,
406
560
  task_idx=task,
407
561
  element_idx=element,
@@ -409,36 +563,36 @@ def _make_workflow_CLI(app):
409
563
 
410
564
  @workflow.command(name="get-param")
411
565
  @click.argument("index", type=click.INT)
412
- @click.pass_context
413
- def get_parameter(ctx, index):
566
+ @_pass_workflow
567
+ def get_parameter(wf: Workflow, index: int):
414
568
  """Get a parameter value by data index."""
415
- click.echo(ctx.obj["workflow"].get_parameter_data(index))
569
+ click.echo(wf.get_parameter_data(index))
416
570
 
417
571
  @workflow.command(name="get-param-source")
418
572
  @click.argument("index", type=click.INT)
419
- @click.pass_context
420
- def get_parameter_source(ctx, index):
573
+ @_pass_workflow
574
+ def get_parameter_source(wf: Workflow, index: int):
421
575
  """Get a parameter source by data index."""
422
- click.echo(ctx.obj["workflow"].get_parameter_source(index))
576
+ click.echo(wf.get_parameter_source(index))
423
577
 
424
578
  @workflow.command(name="get-all-params")
425
- @click.pass_context
426
- def get_all_parameters(ctx):
579
+ @_pass_workflow
580
+ def get_all_parameters(wf: Workflow):
427
581
  """Get all parameter values."""
428
- click.echo(ctx.obj["workflow"].get_all_parameter_data())
582
+ click.echo(wf.get_all_parameter_data())
429
583
 
430
584
  @workflow.command(name="is-param-set")
431
585
  @click.argument("index", type=click.INT)
432
- @click.pass_context
433
- def is_parameter_set(ctx, index):
586
+ @_pass_workflow
587
+ def is_parameter_set(wf: Workflow, index: int):
434
588
  """Check if a parameter specified by data index is set."""
435
- click.echo(ctx.obj["workflow"].is_parameter_set(index))
589
+ click.echo(wf.is_parameter_set(index))
436
590
 
437
591
  @workflow.command(name="show-all-status")
438
- @click.pass_context
439
- def show_all_EAR_statuses(ctx):
592
+ @_pass_workflow
593
+ def show_all_EAR_statuses(wf: Workflow):
440
594
  """Show the submission status of all workflow EARs."""
441
- ctx.obj["workflow"].show_all_EAR_statuses()
595
+ wf.show_all_EAR_statuses()
442
596
 
443
597
  @workflow.command(name="zip")
444
598
  @zip_path_opt
@@ -446,19 +600,19 @@ def _make_workflow_CLI(app):
446
600
  @zip_log_opt
447
601
  @zip_include_execute_opt
448
602
  @zip_include_rechunk_backups_opt
449
- @click.pass_context
603
+ @_pass_workflow
450
604
  def zip_workflow(
451
- ctx,
452
- path,
453
- overwrite,
454
- log,
455
- include_execute,
456
- include_rechunk_backups,
605
+ wf: Workflow,
606
+ path: str,
607
+ overwrite: bool,
608
+ log: str | None,
609
+ include_execute: bool,
610
+ include_rechunk_backups: bool,
457
611
  ):
458
612
  """Generate a copy of the workflow in the zip file format in the current working
459
613
  directory."""
460
614
  click.echo(
461
- ctx.obj["workflow"].zip(
615
+ wf.zip(
462
616
  path=path,
463
617
  overwrite=overwrite,
464
618
  log=log,
@@ -470,54 +624,114 @@ def _make_workflow_CLI(app):
470
624
  @workflow.command(name="unzip")
471
625
  @unzip_path_opt
472
626
  @unzip_log_opt
473
- @click.pass_context
474
- def unzip_workflow(ctx, path, log):
627
+ @_pass_workflow
628
+ def unzip_workflow(wf: Workflow, path: str, log: str | None):
475
629
  """Generate a copy of the zipped workflow in the submittable Zarr format in the
476
630
  current working directory."""
477
- click.echo(ctx.obj["workflow"].unzip(path=path, log=log))
631
+ click.echo(wf.unzip(path=path, log=log))
478
632
 
479
633
  @workflow.command(name="rechunk")
480
634
  @rechunk_backup_opt
481
635
  @rechunk_chunk_size_opt
482
636
  @rechunk_status_opt
483
- @click.pass_context
484
- def rechunk(ctx, backup, chunk_size, status):
637
+ @_pass_workflow
638
+ def rechunk(wf: Workflow, backup: bool, chunk_size: int, status: bool):
485
639
  """Rechunk metadata/runs and parameters/base arrays."""
486
- ctx.obj["workflow"].rechunk(backup=backup, chunk_size=chunk_size, status=status)
640
+ wf.rechunk(backup=backup, chunk_size=chunk_size, status=status)
487
641
 
488
642
  @workflow.command(name="rechunk-runs")
489
643
  @rechunk_backup_opt
490
644
  @rechunk_chunk_size_opt
491
645
  @rechunk_status_opt
492
- @click.pass_context
493
- def rechunk_runs(ctx, backup, chunk_size, status):
646
+ @_pass_workflow
647
+ def rechunk_runs(wf: Workflow, backup: bool, chunk_size: int, status: bool):
494
648
  """Rechunk the metadata/runs array."""
495
- ctx.obj["workflow"].rechunk_runs(
496
- backup=backup, chunk_size=chunk_size, status=status
497
- )
649
+ wf.rechunk_runs(backup=backup, chunk_size=chunk_size, status=status)
498
650
 
499
651
  @workflow.command(name="rechunk-parameter-base")
500
652
  @rechunk_backup_opt
501
653
  @rechunk_chunk_size_opt
502
654
  @rechunk_status_opt
503
- @click.pass_context
504
- def rechunk_parameter_base(ctx, backup, chunk_size, status):
655
+ @_pass_workflow
656
+ def rechunk_parameter_base(wf: Workflow, backup: bool, chunk_size: int, status: bool):
505
657
  """Rechunk the parameters/base array."""
506
- ctx.obj["workflow"].rechunk_parameter_base(
507
- backup=backup, chunk_size=chunk_size, status=status
658
+ wf.rechunk_parameter_base(backup=backup, chunk_size=chunk_size, status=status)
659
+
660
+ @workflow.command()
661
+ @_pass_workflow
662
+ def get_scheduler_job_IDs(wf: Workflow):
663
+ """Print jobscript scheduler job IDs from all submissions of this workflow."""
664
+ job_IDs = wf.get_scheduler_job_IDs()
665
+ if job_IDs:
666
+ print("\n".join(job_IDs))
667
+
668
+ @workflow.command()
669
+ @_pass_workflow
670
+ def get_process_IDs(wf: Workflow):
671
+ """Print jobscript process IDs from all submissions of this workflow."""
672
+ proc_IDs = wf.get_process_IDs()
673
+ if proc_IDs:
674
+ print("\n".join(str(i) for i in proc_IDs))
675
+
676
+ @workflow.command()
677
+ @click.option(
678
+ "--sub-idx",
679
+ type=click.INT,
680
+ default=0,
681
+ help="Submission index whose jobscripts are to be shown.",
682
+ )
683
+ @list_js_max_js_opt
684
+ @list_js_jobscripts_opt
685
+ @list_js_width_opt
686
+ @_pass_workflow
687
+ def list_jobscripts(
688
+ wf: Workflow,
689
+ sub_idx: int,
690
+ max_js: int | None,
691
+ jobscripts: str | None,
692
+ width: int | None,
693
+ ):
694
+ """Print a table listing jobscripts and associated information from the specified
695
+ submission."""
696
+ jobscripts_ = [int(i) for i in jobscripts.split(",")] if jobscripts else None
697
+ wf.list_jobscripts(
698
+ sub_idx=sub_idx, max_js=max_js, jobscripts=jobscripts_, width=width
508
699
  )
509
700
 
510
- workflow.help = workflow.help.format(app_name=app.name)
701
+ @workflow.command()
702
+ @click.option(
703
+ "--sub-idx",
704
+ type=click.INT,
705
+ default=0,
706
+ help="Submission index whose tasks are to be shown.",
707
+ )
708
+ @list_task_js_max_js_opt
709
+ @list_task_js_task_names_opt
710
+ @list_js_width_opt
711
+ @_pass_workflow
712
+ def list_task_jobscripts(
713
+ wf: Workflow,
714
+ sub_idx: int,
715
+ max_js: int | None,
716
+ task_names: str | None,
717
+ width: int | None,
718
+ ):
719
+ """Print a table listing tasks and their associated jobscripts from the specified
720
+ submission."""
721
+ task_names_ = list(task_names.split(",")) if task_names else None
722
+ wf.list_task_jobscripts(
723
+ sub_idx=sub_idx, task_names=task_names_, max_js=max_js, width=width
724
+ )
511
725
 
726
+ _set_help_name(workflow, app)
512
727
  workflow.add_command(_make_workflow_submission_CLI(app))
513
-
514
728
  return workflow
515
729
 
516
730
 
517
- def _make_submission_CLI(app):
731
+ def _make_submission_CLI(app: BaseApp):
518
732
  """Generate the CLI for submission related queries."""
519
733
 
520
- def OS_info_callback(ctx, param, value):
734
+ def OS_info_callback(ctx: click.Context, param, value: bool):
521
735
  if not value or ctx.resilient_parsing:
522
736
  return
523
737
  pprint(app.get_OS_info())
@@ -532,16 +746,15 @@ def _make_submission_CLI(app):
532
746
  expose_value=False,
533
747
  callback=OS_info_callback,
534
748
  )
535
- @click.pass_context
536
- def submission(ctx):
749
+ def submission():
537
750
  """Submission-related queries."""
538
- ctx.ensure_object(dict)
751
+ pass
539
752
 
540
753
  @submission.command("shell-info")
541
- @click.argument("shell_name", type=click.Choice(ALL_SHELLS))
754
+ @click.argument("shell_name", type=click.Choice(list(ALL_SHELLS)))
542
755
  @click.option("--exclude-os", is_flag=True, default=False)
543
756
  @click.pass_context
544
- def shell_info(ctx, shell_name, exclude_os):
757
+ def shell_info(ctx: click.Context, shell_name: str, exclude_os: bool):
545
758
  """Show information about the specified shell, such as the version."""
546
759
  pprint(app.get_shell_info(shell_name, exclude_os))
547
760
  ctx.exit()
@@ -549,13 +762,14 @@ def _make_submission_CLI(app):
549
762
  @submission.group("scheduler")
550
763
  @click.argument("scheduler_name")
551
764
  @click.pass_context
552
- def scheduler(ctx, scheduler_name):
553
- ctx.obj["scheduler_obj"] = app.get_scheduler(scheduler_name, os.name)
765
+ def scheduler(ctx: click.Context, scheduler_name: str):
766
+ ctx.obj = app.get_scheduler(scheduler_name, os.name)
767
+
768
+ pass_scheduler = click.make_pass_decorator(SGEPosix)
554
769
 
555
770
  @scheduler.command()
556
- @click.pass_context
557
- def get_login_nodes(ctx):
558
- scheduler = ctx.obj["scheduler_obj"]
771
+ @pass_scheduler
772
+ def get_login_nodes(scheduler: SGEPosix):
559
773
  pprint(scheduler.get_login_nodes())
560
774
 
561
775
  @submission.command()
@@ -566,8 +780,7 @@ def _make_submission_CLI(app):
566
780
  default=False,
567
781
  help="Do not format and only show JSON-compatible information.",
568
782
  )
569
- @click.pass_context
570
- def get_known(ctx, as_json=False):
783
+ def get_known(as_json: bool = False):
571
784
  """Print known-submissions information as a formatted Python object."""
572
785
  out = app.get_known_submissions(as_json=as_json)
573
786
  if as_json:
@@ -578,11 +791,11 @@ def _make_submission_CLI(app):
578
791
  return submission
579
792
 
580
793
 
581
- def _make_internal_CLI(app):
794
+ def _make_internal_CLI(app: BaseApp):
582
795
  """Generate the CLI for internal use."""
583
796
 
584
797
  @click.group()
585
- def internal(help=True): # TEMP
798
+ def internal(help: bool = True): # TEMP
586
799
  """Internal CLI to be invoked by scripts generated by the app."""
587
800
  pass
588
801
 
@@ -591,47 +804,75 @@ def _make_internal_CLI(app):
591
804
  """Get the invocation command for this app instance."""
592
805
  click.echo(app.run_time_info.invocation_command)
593
806
 
807
+ @internal.command()
808
+ @click.pass_context
809
+ @click.option("--raise", "raise_opt", is_flag=True)
810
+ @click.option("--click-exit-code", type=click.INT)
811
+ @click.option("--sleep", type=click.INT)
812
+ def noop(ctx, raise_opt, click_exit_code, sleep):
813
+ """Used only in CLI tests."""
814
+ if raise_opt:
815
+ raise ValueError("internal noop raised!")
816
+ elif click_exit_code is not None:
817
+ ctx.exit(click_exit_code)
818
+ elif sleep:
819
+ time.sleep(sleep)
820
+
594
821
  @internal.group()
595
822
  @click.argument("path", type=click.Path(exists=True))
596
823
  @click.pass_context
597
- def workflow(ctx, path):
824
+ def workflow(ctx: click.Context, path: Path):
598
825
  """"""
599
- wk = app.Workflow(path)
600
- ctx.ensure_object(dict)
601
- ctx.obj["workflow"] = wk
826
+ ctx.obj = app.Workflow(path)
602
827
 
603
828
  @workflow.command()
604
- @click.pass_context
829
+ @_pass_workflow
605
830
  @click.argument("submission_idx", type=click.INT)
606
831
  @click.argument("jobscript_idx", type=click.INT)
607
- @click.argument("js_action_idx", type=click.INT)
608
- @click.argument("ear_id", type=click.INT)
609
- def write_commands(
610
- ctx,
832
+ @click.argument("block_idx", type=click.INT)
833
+ @click.argument("block_action_idx", type=click.INT)
834
+ @click.argument("run_id", type=click.INT)
835
+ def execute_run(
836
+ wf: Workflow,
611
837
  submission_idx: int,
612
838
  jobscript_idx: int,
613
- js_action_idx: int,
614
- ear_id: int,
839
+ block_idx: int,
840
+ block_action_idx: int,
841
+ run_id: int,
615
842
  ):
616
- app.CLI_logger.info(f"write commands for EAR ID {ear_id!r}.")
617
- ctx.exit(
618
- ctx.obj["workflow"].write_commands(
619
- submission_idx,
620
- jobscript_idx,
621
- js_action_idx,
622
- ear_id,
623
- )
843
+ app.CLI_logger.info(f"execute commands for EAR ID {run_id!r}.")
844
+ wf.execute_run(
845
+ submission_idx=submission_idx,
846
+ block_act_key=(jobscript_idx, block_idx, block_action_idx),
847
+ run_ID=run_id,
624
848
  )
625
849
 
626
850
  @workflow.command()
627
- @click.pass_context
851
+ @_pass_workflow
852
+ @click.argument("submission_idx", type=click.INT)
853
+ @click.argument("jobscript_idx", type=click.INT)
854
+ def execute_combined_runs(
855
+ wf: Workflow,
856
+ submission_idx: int,
857
+ jobscript_idx: int,
858
+ ):
859
+ app.CLI_logger.info(
860
+ f"execute command for combined scripts of jobscript {jobscript_idx}."
861
+ )
862
+ wf.execute_combined_runs(
863
+ submission_idx=submission_idx,
864
+ jobscript_idx=jobscript_idx,
865
+ )
866
+
867
+ @workflow.command()
868
+ @_pass_workflow
628
869
  @click.argument("name")
629
870
  @click.argument("value")
630
871
  @click.argument("ear_id", type=click.INT)
631
872
  @click.argument("cmd_idx", type=click.INT)
632
873
  @click.option("--stderr", is_flag=True, default=False)
633
874
  def save_parameter(
634
- ctx,
875
+ wf: Workflow,
635
876
  name: str,
636
877
  value: str,
637
878
  ear_id: int,
@@ -643,9 +884,8 @@ def _make_internal_CLI(app):
643
884
  f"{cmd_idx!r} (stderr={stderr!r})"
644
885
  )
645
886
  app.CLI_logger.debug(f"save parameter value is: {value!r}")
646
- wk = ctx.obj["workflow"]
647
- with wk._store.cached_load():
648
- value = wk.process_shell_parameter_output(
887
+ with wf._store.cached_load():
888
+ value = wf.process_shell_parameter_output(
649
889
  name=name,
650
890
  value=value,
651
891
  EAR_ID=ear_id,
@@ -653,63 +893,7 @@ def _make_internal_CLI(app):
653
893
  stderr=stderr,
654
894
  )
655
895
  app.CLI_logger.debug(f"save parameter processed value is: {value!r}")
656
- ctx.exit(wk.save_parameter(name=name, value=value, EAR_ID=ear_id))
657
-
658
- @workflow.command()
659
- @click.pass_context
660
- @click.argument("ear_id", type=click.INT)
661
- def set_EAR_start(ctx, ear_id: int):
662
- app.CLI_logger.info(f"set EAR start for EAR ID {ear_id!r}.")
663
- ctx.exit(ctx.obj["workflow"].set_EAR_start(ear_id))
664
-
665
- @workflow.command()
666
- @click.pass_context
667
- @click.argument("js_idx", type=click.INT)
668
- @click.argument("js_act_idx", type=click.INT)
669
- @click.argument("ear_id", type=click.INT)
670
- @click.argument("exit_code", type=click.INT)
671
- def set_EAR_end(
672
- ctx,
673
- js_idx: int,
674
- js_act_idx: int,
675
- ear_id: int,
676
- exit_code: int,
677
- ):
678
- app.CLI_logger.info(
679
- f"set EAR end for EAR ID {ear_id!r} with exit code {exit_code!r}."
680
- )
681
- ctx.exit(
682
- ctx.obj["workflow"].set_EAR_end(
683
- js_idx=js_idx,
684
- js_act_idx=js_act_idx,
685
- EAR_ID=ear_id,
686
- exit_code=exit_code,
687
- )
688
- )
689
-
690
- @workflow.command()
691
- @click.pass_context
692
- @click.argument("ear_id", type=click.INT)
693
- def set_EAR_skip(ctx, ear_id: int):
694
- app.CLI_logger.info(f"set EAR skip for EAR ID {ear_id!r}.")
695
- ctx.exit(ctx.obj["workflow"].set_EAR_skip(ear_id))
696
-
697
- @workflow.command()
698
- @click.pass_context
699
- @click.argument("ear_id", type=click.INT)
700
- def get_EAR_skipped(ctx, ear_id: int):
701
- """Return 1 if the given EAR is to be skipped, else return 0."""
702
- app.CLI_logger.info(f"get EAR skip for EAR ID {ear_id!r}.")
703
- click.echo(int(ctx.obj["workflow"].get_EAR_skipped(ear_id)))
704
-
705
- @workflow.command()
706
- @click.pass_context
707
- @click.argument("loop_name", type=click.STRING)
708
- @click.argument("ear_id", type=click.INT)
709
- def check_loop(ctx, loop_name: str, ear_id: int):
710
- """Check if an iteration has met its loop's termination condition."""
711
- app.CLI_logger.info(f"check_loop for loop {loop_name!r} and EAR ID {ear_id!r}.")
712
- ctx.exit(ctx.obj["workflow"].check_loop_termination(loop_name, ear_id))
896
+ wf.save_parameter(name=name, value=value, EAR_ID=ear_id)
713
897
 
714
898
  # TODO: in general, maybe the workflow command group can expose the simple Workflow
715
899
  # properties; maybe use a decorator on the Workflow property object to signify
@@ -718,17 +902,17 @@ def _make_internal_CLI(app):
718
902
  return internal
719
903
 
720
904
 
721
- def _make_template_components_CLI(app):
905
+ def _make_template_components_CLI(app: BaseApp):
722
906
  @click.command()
723
- def tc(help=True):
907
+ def tc(help: bool = True):
724
908
  """For showing template component data."""
725
909
  pprint(app.template_components)
726
910
 
727
911
  return tc
728
912
 
729
913
 
730
- def _make_show_CLI(app):
731
- def show_legend_callback(ctx, param, value):
914
+ def _make_show_CLI(app: BaseApp):
915
+ def show_legend_callback(ctx: click.Context, param, value: bool):
732
916
  if not value or ctx.resilient_parsing:
733
917
  return
734
918
  app.show_legend()
@@ -765,14 +949,14 @@ def _make_show_CLI(app):
765
949
  expose_value=False,
766
950
  callback=show_legend_callback,
767
951
  )
768
- def show(max_recent, full, no_update):
952
+ def show(max_recent: int, full: bool, no_update: bool):
769
953
  """Show information about running and recently active workflows."""
770
954
  app.show(max_recent=max_recent, full=full, no_update=no_update)
771
955
 
772
956
  return show
773
957
 
774
958
 
775
- def _make_zip_CLI(app):
959
+ def _make_zip_CLI(app: BaseApp):
776
960
  @click.command(name="zip")
777
961
  @click.argument("workflow_ref")
778
962
  @zip_path_opt
@@ -782,13 +966,13 @@ def _make_zip_CLI(app):
782
966
  @zip_include_rechunk_backups_opt
783
967
  @workflow_ref_type_opt
784
968
  def zip_workflow(
785
- workflow_ref,
786
- path,
787
- overwrite,
788
- log,
789
- include_execute,
790
- include_rechunk_backups,
791
- ref_type,
969
+ workflow_ref: str,
970
+ path: str,
971
+ overwrite: bool,
972
+ log: str | None,
973
+ include_execute: bool,
974
+ include_rechunk_backups: bool,
975
+ ref_type: str | None,
792
976
  ):
793
977
  """Generate a copy of the specified workflow in the zip file format in the
794
978
  current working directory.
@@ -811,12 +995,12 @@ def _make_zip_CLI(app):
811
995
  return zip_workflow
812
996
 
813
997
 
814
- def _make_unzip_CLI(app):
998
+ def _make_unzip_CLI(app: BaseApp):
815
999
  @click.command(name="unzip")
816
1000
  @click.argument("workflow_path")
817
1001
  @unzip_path_opt
818
1002
  @unzip_log_opt
819
- def unzip_workflow(workflow_path, path, log):
1003
+ def unzip_workflow(workflow_path: str, path: str, log: str | None):
820
1004
  """Generate a copy of the specified zipped workflow in the submittable Zarr
821
1005
  format in the current working directory.
822
1006
 
@@ -829,30 +1013,37 @@ def _make_unzip_CLI(app):
829
1013
  return unzip_workflow
830
1014
 
831
1015
 
832
- def _make_cancel_CLI(app):
1016
+ def _make_cancel_CLI(app: BaseApp):
833
1017
  @click.command()
834
1018
  @click.argument("workflow_ref")
835
1019
  @workflow_ref_type_opt
836
- def cancel(workflow_ref, ref_type):
1020
+ @cancel_status_opt
1021
+ def cancel(workflow_ref: str, ref_type: str | None, status: bool):
837
1022
  """Stop all running jobscripts of the specified workflow.
838
1023
 
839
1024
  WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
840
1025
  workflow path.
841
1026
 
842
1027
  """
843
- app.cancel(workflow_ref, ref_type)
1028
+ app.cancel(workflow_ref=workflow_ref, ref_is_path=ref_type, status=status)
844
1029
 
845
1030
  return cancel
846
1031
 
847
1032
 
848
- def _make_rechunk_CLI(app):
1033
+ def _make_rechunk_CLI(app: BaseApp):
849
1034
  @click.command(name="rechunk")
850
1035
  @click.argument("workflow_ref")
851
1036
  @workflow_ref_type_opt
852
1037
  @rechunk_backup_opt
853
1038
  @rechunk_chunk_size_opt
854
1039
  @rechunk_status_opt
855
- def rechunk(workflow_ref, ref_type, backup, chunk_size, status):
1040
+ def rechunk(
1041
+ workflow_ref: str,
1042
+ ref_type: str | None,
1043
+ backup: bool,
1044
+ chunk_size: int,
1045
+ status: bool,
1046
+ ):
856
1047
  """Rechunk metadata/runs and parameters/base arrays.
857
1048
 
858
1049
  WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
@@ -866,7 +1057,7 @@ def _make_rechunk_CLI(app):
866
1057
  return rechunk
867
1058
 
868
1059
 
869
- def _make_open_CLI(app):
1060
+ def _make_open_CLI(app: BaseApp):
870
1061
  @click.group(name="open")
871
1062
  def open_file():
872
1063
  """Open a file (for example {app_name}'s log file) using the default
@@ -874,9 +1065,9 @@ def _make_open_CLI(app):
874
1065
 
875
1066
  @open_file.command()
876
1067
  @click.option("--path", is_flag=True, default=False)
877
- def log(path=False):
1068
+ def log(path: bool = False):
878
1069
  """Open the {app_name} log file."""
879
- file_path = app.config.get("log_file_path")
1070
+ file_path = app.config.log_file_path
880
1071
  if path:
881
1072
  click.echo(file_path)
882
1073
  else:
@@ -884,9 +1075,9 @@ def _make_open_CLI(app):
884
1075
 
885
1076
  @open_file.command()
886
1077
  @click.option("--path", is_flag=True, default=False)
887
- def config(path=False):
1078
+ def config(path: bool = False):
888
1079
  """Open the {app_name} config file, or retrieve it's path."""
889
- file_path = app.config.get("config_file_path")
1080
+ file_path = app.config.config_file_path
890
1081
  if path:
891
1082
  click.echo(file_path)
892
1083
  else:
@@ -895,34 +1086,30 @@ def _make_open_CLI(app):
895
1086
  @open_file.command()
896
1087
  @click.option("--name")
897
1088
  @click.option("--path", is_flag=True, default=False)
898
- def env_source(name=None, path=False):
1089
+ def env_source(name: str | None = None, path: bool = False):
899
1090
  """Open a named environment sources file, or the first one."""
900
- sources = app.config.get("environment_sources")
901
- if not sources:
1091
+ if not (sources := app.config.environment_sources):
902
1092
  raise ValueError("No environment sources specified in the config file.")
903
- file_paths = []
904
1093
  if not name:
905
1094
  file_paths = [sources[0]]
906
1095
  else:
907
- for i in sources:
908
- if i.name == name:
909
- file_paths.append(i)
1096
+ file_paths = [pth for pth in sources if pth.name == name]
910
1097
  if not file_paths:
911
1098
  raise ValueError(
912
1099
  f"No environment source named {name!r} could be found; available "
913
- f"environment source files have names: {[i.name for i in sources]!r}"
1100
+ f"environment source files have names: {[pth.name for pth in sources]!r}"
914
1101
  )
915
1102
 
916
1103
  assert len(file_paths) < 5 # don't open a stupid number of files
917
- for i in file_paths:
1104
+ for pth in file_paths:
918
1105
  if path:
919
- click.echo(i)
1106
+ click.echo(pth)
920
1107
  else:
921
- utils.open_file(i)
1108
+ utils.open_file(pth)
922
1109
 
923
1110
  @open_file.command()
924
1111
  @click.option("--path", is_flag=True, default=False)
925
- def known_subs(path=False):
1112
+ def known_subs(path: bool = False):
926
1113
  """Open the known-submissions text file."""
927
1114
  file_path = app.known_subs_file_path
928
1115
  if path:
@@ -934,7 +1121,7 @@ def _make_open_CLI(app):
934
1121
  @click.argument("workflow_ref")
935
1122
  @click.option("--path", is_flag=True, default=False)
936
1123
  @workflow_ref_type_opt
937
- def workflow(workflow_ref, ref_type, path=False):
1124
+ def workflow(workflow_ref: str, ref_type: str | None, path: bool = False):
938
1125
  """Open a workflow directory using, for example, File Explorer on Windows."""
939
1126
  workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
940
1127
  if path:
@@ -944,7 +1131,7 @@ def _make_open_CLI(app):
944
1131
 
945
1132
  @open_file.command()
946
1133
  @click.option("--path", is_flag=True, default=False)
947
- def user_data_dir(path=False):
1134
+ def user_data_dir(path: bool = False):
948
1135
  dir_path = app._ensure_user_data_dir()
949
1136
  if path:
950
1137
  click.echo(dir_path)
@@ -953,7 +1140,7 @@ def _make_open_CLI(app):
953
1140
 
954
1141
  @open_file.command()
955
1142
  @click.option("--path", is_flag=True, default=False)
956
- def user_cache_dir(path=False):
1143
+ def user_cache_dir(path: bool = False):
957
1144
  dir_path = app._ensure_user_cache_dir()
958
1145
  if path:
959
1146
  click.echo(dir_path)
@@ -962,7 +1149,7 @@ def _make_open_CLI(app):
962
1149
 
963
1150
  @open_file.command()
964
1151
  @click.option("--path", is_flag=True, default=False)
965
- def user_runtime_dir(path=False):
1152
+ def user_runtime_dir(path: bool = False):
966
1153
  dir_path = app._ensure_user_runtime_dir()
967
1154
  if path:
968
1155
  click.echo(dir_path)
@@ -971,7 +1158,7 @@ def _make_open_CLI(app):
971
1158
 
972
1159
  @open_file.command()
973
1160
  @click.option("--path", is_flag=True, default=False)
974
- def user_data_hostname_dir(path=False):
1161
+ def user_data_hostname_dir(path: bool = False):
975
1162
  dir_path = app._ensure_user_data_hostname_dir()
976
1163
  if path:
977
1164
  click.echo(dir_path)
@@ -980,7 +1167,7 @@ def _make_open_CLI(app):
980
1167
 
981
1168
  @open_file.command()
982
1169
  @click.option("--path", is_flag=True, default=False)
983
- def user_cache_hostname_dir(path=False):
1170
+ def user_cache_hostname_dir(path: bool = False):
984
1171
  dir_path = app._ensure_user_cache_hostname_dir()
985
1172
  if path:
986
1173
  click.echo(dir_path)
@@ -989,32 +1176,31 @@ def _make_open_CLI(app):
989
1176
 
990
1177
  @open_file.command()
991
1178
  @click.option("--path", is_flag=True, default=False)
992
- def demo_data_cache_dir(path=False):
1179
+ def demo_data_cache_dir(path: bool = False):
993
1180
  dir_path = app._ensure_demo_data_cache_dir()
994
1181
  if path:
995
1182
  click.echo(dir_path)
996
1183
  else:
997
1184
  utils.open_file(dir_path)
998
1185
 
999
- open_file.help = open_file.help.format(app_name=app.name)
1000
- log.help = log.help.format(app_name=app.name)
1001
- config.help = config.help.format(app_name=app.name)
1002
-
1186
+ _set_help_name(open_file, app)
1187
+ _set_help_name(log, app)
1188
+ _set_help_name(config, app)
1003
1189
  return open_file
1004
1190
 
1005
1191
 
1006
- def _make_demo_data_CLI(app):
1192
+ def _make_demo_data_CLI(app: BaseApp):
1007
1193
  """Generate the CLI for interacting with example data files that are used in demo
1008
1194
  workflows."""
1009
1195
 
1010
- def list_callback(ctx, param, value):
1196
+ def list_callback(ctx: click.Context, param, value: bool):
1011
1197
  if not value or ctx.resilient_parsing:
1012
1198
  return
1013
1199
  # TODO: format with Rich with a one-line description
1014
1200
  click.echo("\n".join(app.list_demo_data_files()))
1015
1201
  ctx.exit()
1016
1202
 
1017
- def cache_all_callback(ctx, param, value):
1203
+ def cache_all_callback(ctx: click.Context, param, value: bool):
1018
1204
  if not value or ctx.resilient_parsing:
1019
1205
  return
1020
1206
  app.cache_all_demo_data_files()
@@ -1036,7 +1222,7 @@ def _make_demo_data_CLI(app):
1036
1222
  @demo_data.command("copy")
1037
1223
  @click.argument("file_name")
1038
1224
  @click.argument("destination")
1039
- def copy_demo_data(file_name, destination):
1225
+ def copy_demo_data(file_name: str, destination: str):
1040
1226
  """Copy a demo data file to the specified location."""
1041
1227
  app.copy_demo_data(file_name=file_name, dst=destination)
1042
1228
 
@@ -1050,14 +1236,14 @@ def _make_demo_data_CLI(app):
1050
1236
  callback=cache_all_callback,
1051
1237
  )
1052
1238
  @click.argument("file_name")
1053
- def cache_demo_data(file_name):
1239
+ def cache_demo_data(file_name: str):
1054
1240
  """Ensure a demo data file is in the demo data cache."""
1055
1241
  app.cache_demo_data_file(file_name)
1056
1242
 
1057
1243
  return demo_data
1058
1244
 
1059
1245
 
1060
- def _make_manage_CLI(app):
1246
+ def _make_manage_CLI(app: BaseApp):
1061
1247
  """Generate the CLI for infrequent app management tasks."""
1062
1248
 
1063
1249
  @click.group()
@@ -1074,7 +1260,7 @@ def _make_manage_CLI(app):
1074
1260
  "--config-dir",
1075
1261
  help="The directory containing the config file to be reset.",
1076
1262
  )
1077
- def reset_config(config_dir):
1263
+ def reset_config(config_dir: str):
1078
1264
  """Reset the configuration file to defaults.
1079
1265
 
1080
1266
  This can be used if the current configuration file is invalid."""
@@ -1085,7 +1271,7 @@ def _make_manage_CLI(app):
1085
1271
  "--config-dir",
1086
1272
  help="The directory containing the config file whose path is to be returned.",
1087
1273
  )
1088
- def get_config_path(config_dir):
1274
+ def get_config_path(config_dir: str):
1089
1275
  """Print the config file path without loading the config.
1090
1276
 
1091
1277
  This can be used instead of `{app_name} open config --path` if the config file
@@ -1106,7 +1292,7 @@ def _make_manage_CLI(app):
1106
1292
 
1107
1293
  @manage.command("clear-cache")
1108
1294
  @click.option("--hostname", is_flag=True, default=False)
1109
- def clear_cache(hostname):
1295
+ def clear_cache(hostname: bool):
1110
1296
  """Delete the app cache directory."""
1111
1297
  if hostname:
1112
1298
  app.clear_user_cache_hostname_dir()
@@ -1121,12 +1307,12 @@ def _make_manage_CLI(app):
1121
1307
  return manage
1122
1308
 
1123
1309
 
1124
- def make_cli(app):
1310
+ def make_cli(app: BaseApp):
1125
1311
  """Generate the root CLI for the app."""
1126
1312
 
1127
1313
  colorama_init(autoreset=True)
1128
1314
 
1129
- def run_time_info_callback(ctx, param, value):
1315
+ def run_time_info_callback(ctx: click.Context, param, value: bool):
1130
1316
  app.run_time_info.from_CLI = True
1131
1317
  if not value or ctx.resilient_parsing:
1132
1318
  return
@@ -1180,8 +1366,25 @@ def make_cli(app):
1180
1366
  "`TimeIt.decorator` are included."
1181
1367
  ),
1182
1368
  )
1369
+ @click.option(
1370
+ "--std-stream",
1371
+ help="File to redirect standard output and error to, and to print exceptions to.",
1372
+ )
1183
1373
  @click.pass_context
1184
- def new_CLI(ctx, config_dir, config_key, with_config, timeit, timeit_file):
1374
+ def new_CLI(
1375
+ ctx: click.Context,
1376
+ config_dir,
1377
+ config_key,
1378
+ with_config,
1379
+ timeit,
1380
+ timeit_file,
1381
+ std_stream: str,
1382
+ ):
1383
+ ctx.ensure_object(dict)
1384
+
1385
+ if std_stream:
1386
+ ctx.with_resource(redirect_std_to_file_click(std_stream))
1387
+
1185
1388
  app.run_time_info.from_CLI = True
1186
1389
  TimeIt.active = timeit or timeit_file
1187
1390
  TimeIt.file_path = timeit_file
@@ -1208,7 +1411,12 @@ def make_cli(app):
1208
1411
  @click.option("--use-current-env", is_flag=True, default=False)
1209
1412
  @click.option("--setup", type=click.STRING)
1210
1413
  @click.option("--env-source-file", type=click.STRING)
1211
- def configure_env(name, use_current_env, setup=None, env_source_file=None):
1414
+ def configure_env(
1415
+ name: str,
1416
+ use_current_env: bool,
1417
+ setup: list[str] | None = None,
1418
+ env_source_file: str | None = None,
1419
+ ):
1212
1420
  """Configure an app environment, using, for example, the currently activated
1213
1421
  Python environment."""
1214
1422
  app.configure_env(
@@ -1216,9 +1424,11 @@ def make_cli(app):
1216
1424
  setup=setup,
1217
1425
  executables=None,
1218
1426
  use_current_env=use_current_env,
1219
- env_source_file=env_source_file,
1427
+ env_source_file=None if env_source_file is None else Path(env_source_file),
1220
1428
  )
1221
1429
 
1430
+ new_CLI.context_class = ErrorPropagatingClickContext
1431
+
1222
1432
  new_CLI.__doc__ = app.description
1223
1433
  new_CLI.add_command(get_config_CLI(app))
1224
1434
  new_CLI.add_command(get_demo_software_CLI(app))