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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. hpcflow/__init__.py +2 -11
  2. hpcflow/__pyinstaller/__init__.py +5 -0
  3. hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
  4. hpcflow/_version.py +1 -1
  5. hpcflow/app.py +43 -0
  6. hpcflow/cli.py +2 -461
  7. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  8. hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
  9. hpcflow/data/jinja_templates/test/test_template.txt +8 -0
  10. hpcflow/data/programs/hello_world/README.md +1 -0
  11. hpcflow/data/programs/hello_world/hello_world.c +87 -0
  12. hpcflow/data/programs/hello_world/linux/hello_world +0 -0
  13. hpcflow/data/programs/hello_world/macos/hello_world +0 -0
  14. hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
  15. hpcflow/data/scripts/__init__.py +1 -0
  16. hpcflow/data/scripts/bad_script.py +2 -0
  17. hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
  18. hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
  19. hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
  20. hpcflow/data/scripts/do_nothing.py +2 -0
  21. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  22. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  23. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  24. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  25. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  26. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  27. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  28. hpcflow/data/scripts/generate_t1_file_01.py +7 -0
  29. hpcflow/data/scripts/import_future_script.py +7 -0
  30. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  31. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  32. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  33. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  34. hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
  35. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  36. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  37. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  38. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  39. hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
  40. hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
  41. hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
  42. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  43. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  44. hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
  45. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
  46. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  47. hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
  48. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
  49. hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
  50. hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
  51. hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
  52. hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
  53. hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
  54. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  55. hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
  56. hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
  57. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  58. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  59. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  60. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  61. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  62. hpcflow/data/scripts/parse_t1_file_01.py +4 -0
  63. hpcflow/data/scripts/script_exit_test.py +5 -0
  64. hpcflow/data/template_components/__init__.py +1 -0
  65. hpcflow/data/template_components/command_files.yaml +26 -0
  66. hpcflow/data/template_components/environments.yaml +13 -0
  67. hpcflow/data/template_components/parameters.yaml +14 -0
  68. hpcflow/data/template_components/task_schemas.yaml +139 -0
  69. hpcflow/data/workflows/workflow_1.yaml +5 -0
  70. hpcflow/examples.ipynb +1037 -0
  71. hpcflow/sdk/__init__.py +149 -0
  72. hpcflow/sdk/app.py +4266 -0
  73. hpcflow/sdk/cli.py +1479 -0
  74. hpcflow/sdk/cli_common.py +385 -0
  75. hpcflow/sdk/config/__init__.py +5 -0
  76. hpcflow/sdk/config/callbacks.py +246 -0
  77. hpcflow/sdk/config/cli.py +388 -0
  78. hpcflow/sdk/config/config.py +1410 -0
  79. hpcflow/sdk/config/config_file.py +501 -0
  80. hpcflow/sdk/config/errors.py +272 -0
  81. hpcflow/sdk/config/types.py +150 -0
  82. hpcflow/sdk/core/__init__.py +38 -0
  83. hpcflow/sdk/core/actions.py +3857 -0
  84. hpcflow/sdk/core/app_aware.py +25 -0
  85. hpcflow/sdk/core/cache.py +224 -0
  86. hpcflow/sdk/core/command_files.py +814 -0
  87. hpcflow/sdk/core/commands.py +424 -0
  88. hpcflow/sdk/core/element.py +2071 -0
  89. hpcflow/sdk/core/enums.py +221 -0
  90. hpcflow/sdk/core/environment.py +256 -0
  91. hpcflow/sdk/core/errors.py +1043 -0
  92. hpcflow/sdk/core/execute.py +207 -0
  93. hpcflow/sdk/core/json_like.py +809 -0
  94. hpcflow/sdk/core/loop.py +1320 -0
  95. hpcflow/sdk/core/loop_cache.py +282 -0
  96. hpcflow/sdk/core/object_list.py +933 -0
  97. hpcflow/sdk/core/parameters.py +3371 -0
  98. hpcflow/sdk/core/rule.py +196 -0
  99. hpcflow/sdk/core/run_dir_files.py +57 -0
  100. hpcflow/sdk/core/skip_reason.py +7 -0
  101. hpcflow/sdk/core/task.py +3792 -0
  102. hpcflow/sdk/core/task_schema.py +993 -0
  103. hpcflow/sdk/core/test_utils.py +538 -0
  104. hpcflow/sdk/core/types.py +447 -0
  105. hpcflow/sdk/core/utils.py +1207 -0
  106. hpcflow/sdk/core/validation.py +87 -0
  107. hpcflow/sdk/core/values.py +477 -0
  108. hpcflow/sdk/core/workflow.py +4820 -0
  109. hpcflow/sdk/core/zarr_io.py +206 -0
  110. hpcflow/sdk/data/__init__.py +13 -0
  111. hpcflow/sdk/data/config_file_schema.yaml +34 -0
  112. hpcflow/sdk/data/config_schema.yaml +260 -0
  113. hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
  114. hpcflow/sdk/data/files_spec_schema.yaml +5 -0
  115. hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
  116. hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
  117. hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
  118. hpcflow/sdk/demo/__init__.py +3 -0
  119. hpcflow/sdk/demo/cli.py +242 -0
  120. hpcflow/sdk/helper/__init__.py +3 -0
  121. hpcflow/sdk/helper/cli.py +137 -0
  122. hpcflow/sdk/helper/helper.py +300 -0
  123. hpcflow/sdk/helper/watcher.py +192 -0
  124. hpcflow/sdk/log.py +288 -0
  125. hpcflow/sdk/persistence/__init__.py +18 -0
  126. hpcflow/sdk/persistence/base.py +2817 -0
  127. hpcflow/sdk/persistence/defaults.py +6 -0
  128. hpcflow/sdk/persistence/discovery.py +39 -0
  129. hpcflow/sdk/persistence/json.py +954 -0
  130. hpcflow/sdk/persistence/pending.py +948 -0
  131. hpcflow/sdk/persistence/store_resource.py +203 -0
  132. hpcflow/sdk/persistence/types.py +309 -0
  133. hpcflow/sdk/persistence/utils.py +73 -0
  134. hpcflow/sdk/persistence/zarr.py +2388 -0
  135. hpcflow/sdk/runtime.py +320 -0
  136. hpcflow/sdk/submission/__init__.py +3 -0
  137. hpcflow/sdk/submission/enums.py +70 -0
  138. hpcflow/sdk/submission/jobscript.py +2379 -0
  139. hpcflow/sdk/submission/schedulers/__init__.py +281 -0
  140. hpcflow/sdk/submission/schedulers/direct.py +233 -0
  141. hpcflow/sdk/submission/schedulers/sge.py +376 -0
  142. hpcflow/sdk/submission/schedulers/slurm.py +598 -0
  143. hpcflow/sdk/submission/schedulers/utils.py +25 -0
  144. hpcflow/sdk/submission/shells/__init__.py +52 -0
  145. hpcflow/sdk/submission/shells/base.py +229 -0
  146. hpcflow/sdk/submission/shells/bash.py +504 -0
  147. hpcflow/sdk/submission/shells/os_version.py +115 -0
  148. hpcflow/sdk/submission/shells/powershell.py +352 -0
  149. hpcflow/sdk/submission/submission.py +1402 -0
  150. hpcflow/sdk/submission/types.py +140 -0
  151. hpcflow/sdk/typing.py +194 -0
  152. hpcflow/sdk/utils/arrays.py +69 -0
  153. hpcflow/sdk/utils/deferred_file.py +55 -0
  154. hpcflow/sdk/utils/hashing.py +16 -0
  155. hpcflow/sdk/utils/patches.py +31 -0
  156. hpcflow/sdk/utils/strings.py +69 -0
  157. hpcflow/tests/api/test_api.py +32 -0
  158. hpcflow/tests/conftest.py +123 -0
  159. hpcflow/tests/data/__init__.py +0 -0
  160. hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
  161. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  162. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  163. hpcflow/tests/data/workflow_1.json +10 -0
  164. hpcflow/tests/data/workflow_1.yaml +5 -0
  165. hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
  166. hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
  167. hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
  168. hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
  169. hpcflow/tests/programs/test_programs.py +180 -0
  170. hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
  171. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  172. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
  173. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  174. hpcflow/tests/scripts/test_main_scripts.py +1361 -0
  175. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  176. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  177. hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
  178. hpcflow/tests/unit/test_action.py +1066 -0
  179. hpcflow/tests/unit/test_action_rule.py +24 -0
  180. hpcflow/tests/unit/test_app.py +132 -0
  181. hpcflow/tests/unit/test_cache.py +46 -0
  182. hpcflow/tests/unit/test_cli.py +172 -0
  183. hpcflow/tests/unit/test_command.py +377 -0
  184. hpcflow/tests/unit/test_config.py +195 -0
  185. hpcflow/tests/unit/test_config_file.py +162 -0
  186. hpcflow/tests/unit/test_element.py +666 -0
  187. hpcflow/tests/unit/test_element_iteration.py +88 -0
  188. hpcflow/tests/unit/test_element_set.py +158 -0
  189. hpcflow/tests/unit/test_group.py +115 -0
  190. hpcflow/tests/unit/test_input_source.py +1479 -0
  191. hpcflow/tests/unit/test_input_value.py +398 -0
  192. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  193. hpcflow/tests/unit/test_json_like.py +1247 -0
  194. hpcflow/tests/unit/test_loop.py +2674 -0
  195. hpcflow/tests/unit/test_meta_task.py +325 -0
  196. hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
  197. hpcflow/tests/unit/test_object_list.py +116 -0
  198. hpcflow/tests/unit/test_parameter.py +243 -0
  199. hpcflow/tests/unit/test_persistence.py +664 -0
  200. hpcflow/tests/unit/test_resources.py +243 -0
  201. hpcflow/tests/unit/test_run.py +286 -0
  202. hpcflow/tests/unit/test_run_directories.py +29 -0
  203. hpcflow/tests/unit/test_runtime.py +9 -0
  204. hpcflow/tests/unit/test_schema_input.py +372 -0
  205. hpcflow/tests/unit/test_shell.py +129 -0
  206. hpcflow/tests/unit/test_slurm.py +39 -0
  207. hpcflow/tests/unit/test_submission.py +502 -0
  208. hpcflow/tests/unit/test_task.py +2560 -0
  209. hpcflow/tests/unit/test_task_schema.py +182 -0
  210. hpcflow/tests/unit/test_utils.py +616 -0
  211. hpcflow/tests/unit/test_value_sequence.py +549 -0
  212. hpcflow/tests/unit/test_values.py +91 -0
  213. hpcflow/tests/unit/test_workflow.py +827 -0
  214. hpcflow/tests/unit/test_workflow_template.py +186 -0
  215. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  216. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  217. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  218. hpcflow/tests/unit/utils/test_patches.py +5 -0
  219. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  220. hpcflow/tests/unit/utils/test_strings.py +97 -0
  221. hpcflow/tests/workflows/__init__.py +0 -0
  222. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  223. hpcflow/tests/workflows/test_jobscript.py +355 -0
  224. hpcflow/tests/workflows/test_run_status.py +198 -0
  225. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  226. hpcflow/tests/workflows/test_submission.py +140 -0
  227. hpcflow/tests/workflows/test_workflows.py +564 -0
  228. hpcflow/tests/workflows/test_zip.py +18 -0
  229. hpcflow/viz_demo.ipynb +6794 -0
  230. hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
  231. hpcflow-0.2.0a271.dist-info/METADATA +65 -0
  232. hpcflow-0.2.0a271.dist-info/RECORD +237 -0
  233. {hpcflow-0.1.15.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
  234. hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
  235. hpcflow/api.py +0 -490
  236. hpcflow/archive/archive.py +0 -307
  237. hpcflow/archive/cloud/cloud.py +0 -45
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -427
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -233
  243. hpcflow/copytree.py +0 -66
  244. hpcflow/data/examples/_config.yml +0 -14
  245. hpcflow/data/examples/damask/demo/1.run.yml +0 -4
  246. hpcflow/data/examples/damask/demo/2.process.yml +0 -29
  247. hpcflow/data/examples/damask/demo/geom.geom +0 -2052
  248. hpcflow/data/examples/damask/demo/load.load +0 -1
  249. hpcflow/data/examples/damask/demo/material.config +0 -185
  250. hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
  251. hpcflow/data/examples/damask/inputs/load.load +0 -1
  252. hpcflow/data/examples/damask/inputs/material.config +0 -185
  253. hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
  254. hpcflow/data/examples/damask/profiles/damask.yml +0 -4
  255. hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
  256. hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
  257. hpcflow/data/examples/damask/profiles/default.yml +0 -6
  258. hpcflow/data/examples/thinking.yml +0 -177
  259. hpcflow/errors.py +0 -2
  260. hpcflow/init_db.py +0 -37
  261. hpcflow/models.py +0 -2595
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -322
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -166
  268. hpcflow/variables.py +0 -543
  269. hpcflow-0.1.15.dist-info/METADATA +0 -168
  270. hpcflow-0.1.15.dist-info/RECORD +0 -45
  271. hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.15.dist-info/top_level.txt +0 -1
  273. /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
  274. /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
  275. /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
hpcflow/sdk/cli.py ADDED
@@ -0,0 +1,1479 @@
1
+ """
2
+ Command line interface implementation.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import contextlib
7
+ import datetime
8
+ import json
9
+ import os
10
+ import time
11
+ import click
12
+ from colorama import init as colorama_init
13
+ from termcolor import colored # type: ignore
14
+ from typing import TYPE_CHECKING
15
+ from rich.pretty import pprint
16
+
17
+ from hpcflow import __version__, _app_name
18
+ from hpcflow.sdk.config.cli import get_config_CLI
19
+ from hpcflow.sdk.config.errors import ConfigError
20
+ from hpcflow.sdk.core import utils
21
+ from hpcflow.sdk.demo.cli import get_demo_software_CLI, get_demo_workflow_CLI
22
+ from hpcflow.sdk.cli_common import (
23
+ format_option,
24
+ path_option,
25
+ name_option,
26
+ name_timestamp_option,
27
+ name_dir_option,
28
+ overwrite_option,
29
+ store_option,
30
+ ts_fmt_option,
31
+ ts_name_fmt_option,
32
+ variables_option,
33
+ js_parallelism_option,
34
+ wait_option,
35
+ add_to_known_opt,
36
+ print_idx_opt,
37
+ tasks_opt,
38
+ cancel_opt,
39
+ submit_status_opt,
40
+ force_arr_opt,
41
+ make_status_opt,
42
+ add_sub_opt,
43
+ zip_path_opt,
44
+ zip_overwrite_opt,
45
+ zip_log_opt,
46
+ zip_include_execute_opt,
47
+ zip_include_rechunk_backups_opt,
48
+ unzip_path_opt,
49
+ unzip_log_opt,
50
+ rechunk_backup_opt,
51
+ rechunk_chunk_size_opt,
52
+ rechunk_status_opt,
53
+ cancel_status_opt,
54
+ list_js_max_js_opt,
55
+ list_js_jobscripts_opt,
56
+ list_task_js_max_js_opt,
57
+ list_task_js_task_names_opt,
58
+ list_js_width_opt,
59
+ jobscript_std_array_idx_opt,
60
+ _add_doc_from_help,
61
+ )
62
+ from hpcflow.sdk.helper.cli import get_helper_CLI
63
+ from hpcflow.sdk.log import TimeIt
64
+ from hpcflow.sdk.core.workflow import Workflow
65
+ from hpcflow.sdk.submission.shells import ALL_SHELLS
66
+ from hpcflow.sdk.submission.jobscript import Jobscript
67
+ from hpcflow.sdk.submission.submission import Submission
68
+ from hpcflow.sdk.submission.schedulers.sge import SGEPosix
69
+
70
+ if TYPE_CHECKING:
71
+ from pathlib import Path
72
+ from typing import Literal
73
+ from .app import BaseApp
74
+
75
+ #: Standard option
76
+ string_option = click.option(
77
+ "--string",
78
+ is_flag=True,
79
+ default=False,
80
+ help="Determines if passing a file path or a string.",
81
+ )
82
+ #: Standard option
83
+ workflow_ref_type_opt = click.option(
84
+ "--ref-type",
85
+ "-r",
86
+ type=click.Choice(("assume-id", "id", "path")),
87
+ default="assume-id",
88
+ help="How to interpret a reference, as an ID, a path, or to guess.",
89
+ )
90
+
91
+ #: Get the current workflow from the context.
92
+ _pass_workflow = click.make_pass_decorator(Workflow)
93
+ #: Get the current submission from the context.
94
+ _pass_submission = click.make_pass_decorator(Submission)
95
+ #: Get the current jobscript from the context.
96
+ _pass_js = click.make_pass_decorator(Jobscript)
97
+
98
+ _add_doc_from_help(string_option, workflow_ref_type_opt)
99
+
100
+
101
+ class ErrorPropagatingClickContext(click.Context):
102
+ """A click Context class that passes on exception information.
103
+
104
+ Using the standard `click.Context` class, exceptions raised when using a resource specified
105
+ with `ctx.with_resource(my_ctx_manager())` are not passed on to the `__exit__` method of
106
+ `my_ctx_manager`. See: https://github.com/pallets/click/issues/2447.
107
+
108
+ Examples
109
+ --------
110
+ >>> @click.group()
111
+ ... @click.pass_context
112
+ ... def cli(ctx):
113
+ ... ctx.with_resource(my_context_manager())
114
+ ... cli.context_class = ErrorPropagatingClickContext
115
+
116
+ """
117
+
118
+ def __exit__(self, exc_type, exc_value, tb):
119
+ self._depth -= 1
120
+ if self._depth == 0:
121
+ self._exit_stack.__exit__(exc_type, exc_value, tb)
122
+ self._exit_stack = contextlib.ExitStack()
123
+ click.core.pop_context()
124
+
125
+
126
+ @contextlib.contextmanager
127
+ def redirect_std_to_file_click(file, mode: str = "a"):
128
+ def ignore(exc):
129
+ """Do not intercept Click's `Exit` exception when the exit code is zero."""
130
+ if type(exc) is click.exceptions.Exit:
131
+ if exc.exit_code == 0:
132
+ return True
133
+ return exc.exit_code
134
+ return 1 # default exit code
135
+
136
+ with utils.redirect_std_to_file(file=file, ignore=ignore):
137
+ yield
138
+
139
+
140
+ def parse_jobscript_wait_spec(jobscripts: str) -> dict[int, list[int]]:
141
+ """
142
+ Parse a jobscript wait specification.
143
+ """
144
+ sub_js_idx_dct = {}
145
+ for sub_i in jobscripts.split(";"):
146
+ sub_idx_str, js_idx_lst_str = sub_i.split(":")
147
+ sub_js_idx_dct[int(sub_idx_str)] = [int(i) for i in js_idx_lst_str.split(",")]
148
+ return sub_js_idx_dct
149
+
150
+
151
+ def _set_help_name(cmd: click.Group | click.Command, app: BaseApp):
152
+ """
153
+ Update the help string of the command to contain the name of the application.
154
+ """
155
+ if cmd.help:
156
+ cmd.help = cmd.help.format(app_name=app.name)
157
+
158
+
159
+ def _make_API_CLI(app: BaseApp):
160
+ """Generate the CLI for the main functionality."""
161
+
162
+ @click.command(name="make")
163
+ @click.argument("template_file_or_str")
164
+ @string_option
165
+ @format_option
166
+ @path_option
167
+ @name_option
168
+ @name_timestamp_option
169
+ @name_dir_option
170
+ @overwrite_option
171
+ @store_option
172
+ @ts_fmt_option
173
+ @ts_name_fmt_option
174
+ @variables_option
175
+ @make_status_opt
176
+ @add_sub_opt
177
+ def make_workflow(
178
+ template_file_or_str: str,
179
+ string: bool,
180
+ format: Literal["json", "yaml"] | None,
181
+ path: Path | None,
182
+ name: str | None,
183
+ name_add_timestamp: bool | None,
184
+ name_use_dir: bool | None,
185
+ overwrite: bool,
186
+ store: str,
187
+ ts_fmt: str | None = None,
188
+ ts_name_fmt: str | None = None,
189
+ variables: list[tuple[str, str]] | None = None,
190
+ status: bool = True,
191
+ add_submission: bool = False,
192
+ ):
193
+ """Generate a new {app_name} workflow.
194
+
195
+ TEMPLATE_FILE_OR_STR is either a path to a template file in YAML or JSON
196
+ format, or a YAML/JSON string.
197
+
198
+ """
199
+ wk_or_sub = app.make_workflow(
200
+ template_file_or_str=template_file_or_str,
201
+ is_string=string,
202
+ template_format=format,
203
+ path=path,
204
+ name=name,
205
+ name_add_timestamp=name_add_timestamp,
206
+ name_use_dir=name_use_dir,
207
+ overwrite=overwrite,
208
+ store=store,
209
+ ts_fmt=ts_fmt,
210
+ ts_name_fmt=ts_name_fmt,
211
+ variables=dict(variables) if variables is not None else None,
212
+ status=status,
213
+ add_submission=add_submission,
214
+ )
215
+ if add_submission:
216
+ assert isinstance(wk_or_sub, Submission)
217
+ click.echo(wk_or_sub.workflow.path)
218
+ else:
219
+ assert isinstance(wk_or_sub, Workflow)
220
+ click.echo(wk_or_sub.path)
221
+
222
+ @click.command(name="go")
223
+ @click.argument("template_file_or_str")
224
+ @string_option
225
+ @format_option
226
+ @path_option
227
+ @name_option
228
+ @name_timestamp_option
229
+ @name_dir_option
230
+ @overwrite_option
231
+ @store_option
232
+ @ts_fmt_option
233
+ @ts_name_fmt_option
234
+ @variables_option
235
+ @js_parallelism_option
236
+ @wait_option
237
+ @add_to_known_opt
238
+ @print_idx_opt
239
+ @tasks_opt
240
+ @cancel_opt
241
+ @submit_status_opt
242
+ def make_and_submit_workflow(
243
+ template_file_or_str: str,
244
+ string: bool,
245
+ format: Literal["json", "yaml"] | None,
246
+ path: Path | None,
247
+ name: str | None,
248
+ name_add_timestamp: bool | None,
249
+ name_use_dir: bool | None,
250
+ overwrite: bool,
251
+ store: str,
252
+ ts_fmt: str | None = None,
253
+ ts_name_fmt: str | None = None,
254
+ variables: list[tuple[str, str]] | None = None,
255
+ js_parallelism: bool | None = None,
256
+ wait: bool = False,
257
+ add_to_known: bool = True,
258
+ print_idx: bool = False,
259
+ tasks: list[int] | None = None,
260
+ cancel: bool = False,
261
+ status: bool = True,
262
+ ):
263
+ """Generate and submit a new {app_name} workflow.
264
+
265
+ TEMPLATE_FILE_OR_STR is either a path to a template file in YAML or JSON
266
+ format, or a YAML/JSON string.
267
+
268
+ """
269
+ # TODO: allow submitting a persistent workflow via this command?
270
+ out = app.make_and_submit_workflow(
271
+ template_file_or_str=template_file_or_str,
272
+ is_string=string,
273
+ template_format=format,
274
+ path=path,
275
+ name=name,
276
+ name_add_timestamp=name_add_timestamp,
277
+ name_use_dir=name_use_dir,
278
+ overwrite=overwrite,
279
+ store=store,
280
+ ts_fmt=ts_fmt,
281
+ ts_name_fmt=ts_name_fmt,
282
+ variables=dict(variables) if variables is not None else None,
283
+ JS_parallelism=js_parallelism,
284
+ wait=wait,
285
+ add_to_known=add_to_known,
286
+ return_idx=print_idx,
287
+ tasks=tasks,
288
+ cancel=cancel,
289
+ status=status,
290
+ )
291
+ if print_idx:
292
+ assert isinstance(out, tuple)
293
+ click.echo(out[1])
294
+
295
+ @click.command(context_settings={"ignore_unknown_options": True})
296
+ @click.argument("py_test_args", nargs=-1, type=click.UNPROCESSED)
297
+ @click.pass_context
298
+ def test(ctx: click.Context, py_test_args: list[str]):
299
+ """Run {app_name} test suite.
300
+
301
+ PY_TEST_ARGS are arguments passed on to Pytest.
302
+
303
+ """
304
+ ctx.exit(app.run_tests(*py_test_args))
305
+
306
+ @click.command(context_settings={"ignore_unknown_options": True})
307
+ @click.argument("py_test_args", nargs=-1, type=click.UNPROCESSED)
308
+ @click.pass_context
309
+ def test_hpcflow(ctx: click.Context, py_test_args: list[str]):
310
+ """Run hpcFlow test suite.
311
+
312
+ PY_TEST_ARGS are arguments passed on to Pytest.
313
+
314
+ """
315
+ ctx.exit(app.run_hpcflow_tests(*py_test_args))
316
+
317
+ commands = [
318
+ make_workflow,
319
+ make_and_submit_workflow,
320
+ test,
321
+ ]
322
+ for cmd in commands:
323
+ _set_help_name(cmd, app)
324
+
325
+ if app.name != "hpcFlow":
326
+ # `test_hpcflow` is the same as `test` for the hpcflow app no need to add both:
327
+ commands.append(test_hpcflow)
328
+
329
+ return commands
330
+
331
+
332
+ def _make_workflow_submission_jobscript_CLI(app: BaseApp):
333
+ """Generate the CLI for interacting with existing workflow submission
334
+ jobscripts."""
335
+
336
+ @click.group(name="js")
337
+ @_pass_submission
338
+ @click.pass_context
339
+ @click.argument("js_idx", type=click.INT)
340
+ def jobscript(ctx: click.Context, sb: Submission, js_idx: int):
341
+ """Interact with existing {app_name} workflow submission jobscripts.
342
+
343
+ JS_IDX is the jobscript index within the submission object.
344
+
345
+ """
346
+ ctx.obj = sb.jobscripts[js_idx]
347
+
348
+ @jobscript.command(name="res")
349
+ @_pass_js
350
+ def resources(job: Jobscript):
351
+ """Get resources associated with this jobscript."""
352
+ click.echo(job.resources.__dict__)
353
+
354
+ @jobscript.command(name="deps")
355
+ @_pass_js
356
+ def dependencies(job: Jobscript):
357
+ """Get jobscript dependencies."""
358
+ click.echo(job.dependencies)
359
+
360
+ @jobscript.command()
361
+ @_pass_js
362
+ def path(job: Jobscript):
363
+ """Get the file path to the jobscript."""
364
+ click.echo(job.jobscript_path)
365
+
366
+ @jobscript.command()
367
+ @_pass_js
368
+ def show(job: Jobscript):
369
+ """Show the jobscript file."""
370
+ with job.jobscript_path.open("rt") as fp:
371
+ click.echo(fp.read())
372
+
373
+ @jobscript.command()
374
+ @jobscript_std_array_idx_opt
375
+ @_pass_js
376
+ def stdout(job: Jobscript, array_idx: int):
377
+ """Print the contents of the standard output stream file."""
378
+ job.print_stdout(array_idx=array_idx)
379
+
380
+ @jobscript.command()
381
+ @jobscript_std_array_idx_opt
382
+ @_pass_js
383
+ def stderr(job: Jobscript, array_idx: int):
384
+ """Print the contents of the standard error stream file."""
385
+ job.print_stderr(array_idx=array_idx)
386
+
387
+ _set_help_name(jobscript, app)
388
+ return jobscript
389
+
390
+
391
+ def _make_workflow_submission_CLI(app: BaseApp):
392
+ """Generate the CLI for interacting with existing workflow submissions."""
393
+
394
+ @click.group(name="sub")
395
+ @_pass_workflow
396
+ @click.pass_context
397
+ @click.argument("sub_idx", type=click.INT)
398
+ def submission(ctx: click.Context, wf: Workflow, sub_idx: int):
399
+ """Interact with existing {app_name} workflow submissions.
400
+
401
+ SUB_IDX is the submission index.
402
+
403
+ """
404
+ ctx.obj = wf.submissions[sub_idx]
405
+
406
+ @submission.command("status")
407
+ @_pass_submission
408
+ def status(sb: Submission):
409
+ """Get the submission status."""
410
+ click.echo(sb.status.name.lower())
411
+
412
+ @submission.command("submitted-js")
413
+ @_pass_submission
414
+ def submitted_JS(sb: Submission):
415
+ """Get a list of jobscript indices that have been submitted."""
416
+ click.echo(sb.submitted_jobscripts)
417
+
418
+ @submission.command("outstanding-js")
419
+ @_pass_submission
420
+ def outstanding_JS(sb: Submission):
421
+ """Get a list of jobscript indices that have not yet been submitted."""
422
+ click.echo(sb.outstanding_jobscripts)
423
+
424
+ @submission.command("needs-submit")
425
+ @_pass_submission
426
+ def needs_submit(sb: Submission):
427
+ """Check if this submission needs submitting."""
428
+ click.echo(sb.needs_submit)
429
+
430
+ @submission.command("get-active-jobscripts")
431
+ @_pass_submission
432
+ def get_active_jobscripts(sb: Submission):
433
+ """Show active jobscripts and their jobscript-element states."""
434
+ pprint(sb.get_active_jobscripts(as_json=True))
435
+
436
+ @submission.command()
437
+ @_pass_submission
438
+ def get_scheduler_job_IDs(sb: Submission):
439
+ """Print jobscript scheduler job IDs."""
440
+ job_IDs = sb.get_scheduler_job_IDs()
441
+ if job_IDs:
442
+ print("\n".join(job_IDs))
443
+
444
+ @submission.command()
445
+ @_pass_submission
446
+ def get_process_IDs(sb: Submission):
447
+ """Print jobscript process IDs."""
448
+ proc_IDs = sb.get_process_IDs()
449
+ if proc_IDs:
450
+ print("\n".join(str(i) for i in proc_IDs))
451
+
452
+ @submission.command()
453
+ @list_js_max_js_opt
454
+ @list_js_jobscripts_opt
455
+ @list_js_width_opt
456
+ @_pass_submission
457
+ def list_jobscripts(
458
+ sb: Submission, max_js: int | None, jobscripts: str | None, width: int | None
459
+ ):
460
+ """Print a table listing jobscripts and associated information."""
461
+ jobscripts_ = [int(i) for i in jobscripts.split(",")] if jobscripts else None
462
+ sb.list_jobscripts(max_js=max_js, jobscripts=jobscripts_, width=width)
463
+
464
+ @submission.command()
465
+ @list_task_js_max_js_opt
466
+ @list_task_js_task_names_opt
467
+ @list_js_width_opt
468
+ @_pass_submission
469
+ def list_task_jobscripts(
470
+ sb: Submission,
471
+ max_js: int | None,
472
+ task_names: str | None,
473
+ width: int | None,
474
+ ):
475
+ """Print a table listing tasks and their associated jobscripts."""
476
+ task_names_ = list(task_names.split(",")) if task_names else None
477
+ sb.list_task_jobscripts(task_names=task_names_, max_js=max_js, width=width)
478
+
479
+ _set_help_name(submission, app)
480
+ submission.add_command(_make_workflow_submission_jobscript_CLI(app))
481
+ return submission
482
+
483
+
484
+ def _make_workflow_CLI(app: BaseApp):
485
+ """Generate the CLI for interacting with existing workflows."""
486
+
487
+ @click.group()
488
+ @click.argument("workflow_ref")
489
+ @workflow_ref_type_opt
490
+ @click.pass_context
491
+ def workflow(ctx: click.Context, workflow_ref: str, ref_type: str | None):
492
+ """Interact with existing {app_name} workflows.
493
+
494
+ WORKFLOW_REF is the path to, or local ID of, an existing workflow.
495
+
496
+ """
497
+ workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
498
+ ctx.obj = app.Workflow(workflow_path)
499
+
500
+ @workflow.command(name="submit")
501
+ @js_parallelism_option
502
+ @wait_option
503
+ @add_to_known_opt
504
+ @print_idx_opt
505
+ @tasks_opt
506
+ @cancel_opt
507
+ @submit_status_opt
508
+ @_pass_workflow
509
+ def submit_workflow(
510
+ wf: Workflow,
511
+ js_parallelism: bool | None = None,
512
+ wait: bool = False,
513
+ add_to_known: bool = True,
514
+ print_idx: bool = False,
515
+ tasks: list[int] | None = None,
516
+ cancel: bool = False,
517
+ status: bool = True,
518
+ ):
519
+ """Submit the workflow."""
520
+ out = wf.submit(
521
+ JS_parallelism=js_parallelism,
522
+ wait=wait,
523
+ add_to_known=add_to_known,
524
+ return_idx=True,
525
+ tasks=tasks,
526
+ cancel=cancel,
527
+ status=status,
528
+ )
529
+ if print_idx:
530
+ click.echo(out)
531
+
532
+ @workflow.command(name="add-submission")
533
+ @js_parallelism_option
534
+ @tasks_opt
535
+ @force_arr_opt
536
+ @submit_status_opt
537
+ @click.pass_context
538
+ def add_submission(
539
+ ctx,
540
+ js_parallelism=None,
541
+ tasks=None,
542
+ force_array=False,
543
+ status=True,
544
+ ):
545
+ """Add a new submission to the workflow, but do not submit."""
546
+ ctx.obj["workflow"].add_submission(
547
+ JS_parallelism=js_parallelism,
548
+ tasks=tasks,
549
+ force_array=force_array,
550
+ status=status,
551
+ )
552
+
553
+ @workflow.command(name="wait")
554
+ @click.option(
555
+ "-j",
556
+ "--jobscripts",
557
+ help=(
558
+ "Wait for only these jobscripts to finish. Jobscripts should be specified by "
559
+ "their submission index, followed by a colon, followed by a comma-separated "
560
+ "list of jobscript indices within that submission (no spaces are allowed). "
561
+ "To specify jobscripts across multiple submissions, use a semicolon to "
562
+ "separate patterns like these."
563
+ ),
564
+ )
565
+ @_pass_workflow
566
+ def wait(wf: Workflow, jobscripts: str | None):
567
+ js_spec = parse_jobscript_wait_spec(jobscripts) if jobscripts else None
568
+ wf.wait(sub_js=js_spec)
569
+
570
+ @workflow.command(name="abort-run")
571
+ @click.option("--submission", type=click.INT, default=-1)
572
+ @click.option("--task", type=click.INT)
573
+ @click.option("--element", type=click.INT)
574
+ @_pass_workflow
575
+ def abort_run(wf: Workflow, submission: int, task: int, element: int):
576
+ """Abort the specified run."""
577
+ wf.abort_run(
578
+ submission_idx=submission,
579
+ task_idx=task,
580
+ element_idx=element,
581
+ )
582
+
583
+ @workflow.command(name="get-param")
584
+ @click.argument("index", type=click.INT)
585
+ @_pass_workflow
586
+ def get_parameter(wf: Workflow, index: int):
587
+ """Get a parameter value by data index."""
588
+ click.echo(wf.get_parameter_data(index))
589
+
590
+ @workflow.command(name="get-param-source")
591
+ @click.argument("index", type=click.INT)
592
+ @_pass_workflow
593
+ def get_parameter_source(wf: Workflow, index: int):
594
+ """Get a parameter source by data index."""
595
+ click.echo(wf.get_parameter_source(index))
596
+
597
+ @workflow.command(name="get-all-params")
598
+ @_pass_workflow
599
+ def get_all_parameters(wf: Workflow):
600
+ """Get all parameter values."""
601
+ click.echo(wf.get_all_parameter_data())
602
+
603
+ @workflow.command(name="is-param-set")
604
+ @click.argument("index", type=click.INT)
605
+ @_pass_workflow
606
+ def is_parameter_set(wf: Workflow, index: int):
607
+ """Check if a parameter specified by data index is set."""
608
+ click.echo(wf.is_parameter_set(index))
609
+
610
+ @workflow.command(name="show-all-status")
611
+ @_pass_workflow
612
+ def show_all_EAR_statuses(wf: Workflow):
613
+ """Show the submission status of all workflow EARs."""
614
+ wf.show_all_EAR_statuses()
615
+
616
+ @workflow.command(name="zip")
617
+ @zip_path_opt
618
+ @zip_overwrite_opt
619
+ @zip_log_opt
620
+ @zip_include_execute_opt
621
+ @zip_include_rechunk_backups_opt
622
+ @_pass_workflow
623
+ def zip_workflow(
624
+ wf: Workflow,
625
+ path: str,
626
+ overwrite: bool,
627
+ log: str | None,
628
+ include_execute: bool,
629
+ include_rechunk_backups: bool,
630
+ ):
631
+ """Generate a copy of the workflow in the zip file format in the current working
632
+ directory."""
633
+ click.echo(
634
+ wf.zip(
635
+ path=path,
636
+ overwrite=overwrite,
637
+ log=log,
638
+ include_execute=include_execute,
639
+ include_rechunk_backups=include_rechunk_backups,
640
+ )
641
+ )
642
+
643
+ @workflow.command(name="unzip")
644
+ @unzip_path_opt
645
+ @unzip_log_opt
646
+ @_pass_workflow
647
+ def unzip_workflow(wf: Workflow, path: str, log: str | None):
648
+ """Generate a copy of the zipped workflow in the submittable Zarr format in the
649
+ current working directory."""
650
+ click.echo(wf.unzip(path=path, log=log))
651
+
652
+ @workflow.command(name="rechunk")
653
+ @rechunk_backup_opt
654
+ @rechunk_chunk_size_opt
655
+ @rechunk_status_opt
656
+ @_pass_workflow
657
+ def rechunk(wf: Workflow, backup: bool, chunk_size: int, status: bool):
658
+ """Rechunk metadata/runs and parameters/base arrays."""
659
+ wf.rechunk(backup=backup, chunk_size=chunk_size, status=status)
660
+
661
+ @workflow.command(name="rechunk-runs")
662
+ @rechunk_backup_opt
663
+ @rechunk_chunk_size_opt
664
+ @rechunk_status_opt
665
+ @_pass_workflow
666
+ def rechunk_runs(wf: Workflow, backup: bool, chunk_size: int, status: bool):
667
+ """Rechunk the metadata/runs array."""
668
+ wf.rechunk_runs(backup=backup, chunk_size=chunk_size, status=status)
669
+
670
+ @workflow.command(name="rechunk-parameter-base")
671
+ @rechunk_backup_opt
672
+ @rechunk_chunk_size_opt
673
+ @rechunk_status_opt
674
+ @_pass_workflow
675
+ def rechunk_parameter_base(wf: Workflow, backup: bool, chunk_size: int, status: bool):
676
+ """Rechunk the parameters/base array."""
677
+ wf.rechunk_parameter_base(backup=backup, chunk_size=chunk_size, status=status)
678
+
679
+ @workflow.command()
680
+ @_pass_workflow
681
+ def get_scheduler_job_IDs(wf: Workflow):
682
+ """Print jobscript scheduler job IDs from all submissions of this workflow."""
683
+ job_IDs = wf.get_scheduler_job_IDs()
684
+ if job_IDs:
685
+ print("\n".join(job_IDs))
686
+
687
+ @workflow.command()
688
+ @_pass_workflow
689
+ def get_process_IDs(wf: Workflow):
690
+ """Print jobscript process IDs from all submissions of this workflow."""
691
+ proc_IDs = wf.get_process_IDs()
692
+ if proc_IDs:
693
+ print("\n".join(str(i) for i in proc_IDs))
694
+
695
+ @workflow.command()
696
+ @click.option(
697
+ "--sub-idx",
698
+ type=click.INT,
699
+ default=0,
700
+ help="Submission index whose jobscripts are to be shown.",
701
+ )
702
+ @list_js_max_js_opt
703
+ @list_js_jobscripts_opt
704
+ @list_js_width_opt
705
+ @_pass_workflow
706
+ def list_jobscripts(
707
+ wf: Workflow,
708
+ sub_idx: int,
709
+ max_js: int | None,
710
+ jobscripts: str | None,
711
+ width: int | None,
712
+ ):
713
+ """Print a table listing jobscripts and associated information from the specified
714
+ submission."""
715
+ jobscripts_ = [int(i) for i in jobscripts.split(",")] if jobscripts else None
716
+ wf.list_jobscripts(
717
+ sub_idx=sub_idx, max_js=max_js, jobscripts=jobscripts_, width=width
718
+ )
719
+
720
+ @workflow.command()
721
+ @click.option(
722
+ "--sub-idx",
723
+ type=click.INT,
724
+ default=0,
725
+ help="Submission index whose tasks are to be shown.",
726
+ )
727
+ @list_task_js_max_js_opt
728
+ @list_task_js_task_names_opt
729
+ @list_js_width_opt
730
+ @_pass_workflow
731
+ def list_task_jobscripts(
732
+ wf: Workflow,
733
+ sub_idx: int,
734
+ max_js: int | None,
735
+ task_names: str | None,
736
+ width: int | None,
737
+ ):
738
+ """Print a table listing tasks and their associated jobscripts from the specified
739
+ submission."""
740
+ task_names_ = list(task_names.split(",")) if task_names else None
741
+ wf.list_task_jobscripts(
742
+ sub_idx=sub_idx, task_names=task_names_, max_js=max_js, width=width
743
+ )
744
+
745
+ _set_help_name(workflow, app)
746
+ workflow.add_command(_make_workflow_submission_CLI(app))
747
+ return workflow
748
+
749
+
750
+ def _make_submission_CLI(app: BaseApp):
751
+ """Generate the CLI for submission related queries."""
752
+
753
+ def OS_info_callback(ctx: click.Context, param, value: bool):
754
+ if not value or ctx.resilient_parsing:
755
+ return
756
+ pprint(app.get_OS_info())
757
+ ctx.exit()
758
+
759
+ @click.group()
760
+ @click.option(
761
+ "--os-info",
762
+ help="Print information about the operating system.",
763
+ is_flag=True,
764
+ is_eager=True,
765
+ expose_value=False,
766
+ callback=OS_info_callback,
767
+ )
768
+ def submission():
769
+ """Submission-related queries."""
770
+ pass
771
+
772
+ @submission.command("shell-info")
773
+ @click.argument("shell_name", type=click.Choice(list(ALL_SHELLS)))
774
+ @click.option("--exclude-os", is_flag=True, default=False)
775
+ @click.pass_context
776
+ def shell_info(ctx: click.Context, shell_name: str, exclude_os: bool):
777
+ """Show information about the specified shell, such as the version."""
778
+ pprint(app.get_shell_info(shell_name, exclude_os))
779
+ ctx.exit()
780
+
781
+ @submission.group("scheduler")
782
+ @click.argument("scheduler_name")
783
+ @click.pass_context
784
+ def scheduler(ctx: click.Context, scheduler_name: str):
785
+ ctx.obj = app.get_scheduler(scheduler_name, os.name)
786
+
787
+ pass_scheduler = click.make_pass_decorator(SGEPosix)
788
+
789
+ @scheduler.command()
790
+ @pass_scheduler
791
+ def get_login_nodes(scheduler: SGEPosix):
792
+ pprint(scheduler.get_login_nodes())
793
+
794
+ class _DateTimeJSONEncoder(json.JSONEncoder):
795
+ def default(self, obj):
796
+ if isinstance(obj, datetime.datetime):
797
+ return obj.isoformat()
798
+ return super().default(obj)
799
+
800
+ @submission.command()
801
+ @click.option(
802
+ "as_json",
803
+ "--json",
804
+ is_flag=True,
805
+ default=False,
806
+ help="Do not format and only show JSON-compatible information.",
807
+ )
808
+ def get_known(as_json: bool = False):
809
+ """Print known-submissions information as a formatted Python object."""
810
+ out = app.get_known_submissions(as_json=as_json)
811
+ if as_json:
812
+ click.echo(json.dumps(out, cls=_DateTimeJSONEncoder))
813
+ else:
814
+ pprint(out)
815
+
816
+ return submission
817
+
818
+
819
+ def _make_internal_CLI(app: BaseApp):
820
+ """Generate the CLI for internal use."""
821
+
822
+ @click.group()
823
+ def internal(help: bool = True): # TEMP
824
+ """Internal CLI to be invoked by scripts generated by the app."""
825
+ pass
826
+
827
+ @internal.command(
828
+ name="get-invoc-cmd"
829
+ ) # explicit, because Click 8.2.0+ removes suffixes like "cmd" for some reason
830
+ def get_invoc_cmd():
831
+ """Get the invocation command for this app instance."""
832
+ click.echo(app.run_time_info.invocation_command)
833
+
834
+ @internal.command()
835
+ @click.pass_context
836
+ @click.option("--raise", "raise_opt", is_flag=True)
837
+ @click.option("--click-exit-code", type=click.INT)
838
+ @click.option("--sleep", type=click.INT)
839
+ def noop(ctx, raise_opt, click_exit_code, sleep):
840
+ """Used only in CLI tests."""
841
+ if raise_opt:
842
+ raise ValueError("internal noop raised!")
843
+ elif click_exit_code is not None:
844
+ ctx.exit(click_exit_code)
845
+ elif sleep:
846
+ time.sleep(sleep)
847
+
848
+ @internal.group()
849
+ @click.argument("path", type=click.Path(exists=True))
850
+ @click.pass_context
851
+ def workflow(ctx: click.Context, path: Path):
852
+ """"""
853
+ ctx.obj = app.Workflow(path)
854
+
855
+ @workflow.command()
856
+ @_pass_workflow
857
+ @click.argument("submission_idx", type=click.INT)
858
+ @click.argument("jobscript_idx", type=click.INT)
859
+ @click.argument("block_idx", type=click.INT)
860
+ @click.argument("block_action_idx", type=click.INT)
861
+ @click.argument("run_id", type=click.INT)
862
+ def execute_run(
863
+ wf: Workflow,
864
+ submission_idx: int,
865
+ jobscript_idx: int,
866
+ block_idx: int,
867
+ block_action_idx: int,
868
+ run_id: int,
869
+ ):
870
+ app.CLI_logger.info(f"execute commands for EAR ID {run_id!r}.")
871
+ wf.execute_run(
872
+ submission_idx=submission_idx,
873
+ block_act_key=(jobscript_idx, block_idx, block_action_idx),
874
+ run_ID=run_id,
875
+ )
876
+
877
+ @workflow.command()
878
+ @_pass_workflow
879
+ @click.argument("submission_idx", type=click.INT)
880
+ @click.argument("jobscript_idx", type=click.INT)
881
+ def execute_combined_runs(
882
+ wf: Workflow,
883
+ submission_idx: int,
884
+ jobscript_idx: int,
885
+ ):
886
+ app.CLI_logger.info(
887
+ f"execute command for combined scripts of jobscript {jobscript_idx}."
888
+ )
889
+ wf.execute_combined_runs(
890
+ submission_idx=submission_idx,
891
+ jobscript_idx=jobscript_idx,
892
+ )
893
+
894
+ @workflow.command()
895
+ @_pass_workflow
896
+ @click.argument("name")
897
+ @click.argument("value")
898
+ @click.argument("ear_id", type=click.INT)
899
+ @click.argument("cmd_idx", type=click.INT)
900
+ @click.option("--stderr", is_flag=True, default=False)
901
+ def save_parameter(
902
+ wf: Workflow,
903
+ name: str,
904
+ value: str,
905
+ ear_id: int,
906
+ cmd_idx: int,
907
+ stderr: bool,
908
+ ):
909
+ app.CLI_logger.info(
910
+ f"save parameter {name!r} for EAR ID {ear_id!r} and command index "
911
+ f"{cmd_idx!r} (stderr={stderr!r})"
912
+ )
913
+ app.CLI_logger.debug(f"save parameter value is: {value!r}")
914
+ with wf._store.cached_load():
915
+ value = wf.process_shell_parameter_output(
916
+ name=name,
917
+ value=value,
918
+ EAR_ID=ear_id,
919
+ cmd_idx=cmd_idx,
920
+ stderr=stderr,
921
+ )
922
+ app.CLI_logger.debug(f"save parameter processed value is: {value!r}")
923
+ wf.save_parameter(name=name, value=value, EAR_ID=ear_id)
924
+
925
+ # TODO: in general, maybe the workflow command group can expose the simple Workflow
926
+ # properties; maybe use a decorator on the Workflow property object to signify
927
+ # inclusion?
928
+
929
+ return internal
930
+
931
+
932
+ def _make_template_components_CLI(app: BaseApp):
933
+ @click.command()
934
+ def tc(help: bool = True):
935
+ """For showing template component data."""
936
+ pprint(app.template_components)
937
+
938
+ return tc
939
+
940
+
941
+ def _make_show_CLI(app: BaseApp):
942
+ def show_legend_callback(ctx: click.Context, param, value: bool):
943
+ if not value or ctx.resilient_parsing:
944
+ return
945
+ app.show_legend()
946
+ ctx.exit()
947
+
948
+ @click.command()
949
+ @click.option(
950
+ "-r",
951
+ "--max-recent",
952
+ default=3,
953
+ help="The maximum number of inactive submissions to show.",
954
+ )
955
+ @click.option(
956
+ "--no-update",
957
+ is_flag=True,
958
+ default=False,
959
+ help=(
960
+ "If True, do not update the known-submissions file to remove workflows that "
961
+ "are no longer running."
962
+ ),
963
+ )
964
+ @click.option(
965
+ "-f",
966
+ "--full",
967
+ is_flag=True,
968
+ default=False,
969
+ help="Allow multiple lines per workflow submission.",
970
+ )
971
+ @click.option(
972
+ "--legend",
973
+ help="Display the legend for the `show` command output.",
974
+ is_flag=True,
975
+ is_eager=True,
976
+ expose_value=False,
977
+ callback=show_legend_callback,
978
+ )
979
+ def show(max_recent: int, full: bool, no_update: bool):
980
+ """Show information about running and recently active workflows."""
981
+ app.show(max_recent=max_recent, full=full, no_update=no_update)
982
+
983
+ return show
984
+
985
+
986
+ def _make_zip_CLI(app: BaseApp):
987
+ @click.command(name="zip")
988
+ @click.argument("workflow_ref")
989
+ @zip_path_opt
990
+ @zip_overwrite_opt
991
+ @zip_log_opt
992
+ @zip_include_execute_opt
993
+ @zip_include_rechunk_backups_opt
994
+ @workflow_ref_type_opt
995
+ def zip_workflow(
996
+ workflow_ref: str,
997
+ path: str,
998
+ overwrite: bool,
999
+ log: str | None,
1000
+ include_execute: bool,
1001
+ include_rechunk_backups: bool,
1002
+ ref_type: str | None,
1003
+ ):
1004
+ """Generate a copy of the specified workflow in the zip file format in the
1005
+ current working directory.
1006
+
1007
+ WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
1008
+ workflow path.
1009
+ """
1010
+ workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
1011
+ wk = app.Workflow(workflow_path)
1012
+ click.echo(
1013
+ wk.zip(
1014
+ path=path,
1015
+ overwrite=overwrite,
1016
+ log=log,
1017
+ include_execute=include_execute,
1018
+ include_rechunk_backups=include_rechunk_backups,
1019
+ )
1020
+ )
1021
+
1022
+ return zip_workflow
1023
+
1024
+
1025
+ def _make_unzip_CLI(app: BaseApp):
1026
+ @click.command(name="unzip")
1027
+ @click.argument("workflow_path")
1028
+ @unzip_path_opt
1029
+ @unzip_log_opt
1030
+ def unzip_workflow(workflow_path: str, path: str, log: str | None):
1031
+ """Generate a copy of the specified zipped workflow in the submittable Zarr
1032
+ format in the current working directory.
1033
+
1034
+ WORKFLOW_PATH is path of the zip file to unzip.
1035
+
1036
+ """
1037
+ wk = app.Workflow(workflow_path)
1038
+ click.echo(wk.unzip(path=path, log=log))
1039
+
1040
+ return unzip_workflow
1041
+
1042
+
1043
+ def _make_cancel_CLI(app: BaseApp):
1044
+ @click.command()
1045
+ @click.argument("workflow_ref")
1046
+ @workflow_ref_type_opt
1047
+ @cancel_status_opt
1048
+ def cancel(workflow_ref: str, ref_type: str | None, status: bool):
1049
+ """Stop all running jobscripts of the specified workflow.
1050
+
1051
+ WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
1052
+ workflow path.
1053
+
1054
+ """
1055
+ app.cancel(workflow_ref=workflow_ref, ref_is_path=ref_type, status=status)
1056
+
1057
+ return cancel
1058
+
1059
+
1060
+ def _make_rechunk_CLI(app: BaseApp):
1061
+ @click.command(name="rechunk")
1062
+ @click.argument("workflow_ref")
1063
+ @workflow_ref_type_opt
1064
+ @rechunk_backup_opt
1065
+ @rechunk_chunk_size_opt
1066
+ @rechunk_status_opt
1067
+ def rechunk(
1068
+ workflow_ref: str,
1069
+ ref_type: str | None,
1070
+ backup: bool,
1071
+ chunk_size: int,
1072
+ status: bool,
1073
+ ):
1074
+ """Rechunk metadata/runs and parameters/base arrays.
1075
+
1076
+ WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
1077
+ workflow path.
1078
+
1079
+ """
1080
+ workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
1081
+ wk = app.Workflow(workflow_path)
1082
+ wk.rechunk(backup=backup, chunk_size=chunk_size, status=status)
1083
+
1084
+ return rechunk
1085
+
1086
+
1087
+ def _make_open_CLI(app: BaseApp):
1088
+ @click.group(name="open")
1089
+ def open_file():
1090
+ """Open a file (for example {app_name}'s log file) using the default
1091
+ application."""
1092
+
1093
+ @open_file.command()
1094
+ @click.option("--path", is_flag=True, default=False)
1095
+ def log(path: bool = False):
1096
+ """Open the {app_name} log file."""
1097
+ file_path = app.config.log_file_path
1098
+ if path:
1099
+ click.echo(file_path)
1100
+ else:
1101
+ utils.open_file(file_path)
1102
+
1103
+ @open_file.command()
1104
+ @click.option("--path", is_flag=True, default=False)
1105
+ def config(path: bool = False):
1106
+ """Open the {app_name} config file, or retrieve it's path."""
1107
+ file_path = app.config.config_file_path
1108
+ if path:
1109
+ click.echo(file_path)
1110
+ else:
1111
+ utils.open_file(file_path)
1112
+
1113
+ @open_file.command()
1114
+ @click.option("--name")
1115
+ @click.option("--path", is_flag=True, default=False)
1116
+ def env_source(name: str | None = None, path: bool = False):
1117
+ """Open a named environment sources file, or the first one."""
1118
+ if not (sources := app.config.environment_sources):
1119
+ raise ValueError("No environment sources specified in the config file.")
1120
+ if not name:
1121
+ file_paths = [sources[0]]
1122
+ else:
1123
+ file_paths = [pth for pth in sources if pth.name == name]
1124
+ if not file_paths:
1125
+ raise ValueError(
1126
+ f"No environment source named {name!r} could be found; available "
1127
+ f"environment source files have names: {[pth.name for pth in sources]!r}"
1128
+ )
1129
+
1130
+ assert len(file_paths) < 5 # don't open a stupid number of files
1131
+ for pth in file_paths:
1132
+ if path:
1133
+ click.echo(pth)
1134
+ else:
1135
+ utils.open_file(pth)
1136
+
1137
+ @open_file.command()
1138
+ @click.option("--path", is_flag=True, default=False)
1139
+ def known_subs(path: bool = False):
1140
+ """Open the known-submissions text file."""
1141
+ file_path = app.known_subs_file_path
1142
+ if path:
1143
+ click.echo(file_path)
1144
+ else:
1145
+ utils.open_file(file_path)
1146
+
1147
+ @open_file.command()
1148
+ @click.argument("workflow_ref")
1149
+ @click.option("--path", is_flag=True, default=False)
1150
+ @workflow_ref_type_opt
1151
+ def workflow(workflow_ref: str, ref_type: str | None, path: bool = False):
1152
+ """Open a workflow directory using, for example, File Explorer on Windows."""
1153
+ workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
1154
+ if path:
1155
+ click.echo(workflow_path)
1156
+ else:
1157
+ utils.open_file(workflow_path)
1158
+
1159
+ @open_file.command()
1160
+ @click.option("--path", is_flag=True, default=False)
1161
+ def user_data_dir(path: bool = False):
1162
+ dir_path = app._ensure_user_data_dir()
1163
+ if path:
1164
+ click.echo(dir_path)
1165
+ else:
1166
+ utils.open_file(dir_path)
1167
+
1168
+ @open_file.command()
1169
+ @click.option("--path", is_flag=True, default=False)
1170
+ def user_cache_dir(path: bool = False):
1171
+ dir_path = app._ensure_user_cache_dir()
1172
+ if path:
1173
+ click.echo(dir_path)
1174
+ else:
1175
+ utils.open_file(dir_path)
1176
+
1177
+ @open_file.command()
1178
+ @click.option("--path", is_flag=True, default=False)
1179
+ def user_runtime_dir(path: bool = False):
1180
+ dir_path = app._ensure_user_runtime_dir()
1181
+ if path:
1182
+ click.echo(dir_path)
1183
+ else:
1184
+ utils.open_file(dir_path)
1185
+
1186
+ @open_file.command()
1187
+ @click.option("--path", is_flag=True, default=False)
1188
+ def user_data_hostname_dir(path: bool = False):
1189
+ dir_path = app._ensure_user_data_hostname_dir()
1190
+ if path:
1191
+ click.echo(dir_path)
1192
+ else:
1193
+ utils.open_file(dir_path)
1194
+
1195
+ @open_file.command()
1196
+ @click.option("--path", is_flag=True, default=False)
1197
+ def user_cache_hostname_dir(path: bool = False):
1198
+ dir_path = app._ensure_user_cache_hostname_dir()
1199
+ if path:
1200
+ click.echo(dir_path)
1201
+ else:
1202
+ utils.open_file(dir_path)
1203
+
1204
+ @open_file.command()
1205
+ @click.option("--path", is_flag=True, default=False)
1206
+ def demo_data_cache_dir(path: bool = False):
1207
+ dir_path = app._ensure_demo_data_cache_dir()
1208
+ if path:
1209
+ click.echo(dir_path)
1210
+ else:
1211
+ utils.open_file(dir_path)
1212
+
1213
+ _set_help_name(open_file, app)
1214
+ _set_help_name(log, app)
1215
+ _set_help_name(config, app)
1216
+ return open_file
1217
+
1218
+
1219
+ def _make_demo_data_CLI(app: BaseApp):
1220
+ """Generate the CLI for interacting with example data files that are used in demo
1221
+ workflows."""
1222
+
1223
+ def list_callback(ctx: click.Context, param, value: bool):
1224
+ if not value or ctx.resilient_parsing:
1225
+ return
1226
+ # TODO: format with Rich with a one-line description
1227
+ click.echo("\n".join(app.list_demo_data_files()))
1228
+ ctx.exit()
1229
+
1230
+ def cache_all_callback(ctx: click.Context, param, value: bool):
1231
+ if not value or ctx.resilient_parsing:
1232
+ return
1233
+ app.cache_all_demo_data_files()
1234
+ ctx.exit()
1235
+
1236
+ @click.group()
1237
+ @click.option(
1238
+ "-l",
1239
+ "--list",
1240
+ help="Print available example data files.",
1241
+ is_flag=True,
1242
+ is_eager=True,
1243
+ expose_value=False,
1244
+ callback=list_callback,
1245
+ )
1246
+ def demo_data():
1247
+ """Interact with builtin demo data files."""
1248
+
1249
+ @demo_data.command("copy")
1250
+ @click.argument("file_name")
1251
+ @click.argument("destination")
1252
+ def copy_demo_data(file_name: str, destination: str):
1253
+ """Copy a demo data file to the specified location."""
1254
+ app.copy_demo_data(file_name=file_name, dst=destination)
1255
+
1256
+ @demo_data.command("cache")
1257
+ @click.option(
1258
+ "--all",
1259
+ help="Cache all demo data files.",
1260
+ is_flag=True,
1261
+ is_eager=True,
1262
+ expose_value=False,
1263
+ callback=cache_all_callback,
1264
+ )
1265
+ @click.argument("file_name")
1266
+ def cache_demo_data(file_name: str):
1267
+ """Ensure a demo data file is in the demo data cache."""
1268
+ app.cache_demo_data_file(file_name)
1269
+
1270
+ return demo_data
1271
+
1272
+
1273
+ def _make_manage_CLI(app: BaseApp):
1274
+ """Generate the CLI for infrequent app management tasks."""
1275
+
1276
+ @click.group()
1277
+ def manage():
1278
+ """Infrequent app management tasks.
1279
+
1280
+ App config is not loaded.
1281
+
1282
+ """
1283
+ pass
1284
+
1285
+ @manage.command()
1286
+ @click.option(
1287
+ "--config-dir",
1288
+ help="The directory containing the config file to be reset.",
1289
+ )
1290
+ def reset_config(config_dir: str):
1291
+ """Reset the configuration file to defaults.
1292
+
1293
+ This can be used if the current configuration file is invalid."""
1294
+ app.reset_config(config_dir)
1295
+
1296
+ @manage.command()
1297
+ @click.option(
1298
+ "--config-dir",
1299
+ help="The directory containing the config file whose path is to be returned.",
1300
+ )
1301
+ def get_config_path(config_dir: str):
1302
+ """Print the config file path without loading the config.
1303
+
1304
+ This can be used instead of `{app_name} open config --path` if the config file
1305
+ is invalid, because this command does not load the config.
1306
+
1307
+ """
1308
+ click.echo(app.get_config_path(config_dir))
1309
+
1310
+ @manage.command("clear-known-subs")
1311
+ def clear_known_subs():
1312
+ """Delete the contents of the known-submissions file."""
1313
+ app.clear_known_submissions_file()
1314
+
1315
+ @manage.command("clear-temp-dir")
1316
+ def clear_runtime_dir():
1317
+ """Delete all files in the user runtime directory."""
1318
+ app.clear_user_runtime_dir()
1319
+
1320
+ @manage.command("clear-cache")
1321
+ @click.option("--hostname", is_flag=True, default=False)
1322
+ def clear_cache(hostname: bool):
1323
+ """Delete the app cache directory."""
1324
+ if hostname:
1325
+ app.clear_user_cache_hostname_dir()
1326
+ else:
1327
+ app.clear_user_cache_dir()
1328
+
1329
+ @manage.command("clear-demo-data-cache")
1330
+ def clear_demo_data_cache():
1331
+ """Delete the app demo data cache directory."""
1332
+ app.clear_demo_data_cache_dir()
1333
+
1334
+ return manage
1335
+
1336
+
1337
+ def make_cli(app: BaseApp):
1338
+ """Generate the root CLI for the app."""
1339
+
1340
+ colorama_init(autoreset=True)
1341
+
1342
+ def run_time_info_callback(ctx: click.Context, param, value: bool):
1343
+ app.run_time_info.from_CLI = True
1344
+ if not value or ctx.resilient_parsing:
1345
+ return
1346
+ app.run_time_info.show()
1347
+ ctx.exit()
1348
+
1349
+ @click.group(name=app.name)
1350
+ @click.version_option(
1351
+ version=app.version,
1352
+ package_name=app.name,
1353
+ prog_name=app.name,
1354
+ help=f"Show the version of {app.name} and exit.",
1355
+ )
1356
+ @click.version_option(
1357
+ __version__,
1358
+ "--hpcflow-version",
1359
+ help="Show the version of hpcflow and exit.",
1360
+ package_name="hpcflow",
1361
+ prog_name=_app_name,
1362
+ )
1363
+ @click.help_option()
1364
+ @click.option(
1365
+ "--run-time-info",
1366
+ help="Print run-time information!",
1367
+ is_flag=True,
1368
+ is_eager=True,
1369
+ expose_value=False,
1370
+ callback=run_time_info_callback,
1371
+ )
1372
+ @click.option("--config-dir", help="Set the configuration directory.")
1373
+ @click.option("--config-key", help="Set the configuration invocation key.")
1374
+ @click.option(
1375
+ "--with-config",
1376
+ help="Override a config item in the config file",
1377
+ nargs=2,
1378
+ multiple=True,
1379
+ )
1380
+ @click.option(
1381
+ "--timeit",
1382
+ help=(
1383
+ "Time function pathways as the code executes and write out a summary at the "
1384
+ "end. Only functions decorated by `TimeIt.decorator` are included."
1385
+ ),
1386
+ is_flag=True,
1387
+ )
1388
+ @click.option(
1389
+ "--timeit-file",
1390
+ help=(
1391
+ "Time function pathways as the code executes and write out a summary at the "
1392
+ "end to a text file given by this file path. Only functions decorated by "
1393
+ "`TimeIt.decorator` are included."
1394
+ ),
1395
+ )
1396
+ @click.option(
1397
+ "--std-stream",
1398
+ help="File to redirect standard output and error to, and to print exceptions to.",
1399
+ )
1400
+ @click.pass_context
1401
+ def new_CLI(
1402
+ ctx: click.Context,
1403
+ config_dir,
1404
+ config_key,
1405
+ with_config,
1406
+ timeit,
1407
+ timeit_file,
1408
+ std_stream: str,
1409
+ ):
1410
+ ctx.ensure_object(dict)
1411
+
1412
+ if std_stream:
1413
+ ctx.with_resource(redirect_std_to_file_click(std_stream))
1414
+
1415
+ app.run_time_info.from_CLI = True
1416
+ TimeIt.active = timeit or timeit_file
1417
+ TimeIt.file_path = timeit_file
1418
+ if ctx.invoked_subcommand != "manage":
1419
+ # load the config
1420
+ overrides = {kv[0]: kv[1] for kv in with_config}
1421
+ try:
1422
+ app.load_config(
1423
+ config_dir=config_dir,
1424
+ config_key=config_key,
1425
+ **overrides,
1426
+ )
1427
+ except ConfigError as err:
1428
+ click.echo(f"{colored(err.__class__.__name__, 'red')}: {err}")
1429
+ ctx.exit(1)
1430
+
1431
+ @new_CLI.result_callback()
1432
+ def post_execution(*args, **kwargs):
1433
+ if TimeIt.active:
1434
+ TimeIt.summarise_string()
1435
+
1436
+ @new_CLI.command()
1437
+ @click.argument("name")
1438
+ @click.option("--use-current-env", is_flag=True, default=False)
1439
+ @click.option("--setup", type=click.STRING)
1440
+ @click.option("--env-source-file", type=click.STRING)
1441
+ def configure_env(
1442
+ name: str,
1443
+ use_current_env: bool,
1444
+ setup: list[str] | None = None,
1445
+ env_source_file: str | None = None,
1446
+ ):
1447
+ """Configure an app environment, using, for example, the currently activated
1448
+ Python environment."""
1449
+ app.configure_env(
1450
+ name=name,
1451
+ setup=setup,
1452
+ executables=None,
1453
+ use_current_env=use_current_env,
1454
+ env_source_file=None if env_source_file is None else Path(env_source_file),
1455
+ )
1456
+
1457
+ new_CLI.context_class = ErrorPropagatingClickContext
1458
+
1459
+ new_CLI.__doc__ = app.description
1460
+ new_CLI.add_command(get_config_CLI(app))
1461
+ new_CLI.add_command(get_demo_software_CLI(app))
1462
+ new_CLI.add_command(get_demo_workflow_CLI(app))
1463
+ new_CLI.add_command(get_helper_CLI(app))
1464
+ new_CLI.add_command(_make_demo_data_CLI(app))
1465
+ new_CLI.add_command(_make_manage_CLI(app))
1466
+ new_CLI.add_command(_make_workflow_CLI(app))
1467
+ new_CLI.add_command(_make_submission_CLI(app))
1468
+ new_CLI.add_command(_make_internal_CLI(app))
1469
+ new_CLI.add_command(_make_template_components_CLI(app))
1470
+ new_CLI.add_command(_make_show_CLI(app))
1471
+ new_CLI.add_command(_make_open_CLI(app))
1472
+ new_CLI.add_command(_make_cancel_CLI(app))
1473
+ new_CLI.add_command(_make_zip_CLI(app))
1474
+ new_CLI.add_command(_make_unzip_CLI(app))
1475
+ new_CLI.add_command(_make_rechunk_CLI(app))
1476
+ for cli_cmd in _make_API_CLI(app):
1477
+ new_CLI.add_command(cli_cmd)
1478
+
1479
+ return new_CLI