hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a199__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
- hpcflow/_version.py +1 -1
- hpcflow/data/scripts/bad_script.py +2 -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/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_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_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_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -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/script_exit_test.py +5 -0
- hpcflow/data/template_components/environments.yaml +1 -1
- hpcflow/sdk/__init__.py +5 -0
- hpcflow/sdk/app.py +150 -89
- hpcflow/sdk/cli.py +263 -84
- hpcflow/sdk/cli_common.py +99 -5
- hpcflow/sdk/config/callbacks.py +38 -1
- hpcflow/sdk/config/config.py +102 -13
- hpcflow/sdk/config/errors.py +19 -5
- hpcflow/sdk/config/types.py +3 -0
- hpcflow/sdk/core/__init__.py +25 -1
- hpcflow/sdk/core/actions.py +914 -262
- hpcflow/sdk/core/cache.py +76 -34
- hpcflow/sdk/core/command_files.py +14 -128
- hpcflow/sdk/core/commands.py +35 -6
- hpcflow/sdk/core/element.py +122 -50
- hpcflow/sdk/core/errors.py +58 -2
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/loop.py +408 -50
- hpcflow/sdk/core/loop_cache.py +4 -4
- hpcflow/sdk/core/parameters.py +382 -37
- hpcflow/sdk/core/run_dir_files.py +13 -40
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +119 -30
- hpcflow/sdk/core/task_schema.py +68 -0
- hpcflow/sdk/core/test_utils.py +66 -27
- hpcflow/sdk/core/types.py +54 -1
- hpcflow/sdk/core/utils.py +78 -7
- hpcflow/sdk/core/workflow.py +1538 -336
- hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
- hpcflow/sdk/demo/cli.py +7 -0
- hpcflow/sdk/helper/cli.py +1 -0
- hpcflow/sdk/log.py +42 -15
- hpcflow/sdk/persistence/base.py +405 -53
- hpcflow/sdk/persistence/json.py +177 -52
- hpcflow/sdk/persistence/pending.py +237 -69
- hpcflow/sdk/persistence/store_resource.py +3 -2
- hpcflow/sdk/persistence/types.py +15 -4
- hpcflow/sdk/persistence/zarr.py +928 -81
- hpcflow/sdk/submission/jobscript.py +1408 -489
- hpcflow/sdk/submission/schedulers/__init__.py +40 -5
- hpcflow/sdk/submission/schedulers/direct.py +33 -19
- hpcflow/sdk/submission/schedulers/sge.py +51 -16
- hpcflow/sdk/submission/schedulers/slurm.py +44 -16
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/base.py +68 -20
- hpcflow/sdk/submission/shells/bash.py +222 -129
- hpcflow/sdk/submission/shells/powershell.py +200 -150
- hpcflow/sdk/submission/submission.py +852 -119
- hpcflow/sdk/submission/types.py +18 -21
- hpcflow/sdk/typing.py +24 -5
- hpcflow/sdk/utils/arrays.py +71 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +12 -0
- hpcflow/sdk/utils/strings.py +33 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +19 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +821 -70
- 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 +6 -0
- hpcflow/tests/unit/test_action.py +176 -0
- hpcflow/tests/unit/test_app.py +20 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +133 -0
- hpcflow/tests/unit/test_config.py +122 -1
- hpcflow/tests/unit/test_element_iteration.py +47 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_loop.py +1332 -27
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
- hpcflow/tests/unit/test_parameter.py +13 -0
- hpcflow/tests/unit/test_persistence.py +190 -8
- hpcflow/tests/unit/test_run.py +109 -3
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_shell.py +20 -0
- hpcflow/tests/unit/test_submission.py +5 -76
- 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/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +332 -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 +142 -2
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6587 -3
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +7 -4
- hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
- hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/cli.py
CHANGED
@@ -3,8 +3,10 @@ Command line interface implementation.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
|
+
import contextlib
|
6
7
|
import json
|
7
8
|
import os
|
9
|
+
import time
|
8
10
|
import click
|
9
11
|
from colorama import init as colorama_init
|
10
12
|
from termcolor import colored # type: ignore
|
@@ -32,7 +34,9 @@ from hpcflow.sdk.cli_common import (
|
|
32
34
|
tasks_opt,
|
33
35
|
cancel_opt,
|
34
36
|
submit_status_opt,
|
37
|
+
force_arr_opt,
|
35
38
|
make_status_opt,
|
39
|
+
add_sub_opt,
|
36
40
|
zip_path_opt,
|
37
41
|
zip_overwrite_opt,
|
38
42
|
zip_log_opt,
|
@@ -43,6 +47,13 @@ from hpcflow.sdk.cli_common import (
|
|
43
47
|
rechunk_backup_opt,
|
44
48
|
rechunk_chunk_size_opt,
|
45
49
|
rechunk_status_opt,
|
50
|
+
cancel_status_opt,
|
51
|
+
list_js_max_js_opt,
|
52
|
+
list_js_jobscripts_opt,
|
53
|
+
list_task_js_max_js_opt,
|
54
|
+
list_task_js_task_names_opt,
|
55
|
+
list_js_width_opt,
|
56
|
+
jobscript_std_array_idx_opt,
|
46
57
|
_add_doc_from_help,
|
47
58
|
)
|
48
59
|
from hpcflow.sdk.helper.cli import get_helper_CLI
|
@@ -84,6 +95,45 @@ _pass_js = click.make_pass_decorator(Jobscript)
|
|
84
95
|
_add_doc_from_help(string_option, workflow_ref_type_opt)
|
85
96
|
|
86
97
|
|
98
|
+
class ErrorPropagatingClickContext(click.Context):
|
99
|
+
"""A click Context class that passes on exception information.
|
100
|
+
|
101
|
+
Using the standard `click.Context` class, exceptions raised when using a resource specified
|
102
|
+
with `ctx.with_resource(my_ctx_manager())` are not passed on to the `__exit__` method of
|
103
|
+
`my_ctx_manager`. See: https://github.com/pallets/click/issues/2447.
|
104
|
+
|
105
|
+
Examples
|
106
|
+
--------
|
107
|
+
>>> @click.group()
|
108
|
+
... @click.pass_context
|
109
|
+
... def cli(ctx):
|
110
|
+
... ctx.with_resource(my_context_manager())
|
111
|
+
... cli.context_class = ErrorPropagatingClickContext
|
112
|
+
|
113
|
+
"""
|
114
|
+
|
115
|
+
def __exit__(self, exc_type, exc_value, tb):
|
116
|
+
self._depth -= 1
|
117
|
+
if self._depth == 0:
|
118
|
+
self._exit_stack.__exit__(exc_type, exc_value, tb)
|
119
|
+
self._exit_stack = contextlib.ExitStack()
|
120
|
+
click.core.pop_context()
|
121
|
+
|
122
|
+
|
123
|
+
@contextlib.contextmanager
|
124
|
+
def redirect_std_to_file_click(file, mode: str = "a"):
|
125
|
+
def ignore(exc):
|
126
|
+
"""Do not intercept Click's `Exit` exception when the exit code is zero."""
|
127
|
+
if type(exc) is click.exceptions.Exit:
|
128
|
+
if exc.exit_code == 0:
|
129
|
+
return True
|
130
|
+
return exc.exit_code
|
131
|
+
return 1 # default exit code
|
132
|
+
|
133
|
+
with utils.redirect_std_to_file(file=file, ignore=ignore):
|
134
|
+
yield
|
135
|
+
|
136
|
+
|
87
137
|
def parse_jobscript_wait_spec(jobscripts: str) -> dict[int, list[int]]:
|
88
138
|
"""
|
89
139
|
Parse a jobscript wait specification.
|
@@ -118,6 +168,7 @@ def _make_API_CLI(app: BaseApp):
|
|
118
168
|
@ts_name_fmt_option
|
119
169
|
@variables_option
|
120
170
|
@make_status_opt
|
171
|
+
@add_sub_opt
|
121
172
|
def make_workflow(
|
122
173
|
template_file_or_str: str,
|
123
174
|
string: bool,
|
@@ -130,6 +181,7 @@ def _make_API_CLI(app: BaseApp):
|
|
130
181
|
ts_name_fmt: str | None = None,
|
131
182
|
variables: list[tuple[str, str]] | None = None,
|
132
183
|
status: bool = True,
|
184
|
+
add_submission: bool = False,
|
133
185
|
):
|
134
186
|
"""Generate a new {app_name} workflow.
|
135
187
|
|
@@ -149,7 +201,9 @@ def _make_API_CLI(app: BaseApp):
|
|
149
201
|
ts_name_fmt=ts_name_fmt,
|
150
202
|
variables=dict(variables) if variables is not None else None,
|
151
203
|
status=status,
|
204
|
+
add_submission=add_submission,
|
152
205
|
)
|
206
|
+
assert isinstance(wk, Workflow)
|
153
207
|
click.echo(wk.path)
|
154
208
|
|
155
209
|
@click.command(name="go")
|
@@ -297,6 +351,20 @@ def _make_workflow_submission_jobscript_CLI(app: BaseApp):
|
|
297
351
|
with job.jobscript_path.open("rt") as fp:
|
298
352
|
click.echo(fp.read())
|
299
353
|
|
354
|
+
@jobscript.command()
|
355
|
+
@jobscript_std_array_idx_opt
|
356
|
+
@_pass_js
|
357
|
+
def stdout(job: Jobscript, array_idx: int):
|
358
|
+
"""Print the contents of the standard output stream file."""
|
359
|
+
job.print_stdout(array_idx=array_idx)
|
360
|
+
|
361
|
+
@jobscript.command()
|
362
|
+
@jobscript_std_array_idx_opt
|
363
|
+
@_pass_js
|
364
|
+
def stderr(job: Jobscript, array_idx: int):
|
365
|
+
"""Print the contents of the standard error stream file."""
|
366
|
+
job.print_stderr(array_idx=array_idx)
|
367
|
+
|
300
368
|
_set_help_name(jobscript, app)
|
301
369
|
return jobscript
|
302
370
|
|
@@ -346,6 +414,49 @@ def _make_workflow_submission_CLI(app: BaseApp):
|
|
346
414
|
"""Show active jobscripts and their jobscript-element states."""
|
347
415
|
pprint(sb.get_active_jobscripts(as_json=True))
|
348
416
|
|
417
|
+
@submission.command()
|
418
|
+
@_pass_submission
|
419
|
+
def get_scheduler_job_IDs(sb: Submission):
|
420
|
+
"""Print jobscript scheduler job IDs."""
|
421
|
+
job_IDs = sb.get_scheduler_job_IDs()
|
422
|
+
if job_IDs:
|
423
|
+
print("\n".join(job_IDs))
|
424
|
+
|
425
|
+
@submission.command()
|
426
|
+
@_pass_submission
|
427
|
+
def get_process_IDs(sb: Submission):
|
428
|
+
"""Print jobscript process IDs."""
|
429
|
+
proc_IDs = sb.get_process_IDs()
|
430
|
+
if proc_IDs:
|
431
|
+
print("\n".join(str(i) for i in proc_IDs))
|
432
|
+
|
433
|
+
@submission.command()
|
434
|
+
@list_js_max_js_opt
|
435
|
+
@list_js_jobscripts_opt
|
436
|
+
@list_js_width_opt
|
437
|
+
@_pass_submission
|
438
|
+
def list_jobscripts(
|
439
|
+
sb: Submission, max_js: int | None, jobscripts: str | None, width: int | None
|
440
|
+
):
|
441
|
+
"""Print a table listing jobscripts and associated information."""
|
442
|
+
jobscripts_ = [int(i) for i in jobscripts.split(",")] if jobscripts else None
|
443
|
+
sb.list_jobscripts(max_js=max_js, jobscripts=jobscripts_, width=width)
|
444
|
+
|
445
|
+
@submission.command()
|
446
|
+
@list_task_js_max_js_opt
|
447
|
+
@list_task_js_task_names_opt
|
448
|
+
@list_js_width_opt
|
449
|
+
@_pass_submission
|
450
|
+
def list_task_jobscripts(
|
451
|
+
sb: Submission,
|
452
|
+
max_js: int | None,
|
453
|
+
task_names: str | None,
|
454
|
+
width: int | None,
|
455
|
+
):
|
456
|
+
"""Print a table listing tasks and their associated jobscripts."""
|
457
|
+
task_names_ = list(task_names.split(",")) if task_names else None
|
458
|
+
sb.list_task_jobscripts(task_names=task_names_, max_js=max_js, width=width)
|
459
|
+
|
349
460
|
_set_help_name(submission, app)
|
350
461
|
submission.add_command(_make_workflow_submission_jobscript_CLI(app))
|
351
462
|
return submission
|
@@ -399,6 +510,27 @@ def _make_workflow_CLI(app: BaseApp):
|
|
399
510
|
if print_idx:
|
400
511
|
click.echo(out)
|
401
512
|
|
513
|
+
@workflow.command(name="add-submission")
|
514
|
+
@js_parallelism_option
|
515
|
+
@tasks_opt
|
516
|
+
@force_arr_opt
|
517
|
+
@submit_status_opt
|
518
|
+
@click.pass_context
|
519
|
+
def add_submission(
|
520
|
+
ctx,
|
521
|
+
js_parallelism=None,
|
522
|
+
tasks=None,
|
523
|
+
force_array=False,
|
524
|
+
status=True,
|
525
|
+
):
|
526
|
+
"""Add a new submission to the workflow, but do not submit."""
|
527
|
+
ctx.obj["workflow"].add_submission(
|
528
|
+
JS_parallelism=js_parallelism,
|
529
|
+
tasks=tasks,
|
530
|
+
force_array=force_array,
|
531
|
+
status=status,
|
532
|
+
)
|
533
|
+
|
402
534
|
@workflow.command(name="wait")
|
403
535
|
@click.option(
|
404
536
|
"-j",
|
@@ -525,6 +657,72 @@ def _make_workflow_CLI(app: BaseApp):
|
|
525
657
|
"""Rechunk the parameters/base array."""
|
526
658
|
wf.rechunk_parameter_base(backup=backup, chunk_size=chunk_size, status=status)
|
527
659
|
|
660
|
+
@workflow.command()
|
661
|
+
@_pass_workflow
|
662
|
+
def get_scheduler_job_IDs(wf: Workflow):
|
663
|
+
"""Print jobscript scheduler job IDs from all submissions of this workflow."""
|
664
|
+
job_IDs = wf.get_scheduler_job_IDs()
|
665
|
+
if job_IDs:
|
666
|
+
print("\n".join(job_IDs))
|
667
|
+
|
668
|
+
@workflow.command()
|
669
|
+
@_pass_workflow
|
670
|
+
def get_process_IDs(wf: Workflow):
|
671
|
+
"""Print jobscript process IDs from all submissions of this workflow."""
|
672
|
+
proc_IDs = wf.get_process_IDs()
|
673
|
+
if proc_IDs:
|
674
|
+
print("\n".join(str(i) for i in proc_IDs))
|
675
|
+
|
676
|
+
@workflow.command()
|
677
|
+
@click.option(
|
678
|
+
"--sub-idx",
|
679
|
+
type=click.INT,
|
680
|
+
default=0,
|
681
|
+
help="Submission index whose jobscripts are to be shown.",
|
682
|
+
)
|
683
|
+
@list_js_max_js_opt
|
684
|
+
@list_js_jobscripts_opt
|
685
|
+
@list_js_width_opt
|
686
|
+
@_pass_workflow
|
687
|
+
def list_jobscripts(
|
688
|
+
wf: Workflow,
|
689
|
+
sub_idx: int,
|
690
|
+
max_js: int | None,
|
691
|
+
jobscripts: str | None,
|
692
|
+
width: int | None,
|
693
|
+
):
|
694
|
+
"""Print a table listing jobscripts and associated information from the specified
|
695
|
+
submission."""
|
696
|
+
jobscripts_ = [int(i) for i in jobscripts.split(",")] if jobscripts else None
|
697
|
+
wf.list_jobscripts(
|
698
|
+
sub_idx=sub_idx, max_js=max_js, jobscripts=jobscripts_, width=width
|
699
|
+
)
|
700
|
+
|
701
|
+
@workflow.command()
|
702
|
+
@click.option(
|
703
|
+
"--sub-idx",
|
704
|
+
type=click.INT,
|
705
|
+
default=0,
|
706
|
+
help="Submission index whose tasks are to be shown.",
|
707
|
+
)
|
708
|
+
@list_task_js_max_js_opt
|
709
|
+
@list_task_js_task_names_opt
|
710
|
+
@list_js_width_opt
|
711
|
+
@_pass_workflow
|
712
|
+
def list_task_jobscripts(
|
713
|
+
wf: Workflow,
|
714
|
+
sub_idx: int,
|
715
|
+
max_js: int | None,
|
716
|
+
task_names: str | None,
|
717
|
+
width: int | None,
|
718
|
+
):
|
719
|
+
"""Print a table listing tasks and their associated jobscripts from the specified
|
720
|
+
submission."""
|
721
|
+
task_names_ = list(task_names.split(",")) if task_names else None
|
722
|
+
wf.list_task_jobscripts(
|
723
|
+
sub_idx=sub_idx, task_names=task_names_, max_js=max_js, width=width
|
724
|
+
)
|
725
|
+
|
528
726
|
_set_help_name(workflow, app)
|
529
727
|
workflow.add_command(_make_workflow_submission_CLI(app))
|
530
728
|
return workflow
|
@@ -606,6 +804,20 @@ def _make_internal_CLI(app: BaseApp):
|
|
606
804
|
"""Get the invocation command for this app instance."""
|
607
805
|
click.echo(app.run_time_info.invocation_command)
|
608
806
|
|
807
|
+
@internal.command()
|
808
|
+
@click.pass_context
|
809
|
+
@click.option("--raise", "raise_opt", is_flag=True)
|
810
|
+
@click.option("--click-exit-code", type=click.INT)
|
811
|
+
@click.option("--sleep", type=click.INT)
|
812
|
+
def noop(ctx, raise_opt, click_exit_code, sleep):
|
813
|
+
"""Used only in CLI tests."""
|
814
|
+
if raise_opt:
|
815
|
+
raise ValueError("internal noop raised!")
|
816
|
+
elif click_exit_code is not None:
|
817
|
+
ctx.exit(click_exit_code)
|
818
|
+
elif sleep:
|
819
|
+
time.sleep(sleep)
|
820
|
+
|
609
821
|
@internal.group()
|
610
822
|
@click.argument("path", type=click.Path(exists=True))
|
611
823
|
@click.pass_context
|
@@ -615,38 +827,51 @@ def _make_internal_CLI(app: BaseApp):
|
|
615
827
|
|
616
828
|
@workflow.command()
|
617
829
|
@_pass_workflow
|
618
|
-
@click.pass_context
|
619
830
|
@click.argument("submission_idx", type=click.INT)
|
620
831
|
@click.argument("jobscript_idx", type=click.INT)
|
621
|
-
@click.argument("
|
622
|
-
@click.argument("
|
623
|
-
|
624
|
-
|
832
|
+
@click.argument("block_idx", type=click.INT)
|
833
|
+
@click.argument("block_action_idx", type=click.INT)
|
834
|
+
@click.argument("run_id", type=click.INT)
|
835
|
+
def execute_run(
|
625
836
|
wf: Workflow,
|
626
837
|
submission_idx: int,
|
627
838
|
jobscript_idx: int,
|
628
|
-
|
629
|
-
|
839
|
+
block_idx: int,
|
840
|
+
block_action_idx: int,
|
841
|
+
run_id: int,
|
630
842
|
):
|
631
|
-
app.CLI_logger.info(f"
|
632
|
-
wf.
|
633
|
-
submission_idx,
|
634
|
-
jobscript_idx,
|
635
|
-
|
636
|
-
|
843
|
+
app.CLI_logger.info(f"execute commands for EAR ID {run_id!r}.")
|
844
|
+
wf.execute_run(
|
845
|
+
submission_idx=submission_idx,
|
846
|
+
block_act_key=(jobscript_idx, block_idx, block_action_idx),
|
847
|
+
run_ID=run_id,
|
848
|
+
)
|
849
|
+
|
850
|
+
@workflow.command()
|
851
|
+
@_pass_workflow
|
852
|
+
@click.argument("submission_idx", type=click.INT)
|
853
|
+
@click.argument("jobscript_idx", type=click.INT)
|
854
|
+
def execute_combined_runs(
|
855
|
+
wf: Workflow,
|
856
|
+
submission_idx: int,
|
857
|
+
jobscript_idx: int,
|
858
|
+
):
|
859
|
+
app.CLI_logger.info(
|
860
|
+
f"execute command for combined scripts of jobscript {jobscript_idx}."
|
861
|
+
)
|
862
|
+
wf.execute_combined_runs(
|
863
|
+
submission_idx=submission_idx,
|
864
|
+
jobscript_idx=jobscript_idx,
|
637
865
|
)
|
638
|
-
ctx.exit()
|
639
866
|
|
640
867
|
@workflow.command()
|
641
868
|
@_pass_workflow
|
642
|
-
@click.pass_context
|
643
869
|
@click.argument("name")
|
644
870
|
@click.argument("value")
|
645
871
|
@click.argument("ear_id", type=click.INT)
|
646
872
|
@click.argument("cmd_idx", type=click.INT)
|
647
873
|
@click.option("--stderr", is_flag=True, default=False)
|
648
874
|
def save_parameter(
|
649
|
-
ctx: click.Context,
|
650
875
|
wf: Workflow,
|
651
876
|
name: str,
|
652
877
|
value: str,
|
@@ -668,71 +893,7 @@ def _make_internal_CLI(app: BaseApp):
|
|
668
893
|
stderr=stderr,
|
669
894
|
)
|
670
895
|
app.CLI_logger.debug(f"save parameter processed value is: {value!r}")
|
671
|
-
|
672
|
-
|
673
|
-
@workflow.command()
|
674
|
-
@_pass_workflow
|
675
|
-
@click.pass_context
|
676
|
-
@click.argument("ear_id", type=click.INT)
|
677
|
-
def set_EAR_start(ctx: click.Context, wf: Workflow, ear_id: int):
|
678
|
-
app.CLI_logger.info(f"set EAR start for EAR ID {ear_id!r}.")
|
679
|
-
wf.set_EAR_start(ear_id)
|
680
|
-
ctx.exit()
|
681
|
-
|
682
|
-
@workflow.command()
|
683
|
-
@_pass_workflow
|
684
|
-
@click.pass_context
|
685
|
-
@click.argument("js_idx", type=click.INT)
|
686
|
-
@click.argument("js_act_idx", type=click.INT)
|
687
|
-
@click.argument("ear_id", type=click.INT)
|
688
|
-
@click.argument("exit_code", type=click.INT)
|
689
|
-
def set_EAR_end(
|
690
|
-
ctx: click.Context,
|
691
|
-
wf: Workflow,
|
692
|
-
js_idx: int,
|
693
|
-
js_act_idx: int,
|
694
|
-
ear_id: int,
|
695
|
-
exit_code: int,
|
696
|
-
):
|
697
|
-
app.CLI_logger.info(
|
698
|
-
f"set EAR end for EAR ID {ear_id!r} with exit code {exit_code!r}."
|
699
|
-
)
|
700
|
-
wf.set_EAR_end(
|
701
|
-
js_idx=js_idx,
|
702
|
-
js_act_idx=js_act_idx,
|
703
|
-
EAR_ID=ear_id,
|
704
|
-
exit_code=exit_code,
|
705
|
-
)
|
706
|
-
ctx.exit()
|
707
|
-
|
708
|
-
@workflow.command()
|
709
|
-
@_pass_workflow
|
710
|
-
@click.pass_context
|
711
|
-
@click.argument("ear_id", type=click.INT)
|
712
|
-
def set_EAR_skip(ctx: click.Context, wf: Workflow, ear_id: int):
|
713
|
-
app.CLI_logger.info(f"set EAR skip for EAR ID {ear_id!r}.")
|
714
|
-
wf.set_EAR_skip(ear_id)
|
715
|
-
ctx.exit()
|
716
|
-
|
717
|
-
@workflow.command()
|
718
|
-
@_pass_workflow
|
719
|
-
@click.pass_context
|
720
|
-
@click.argument("ear_id", type=click.INT)
|
721
|
-
def get_EAR_skipped(ctx: click.Context, wf: Workflow, ear_id: int):
|
722
|
-
"""Return 1 if the given EAR is to be skipped, else return 0."""
|
723
|
-
app.CLI_logger.info(f"get EAR skip for EAR ID {ear_id!r}.")
|
724
|
-
click.echo(int(wf.get_EAR_skipped(ear_id)))
|
725
|
-
|
726
|
-
@workflow.command()
|
727
|
-
@_pass_workflow
|
728
|
-
@click.pass_context
|
729
|
-
@click.argument("loop_name", type=click.STRING)
|
730
|
-
@click.argument("ear_id", type=click.INT)
|
731
|
-
def check_loop(ctx: click.Context, wf: Workflow, loop_name: str, ear_id: int):
|
732
|
-
"""Check if an iteration has met its loop's termination condition."""
|
733
|
-
app.CLI_logger.info(f"check_loop for loop {loop_name!r} and EAR ID {ear_id!r}.")
|
734
|
-
wf.check_loop_termination(loop_name, ear_id)
|
735
|
-
ctx.exit()
|
896
|
+
wf.save_parameter(name=name, value=value, EAR_ID=ear_id)
|
736
897
|
|
737
898
|
# TODO: in general, maybe the workflow command group can expose the simple Workflow
|
738
899
|
# properties; maybe use a decorator on the Workflow property object to signify
|
@@ -856,14 +1017,15 @@ def _make_cancel_CLI(app: BaseApp):
|
|
856
1017
|
@click.command()
|
857
1018
|
@click.argument("workflow_ref")
|
858
1019
|
@workflow_ref_type_opt
|
859
|
-
|
1020
|
+
@cancel_status_opt
|
1021
|
+
def cancel(workflow_ref: str, ref_type: str | None, status: bool):
|
860
1022
|
"""Stop all running jobscripts of the specified workflow.
|
861
1023
|
|
862
1024
|
WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
|
863
1025
|
workflow path.
|
864
1026
|
|
865
1027
|
"""
|
866
|
-
app.cancel(workflow_ref, ref_type)
|
1028
|
+
app.cancel(workflow_ref=workflow_ref, ref_is_path=ref_type, status=status)
|
867
1029
|
|
868
1030
|
return cancel
|
869
1031
|
|
@@ -1204,10 +1366,25 @@ def make_cli(app: BaseApp):
|
|
1204
1366
|
"`TimeIt.decorator` are included."
|
1205
1367
|
),
|
1206
1368
|
)
|
1369
|
+
@click.option(
|
1370
|
+
"--std-stream",
|
1371
|
+
help="File to redirect standard output and error to, and to print exceptions to.",
|
1372
|
+
)
|
1207
1373
|
@click.pass_context
|
1208
1374
|
def new_CLI(
|
1209
|
-
ctx: click.Context,
|
1375
|
+
ctx: click.Context,
|
1376
|
+
config_dir,
|
1377
|
+
config_key,
|
1378
|
+
with_config,
|
1379
|
+
timeit,
|
1380
|
+
timeit_file,
|
1381
|
+
std_stream: str,
|
1210
1382
|
):
|
1383
|
+
ctx.ensure_object(dict)
|
1384
|
+
|
1385
|
+
if std_stream:
|
1386
|
+
ctx.with_resource(redirect_std_to_file_click(std_stream))
|
1387
|
+
|
1211
1388
|
app.run_time_info.from_CLI = True
|
1212
1389
|
TimeIt.active = timeit or timeit_file
|
1213
1390
|
TimeIt.file_path = timeit_file
|
@@ -1250,6 +1427,8 @@ def make_cli(app: BaseApp):
|
|
1250
1427
|
env_source_file=None if env_source_file is None else Path(env_source_file),
|
1251
1428
|
)
|
1252
1429
|
|
1430
|
+
new_CLI.context_class = ErrorPropagatingClickContext
|
1431
|
+
|
1253
1432
|
new_CLI.__doc__ = app.description
|
1254
1433
|
new_CLI.add_command(get_config_CLI(app))
|
1255
1434
|
new_CLI.add_command(get_demo_software_CLI(app))
|
hpcflow/sdk/cli_common.py
CHANGED
@@ -8,6 +8,44 @@ from hpcflow.sdk.persistence.defaults import DEFAULT_STORE_FORMAT
|
|
8
8
|
from hpcflow.sdk.persistence.discovery import ALL_STORE_FORMATS
|
9
9
|
|
10
10
|
|
11
|
+
class BoolOrString(click.ParamType):
|
12
|
+
"""Custom Click parameter type to accepts a bool or a choice of strings."""
|
13
|
+
|
14
|
+
name = "bool-or-string"
|
15
|
+
|
16
|
+
def __init__(self, allowed_strings, true_strings=None, false_strings=None):
|
17
|
+
self.allowed_strings = allowed_strings
|
18
|
+
self.true_strings = true_strings if true_strings else ["true", "yes", "on"]
|
19
|
+
self.false_strings = false_strings if false_strings else ["false", "no", "off"]
|
20
|
+
|
21
|
+
def convert(self, value, param, ctx):
|
22
|
+
# Check if the value is a boolean
|
23
|
+
if isinstance(value, bool):
|
24
|
+
return value
|
25
|
+
|
26
|
+
# Normalize value to string
|
27
|
+
value = str(value).lower()
|
28
|
+
|
29
|
+
# Check if the value is one of the true strings
|
30
|
+
if value in self.true_strings:
|
31
|
+
return True
|
32
|
+
|
33
|
+
# Check if the value is one of the false strings
|
34
|
+
if value in self.false_strings:
|
35
|
+
return False
|
36
|
+
|
37
|
+
# If the value matches neither, it must be one of the expected strings
|
38
|
+
if value not in self.allowed_strings:
|
39
|
+
allowed_fmt = ", ".join(f"{i!r}" for i in self.allowed_strings)
|
40
|
+
self.fail(
|
41
|
+
message=f"{value} is not a valid boolean or one of {allowed_fmt}.",
|
42
|
+
param=param,
|
43
|
+
ctx=ctx,
|
44
|
+
)
|
45
|
+
|
46
|
+
return value
|
47
|
+
|
48
|
+
|
11
49
|
def sub_tasks_callback(ctx, param, value: str | None) -> list[int] | None:
|
12
50
|
"""
|
13
51
|
Parse subtasks.
|
@@ -95,12 +133,14 @@ variables_option = click.option(
|
|
95
133
|
js_parallelism_option = click.option(
|
96
134
|
"--js-parallelism",
|
97
135
|
help=(
|
98
|
-
"If True, allow multiple jobscripts to execute simultaneously.
|
99
|
-
"
|
100
|
-
"
|
101
|
-
"
|
136
|
+
"If True, allow multiple jobscripts to execute simultaneously. If "
|
137
|
+
"'scheduled'/'direct', only allow simultaneous execution of scheduled/direct "
|
138
|
+
"jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store type "
|
139
|
+
"does not support the `jobscript_parallelism` feature. If not set, jobscript "
|
140
|
+
"parallelism will be used if the store type supports it, for scheduled "
|
141
|
+
"jobscripts only."
|
102
142
|
),
|
103
|
-
type=
|
143
|
+
type=BoolOrString(["direct", "scheduled"]),
|
104
144
|
)
|
105
145
|
#: Standard option
|
106
146
|
wait_option = click.option(
|
@@ -144,6 +184,17 @@ submit_status_opt = click.option(
|
|
144
184
|
help="If True, display a live status to track submission progress.",
|
145
185
|
default=True,
|
146
186
|
)
|
187
|
+
#: Standard option
|
188
|
+
force_arr_opt = click.option(
|
189
|
+
"--force-array",
|
190
|
+
help=(
|
191
|
+
"Used to force the use of job arrays, even if the scheduler does not support it. "
|
192
|
+
"This is provided for testing purposes only."
|
193
|
+
),
|
194
|
+
is_flag=True,
|
195
|
+
default=False,
|
196
|
+
)
|
197
|
+
|
147
198
|
#: Standard option
|
148
199
|
make_status_opt = click.option(
|
149
200
|
"--status/--no-status",
|
@@ -151,6 +202,14 @@ make_status_opt = click.option(
|
|
151
202
|
default=True,
|
152
203
|
)
|
153
204
|
|
205
|
+
#: Standard option
|
206
|
+
add_sub_opt = click.option(
|
207
|
+
"--add-submission",
|
208
|
+
help=("If True, add a submission to the workflow (but do not submit)."),
|
209
|
+
is_flag=True,
|
210
|
+
default=False,
|
211
|
+
)
|
212
|
+
|
154
213
|
#: Standard option
|
155
214
|
zip_path_opt = click.option(
|
156
215
|
"--path",
|
@@ -211,6 +270,34 @@ rechunk_status_opt = click.option(
|
|
211
270
|
default=True,
|
212
271
|
help="If True, display a live status to track rechunking progress.",
|
213
272
|
)
|
273
|
+
cancel_status_opt = click.option(
|
274
|
+
"--status/--no-status",
|
275
|
+
default=True,
|
276
|
+
help="If True, display a live status to track cancel progress.",
|
277
|
+
)
|
278
|
+
|
279
|
+
list_js_max_js_opt = click.option(
|
280
|
+
"--max-js", type=click.INT, help="Display up to this jobscript only."
|
281
|
+
)
|
282
|
+
list_js_jobscripts_opt = click.option(
|
283
|
+
"--jobscripts", help="Comma-separated list of jobscript indices to show."
|
284
|
+
)
|
285
|
+
list_task_js_max_js_opt = click.option(
|
286
|
+
"--max-js", type=click.INT, help="Include jobscripts up to this jobscript only."
|
287
|
+
)
|
288
|
+
list_task_js_task_names_opt = click.option(
|
289
|
+
"--task-names", help="Comma-separated list of task name sub-strings to show."
|
290
|
+
)
|
291
|
+
list_js_width_opt = click.option(
|
292
|
+
"--width", type=click.INT, help="Width in characters of the table to print."
|
293
|
+
)
|
294
|
+
jobscript_std_array_idx_opt = click.option(
|
295
|
+
"--array-idx",
|
296
|
+
type=click.INT,
|
297
|
+
help=(
|
298
|
+
"For array jobs only, the job array index whose standard stream is to be printed."
|
299
|
+
),
|
300
|
+
)
|
214
301
|
|
215
302
|
|
216
303
|
def _add_doc_from_help(*args):
|
@@ -259,4 +346,11 @@ _add_doc_from_help(
|
|
259
346
|
rechunk_backup_opt,
|
260
347
|
rechunk_chunk_size_opt,
|
261
348
|
rechunk_status_opt,
|
349
|
+
cancel_status_opt,
|
350
|
+
list_js_max_js_opt,
|
351
|
+
list_js_jobscripts_opt,
|
352
|
+
list_task_js_max_js_opt,
|
353
|
+
list_task_js_task_names_opt,
|
354
|
+
list_js_width_opt,
|
355
|
+
jobscript_std_array_idx_opt,
|
262
356
|
)
|
hpcflow/sdk/config/callbacks.py
CHANGED
@@ -207,8 +207,45 @@ def check_load_data_files(config: Config, value: Any) -> None:
|
|
207
207
|
config._app.reload_template_components(warn=False)
|
208
208
|
|
209
209
|
|
210
|
+
def callback_log_file_path(config, value):
|
211
|
+
value = value.strip()
|
212
|
+
if value:
|
213
|
+
return config._resolve_path(value)
|
214
|
+
else:
|
215
|
+
return value
|
216
|
+
|
217
|
+
|
210
218
|
def callback_update_log_console_level(config: Config, value: str) -> None:
|
211
219
|
"""
|
212
220
|
Callback to set the logging level.
|
213
221
|
"""
|
214
|
-
config._app.log.update_console_level(value)
|
222
|
+
config._app.log.update_console_level(new_level=value)
|
223
|
+
|
224
|
+
|
225
|
+
def callback_unset_log_console_level(config: Config) -> None:
|
226
|
+
"""Reset the console handler to the default level."""
|
227
|
+
config._app.log.update_console_level()
|
228
|
+
|
229
|
+
|
230
|
+
def callback_update_log_file_level(config: Config, value: str) -> None:
|
231
|
+
"""Callback to set the level of the log file handler."""
|
232
|
+
config._app.log.update_file_level(new_level=value)
|
233
|
+
|
234
|
+
|
235
|
+
def callback_update_log_file_path(config: Config, value: str) -> None:
|
236
|
+
"""
|
237
|
+
Callback to update the log file path, or remove the file handler if no path specifed.
|
238
|
+
"""
|
239
|
+
config._app.log.remove_file_handler()
|
240
|
+
if value:
|
241
|
+
config._app.log.add_file_logger(path=value, level=config.get("log_file_level"))
|
242
|
+
|
243
|
+
|
244
|
+
def callback_unset_log_file_level(config: Config) -> None:
|
245
|
+
"""Callback to reset the file handler to the default level."""
|
246
|
+
config._app.log.update_file_level()
|
247
|
+
|
248
|
+
|
249
|
+
def callback_unset_log_file_path(config: Config) -> None:
|
250
|
+
"""Callback to remove the log file handler."""
|
251
|
+
config._app.log.remove_file_handler()
|