hpcflow 0.1.9__py3-none-any.whl → 0.2.0a271__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hpcflow/__init__.py +2 -11
- hpcflow/__pyinstaller/__init__.py +5 -0
- hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
- hpcflow/_version.py +1 -1
- hpcflow/app.py +43 -0
- hpcflow/cli.py +2 -462
- hpcflow/data/demo_data_manifest/__init__.py +3 -0
- hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
- hpcflow/data/jinja_templates/test/test_template.txt +8 -0
- hpcflow/data/programs/hello_world/README.md +1 -0
- hpcflow/data/programs/hello_world/hello_world.c +87 -0
- hpcflow/data/programs/hello_world/linux/hello_world +0 -0
- hpcflow/data/programs/hello_world/macos/hello_world +0 -0
- hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
- hpcflow/data/scripts/__init__.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
- hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/generate_t1_file_01.py +7 -0
- hpcflow/data/scripts/import_future_script.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
- hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
- hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/parse_t1_file_01.py +4 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/__init__.py +1 -0
- hpcflow/data/template_components/command_files.yaml +26 -0
- hpcflow/data/template_components/environments.yaml +13 -0
- hpcflow/data/template_components/parameters.yaml +14 -0
- hpcflow/data/template_components/task_schemas.yaml +139 -0
- hpcflow/data/workflows/workflow_1.yaml +5 -0
- hpcflow/examples.ipynb +1037 -0
- hpcflow/sdk/__init__.py +149 -0
- hpcflow/sdk/app.py +4266 -0
- hpcflow/sdk/cli.py +1479 -0
- hpcflow/sdk/cli_common.py +385 -0
- hpcflow/sdk/config/__init__.py +5 -0
- hpcflow/sdk/config/callbacks.py +246 -0
- hpcflow/sdk/config/cli.py +388 -0
- hpcflow/sdk/config/config.py +1410 -0
- hpcflow/sdk/config/config_file.py +501 -0
- hpcflow/sdk/config/errors.py +272 -0
- hpcflow/sdk/config/types.py +150 -0
- hpcflow/sdk/core/__init__.py +38 -0
- hpcflow/sdk/core/actions.py +3857 -0
- hpcflow/sdk/core/app_aware.py +25 -0
- hpcflow/sdk/core/cache.py +224 -0
- hpcflow/sdk/core/command_files.py +814 -0
- hpcflow/sdk/core/commands.py +424 -0
- hpcflow/sdk/core/element.py +2071 -0
- hpcflow/sdk/core/enums.py +221 -0
- hpcflow/sdk/core/environment.py +256 -0
- hpcflow/sdk/core/errors.py +1043 -0
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +809 -0
- hpcflow/sdk/core/loop.py +1320 -0
- hpcflow/sdk/core/loop_cache.py +282 -0
- hpcflow/sdk/core/object_list.py +933 -0
- hpcflow/sdk/core/parameters.py +3371 -0
- hpcflow/sdk/core/rule.py +196 -0
- hpcflow/sdk/core/run_dir_files.py +57 -0
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +3792 -0
- hpcflow/sdk/core/task_schema.py +993 -0
- hpcflow/sdk/core/test_utils.py +538 -0
- hpcflow/sdk/core/types.py +447 -0
- hpcflow/sdk/core/utils.py +1207 -0
- hpcflow/sdk/core/validation.py +87 -0
- hpcflow/sdk/core/values.py +477 -0
- hpcflow/sdk/core/workflow.py +4820 -0
- hpcflow/sdk/core/zarr_io.py +206 -0
- hpcflow/sdk/data/__init__.py +13 -0
- hpcflow/sdk/data/config_file_schema.yaml +34 -0
- hpcflow/sdk/data/config_schema.yaml +260 -0
- hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
- hpcflow/sdk/data/files_spec_schema.yaml +5 -0
- hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
- hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
- hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
- hpcflow/sdk/demo/__init__.py +3 -0
- hpcflow/sdk/demo/cli.py +242 -0
- hpcflow/sdk/helper/__init__.py +3 -0
- hpcflow/sdk/helper/cli.py +137 -0
- hpcflow/sdk/helper/helper.py +300 -0
- hpcflow/sdk/helper/watcher.py +192 -0
- hpcflow/sdk/log.py +288 -0
- hpcflow/sdk/persistence/__init__.py +18 -0
- hpcflow/sdk/persistence/base.py +2817 -0
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +39 -0
- hpcflow/sdk/persistence/json.py +954 -0
- hpcflow/sdk/persistence/pending.py +948 -0
- hpcflow/sdk/persistence/store_resource.py +203 -0
- hpcflow/sdk/persistence/types.py +309 -0
- hpcflow/sdk/persistence/utils.py +73 -0
- hpcflow/sdk/persistence/zarr.py +2388 -0
- hpcflow/sdk/runtime.py +320 -0
- hpcflow/sdk/submission/__init__.py +3 -0
- hpcflow/sdk/submission/enums.py +70 -0
- hpcflow/sdk/submission/jobscript.py +2379 -0
- hpcflow/sdk/submission/schedulers/__init__.py +281 -0
- hpcflow/sdk/submission/schedulers/direct.py +233 -0
- hpcflow/sdk/submission/schedulers/sge.py +376 -0
- hpcflow/sdk/submission/schedulers/slurm.py +598 -0
- hpcflow/sdk/submission/schedulers/utils.py +25 -0
- hpcflow/sdk/submission/shells/__init__.py +52 -0
- hpcflow/sdk/submission/shells/base.py +229 -0
- hpcflow/sdk/submission/shells/bash.py +504 -0
- hpcflow/sdk/submission/shells/os_version.py +115 -0
- hpcflow/sdk/submission/shells/powershell.py +352 -0
- hpcflow/sdk/submission/submission.py +1402 -0
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +194 -0
- hpcflow/sdk/utils/arrays.py +69 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +31 -0
- hpcflow/sdk/utils/strings.py +69 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +123 -0
- hpcflow/tests/data/__init__.py +0 -0
- hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
- hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_1.json +10 -0
- hpcflow/tests/data/workflow_1.yaml +5 -0
- hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
- hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
- hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
- hpcflow/tests/programs/test_programs.py +180 -0
- hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +1361 -0
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
- hpcflow/tests/unit/test_action.py +1066 -0
- hpcflow/tests/unit/test_action_rule.py +24 -0
- hpcflow/tests/unit/test_app.py +132 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +172 -0
- hpcflow/tests/unit/test_command.py +377 -0
- hpcflow/tests/unit/test_config.py +195 -0
- hpcflow/tests/unit/test_config_file.py +162 -0
- hpcflow/tests/unit/test_element.py +666 -0
- hpcflow/tests/unit/test_element_iteration.py +88 -0
- hpcflow/tests/unit/test_element_set.py +158 -0
- hpcflow/tests/unit/test_group.py +115 -0
- hpcflow/tests/unit/test_input_source.py +1479 -0
- hpcflow/tests/unit/test_input_value.py +398 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +1247 -0
- hpcflow/tests/unit/test_loop.py +2674 -0
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
- hpcflow/tests/unit/test_object_list.py +116 -0
- hpcflow/tests/unit/test_parameter.py +243 -0
- hpcflow/tests/unit/test_persistence.py +664 -0
- hpcflow/tests/unit/test_resources.py +243 -0
- hpcflow/tests/unit/test_run.py +286 -0
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +9 -0
- hpcflow/tests/unit/test_schema_input.py +372 -0
- hpcflow/tests/unit/test_shell.py +129 -0
- hpcflow/tests/unit/test_slurm.py +39 -0
- hpcflow/tests/unit/test_submission.py +502 -0
- hpcflow/tests/unit/test_task.py +2560 -0
- hpcflow/tests/unit/test_task_schema.py +182 -0
- hpcflow/tests/unit/test_utils.py +616 -0
- hpcflow/tests/unit/test_value_sequence.py +549 -0
- hpcflow/tests/unit/test_values.py +91 -0
- hpcflow/tests/unit/test_workflow.py +827 -0
- hpcflow/tests/unit/test_workflow_template.py +186 -0
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/unit/utils/test_strings.py +97 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +355 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +564 -0
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6794 -0
- hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
- hpcflow-0.2.0a271.dist-info/METADATA +65 -0
- hpcflow-0.2.0a271.dist-info/RECORD +237 -0
- {hpcflow-0.1.9.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
- hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
- hpcflow/api.py +0 -458
- hpcflow/archive/archive.py +0 -308
- hpcflow/archive/cloud/cloud.py +0 -47
- hpcflow/archive/cloud/errors.py +0 -9
- hpcflow/archive/cloud/providers/dropbox.py +0 -432
- hpcflow/archive/errors.py +0 -5
- hpcflow/base_db.py +0 -4
- hpcflow/config.py +0 -232
- hpcflow/copytree.py +0 -66
- hpcflow/data/examples/_config.yml +0 -14
- hpcflow/data/examples/damask/demo/1.run.yml +0 -4
- hpcflow/data/examples/damask/demo/2.process.yml +0 -29
- hpcflow/data/examples/damask/demo/geom.geom +0 -2052
- hpcflow/data/examples/damask/demo/load.load +0 -1
- hpcflow/data/examples/damask/demo/material.config +0 -185
- hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
- hpcflow/data/examples/damask/inputs/load.load +0 -1
- hpcflow/data/examples/damask/inputs/material.config +0 -185
- hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
- hpcflow/data/examples/damask/profiles/damask.yml +0 -4
- hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
- hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
- hpcflow/data/examples/damask/profiles/default.yml +0 -6
- hpcflow/data/examples/thinking.yml +0 -177
- hpcflow/errors.py +0 -2
- hpcflow/init_db.py +0 -37
- hpcflow/models.py +0 -2549
- hpcflow/nesting.py +0 -9
- hpcflow/profiles.py +0 -455
- hpcflow/project.py +0 -81
- hpcflow/scheduler.py +0 -323
- hpcflow/utils.py +0 -103
- hpcflow/validation.py +0 -167
- hpcflow/variables.py +0 -544
- hpcflow-0.1.9.dist-info/METADATA +0 -168
- hpcflow-0.1.9.dist-info/RECORD +0 -45
- hpcflow-0.1.9.dist-info/entry_points.txt +0 -8
- hpcflow-0.1.9.dist-info/top_level.txt +0 -1
- /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
- /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
- /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
|