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.
Files changed (130) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
  2. hpcflow/_version.py +1 -1
  3. hpcflow/data/scripts/bad_script.py +2 -0
  4. hpcflow/data/scripts/do_nothing.py +2 -0
  5. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  6. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  7. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  8. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  11. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  12. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  13. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  15. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  16. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  23. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  24. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  25. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  26. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  27. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  28. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  29. hpcflow/data/scripts/script_exit_test.py +5 -0
  30. hpcflow/data/template_components/environments.yaml +1 -1
  31. hpcflow/sdk/__init__.py +5 -0
  32. hpcflow/sdk/app.py +150 -89
  33. hpcflow/sdk/cli.py +263 -84
  34. hpcflow/sdk/cli_common.py +99 -5
  35. hpcflow/sdk/config/callbacks.py +38 -1
  36. hpcflow/sdk/config/config.py +102 -13
  37. hpcflow/sdk/config/errors.py +19 -5
  38. hpcflow/sdk/config/types.py +3 -0
  39. hpcflow/sdk/core/__init__.py +25 -1
  40. hpcflow/sdk/core/actions.py +914 -262
  41. hpcflow/sdk/core/cache.py +76 -34
  42. hpcflow/sdk/core/command_files.py +14 -128
  43. hpcflow/sdk/core/commands.py +35 -6
  44. hpcflow/sdk/core/element.py +122 -50
  45. hpcflow/sdk/core/errors.py +58 -2
  46. hpcflow/sdk/core/execute.py +207 -0
  47. hpcflow/sdk/core/loop.py +408 -50
  48. hpcflow/sdk/core/loop_cache.py +4 -4
  49. hpcflow/sdk/core/parameters.py +382 -37
  50. hpcflow/sdk/core/run_dir_files.py +13 -40
  51. hpcflow/sdk/core/skip_reason.py +7 -0
  52. hpcflow/sdk/core/task.py +119 -30
  53. hpcflow/sdk/core/task_schema.py +68 -0
  54. hpcflow/sdk/core/test_utils.py +66 -27
  55. hpcflow/sdk/core/types.py +54 -1
  56. hpcflow/sdk/core/utils.py +78 -7
  57. hpcflow/sdk/core/workflow.py +1538 -336
  58. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  59. hpcflow/sdk/demo/cli.py +7 -0
  60. hpcflow/sdk/helper/cli.py +1 -0
  61. hpcflow/sdk/log.py +42 -15
  62. hpcflow/sdk/persistence/base.py +405 -53
  63. hpcflow/sdk/persistence/json.py +177 -52
  64. hpcflow/sdk/persistence/pending.py +237 -69
  65. hpcflow/sdk/persistence/store_resource.py +3 -2
  66. hpcflow/sdk/persistence/types.py +15 -4
  67. hpcflow/sdk/persistence/zarr.py +928 -81
  68. hpcflow/sdk/submission/jobscript.py +1408 -489
  69. hpcflow/sdk/submission/schedulers/__init__.py +40 -5
  70. hpcflow/sdk/submission/schedulers/direct.py +33 -19
  71. hpcflow/sdk/submission/schedulers/sge.py +51 -16
  72. hpcflow/sdk/submission/schedulers/slurm.py +44 -16
  73. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  74. hpcflow/sdk/submission/shells/base.py +68 -20
  75. hpcflow/sdk/submission/shells/bash.py +222 -129
  76. hpcflow/sdk/submission/shells/powershell.py +200 -150
  77. hpcflow/sdk/submission/submission.py +852 -119
  78. hpcflow/sdk/submission/types.py +18 -21
  79. hpcflow/sdk/typing.py +24 -5
  80. hpcflow/sdk/utils/arrays.py +71 -0
  81. hpcflow/sdk/utils/deferred_file.py +55 -0
  82. hpcflow/sdk/utils/hashing.py +16 -0
  83. hpcflow/sdk/utils/patches.py +12 -0
  84. hpcflow/sdk/utils/strings.py +33 -0
  85. hpcflow/tests/api/test_api.py +32 -0
  86. hpcflow/tests/conftest.py +19 -0
  87. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  88. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  89. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  90. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  91. hpcflow/tests/scripts/test_main_scripts.py +821 -70
  92. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  93. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  94. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
  95. hpcflow/tests/unit/test_action.py +176 -0
  96. hpcflow/tests/unit/test_app.py +20 -0
  97. hpcflow/tests/unit/test_cache.py +46 -0
  98. hpcflow/tests/unit/test_cli.py +133 -0
  99. hpcflow/tests/unit/test_config.py +122 -1
  100. hpcflow/tests/unit/test_element_iteration.py +47 -0
  101. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  102. hpcflow/tests/unit/test_loop.py +1332 -27
  103. hpcflow/tests/unit/test_meta_task.py +325 -0
  104. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  105. hpcflow/tests/unit/test_parameter.py +13 -0
  106. hpcflow/tests/unit/test_persistence.py +190 -8
  107. hpcflow/tests/unit/test_run.py +109 -3
  108. hpcflow/tests/unit/test_run_directories.py +29 -0
  109. hpcflow/tests/unit/test_shell.py +20 -0
  110. hpcflow/tests/unit/test_submission.py +5 -76
  111. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  112. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  113. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  114. hpcflow/tests/unit/utils/test_patches.py +5 -0
  115. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  116. hpcflow/tests/workflows/__init__.py +0 -0
  117. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  118. hpcflow/tests/workflows/test_jobscript.py +332 -0
  119. hpcflow/tests/workflows/test_run_status.py +198 -0
  120. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  121. hpcflow/tests/workflows/test_submission.py +140 -0
  122. hpcflow/tests/workflows/test_workflows.py +142 -2
  123. hpcflow/tests/workflows/test_zip.py +18 -0
  124. hpcflow/viz_demo.ipynb +6587 -3
  125. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +7 -4
  126. hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
  127. hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
  128. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
  129. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
  130. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/app.py CHANGED
@@ -15,7 +15,7 @@ from contextlib import contextmanager
15
15
  from pathlib import Path
16
16
  import sys
17
17
  from tempfile import TemporaryDirectory
18
- from typing import Any, TypeVar, Generic, cast, TYPE_CHECKING
18
+ from typing import Any, TypeVar, Generic, cast, TYPE_CHECKING, Literal
19
19
  import warnings
20
20
  import zipfile
21
21
  from platformdirs import user_cache_path, user_data_dir
@@ -39,6 +39,7 @@ from hpcflow.sdk.core.utils import (
39
39
  read_JSON_file,
40
40
  write_YAML_file,
41
41
  write_JSON_file,
42
+ redirect_std_to_file as redirect_std_to_file_hpcflow,
42
43
  parse_timestamp,
43
44
  get_file_context,
44
45
  open_text_resource,
@@ -46,6 +47,7 @@ from hpcflow.sdk.core.utils import (
46
47
  from hpcflow.sdk import sdk_classes, sdk_funcs, get_SDK_logger
47
48
  from hpcflow.sdk.config import Config, ConfigFile
48
49
  from hpcflow.sdk.core import ALL_TEMPLATE_FORMATS
50
+ from .core.workflow import Workflow as _Workflow
49
51
  from hpcflow.sdk.log import AppLog, TimeIt
50
52
  from hpcflow.sdk.persistence.defaults import DEFAULT_STORE_FORMAT
51
53
  from hpcflow.sdk.persistence.base import TEMPLATE_COMP_TYPES
@@ -71,6 +73,7 @@ if TYPE_CHECKING:
71
73
  KnownSubmissionItem,
72
74
  PathLike,
73
75
  TemplateComponents,
76
+ MakeWorkflowCommonArgs,
74
77
  )
75
78
  from .config.config import ConfigOptions
76
79
  from .core.actions import (
@@ -132,6 +135,7 @@ if TYPE_CHECKING:
132
135
  ResourceSpec,
133
136
  SchemaOutput,
134
137
  ValueSequence,
138
+ MultiPathSequence,
135
139
  SchemaInput,
136
140
  )
137
141
  from .core.rule import Rule
@@ -146,12 +150,9 @@ if TYPE_CHECKING:
146
150
  ElementSet,
147
151
  )
148
152
  from .core.task_schema import TaskSchema, TaskObjective
149
- from .core.workflow import (
150
- Workflow as _Workflow,
151
- WorkflowTemplate as _WorkflowTemplate,
152
- )
153
+ from .core.workflow import WorkflowTemplate as _WorkflowTemplate
153
154
  from .submission.jobscript import Jobscript
154
- from .submission.submission import Submission
155
+ from .submission.submission import Submission as _Submission # TODO: why?
155
156
  from .submission.schedulers import Scheduler, QueuedScheduler
156
157
  from .submission.schedulers.direct import DirectPosix, DirectWindows
157
158
  from .submission.schedulers.sge import SGEPosix
@@ -176,7 +177,8 @@ if TYPE_CHECKING:
176
177
  store_kwargs: dict[str, Any] | None = None,
177
178
  variables: dict[str, str] | None = None,
178
179
  status: bool = True,
179
- ) -> _Workflow:
180
+ add_submission: bool = False,
181
+ ) -> _Workflow | _Submission | None:
180
182
  ...
181
183
 
182
184
  class _MakeDemoWorkflow(Protocol):
@@ -195,7 +197,8 @@ if TYPE_CHECKING:
195
197
  store_kwargs: dict[str, Any] | None = None,
196
198
  variables: dict[str, str] | None = None,
197
199
  status: bool = True,
198
- ) -> _Workflow:
200
+ add_submission: bool = False,
201
+ ) -> _Workflow | _Submission | None:
199
202
  ...
200
203
 
201
204
  class _MakeAndSubmitWorkflow(Protocol):
@@ -296,6 +299,7 @@ if TYPE_CHECKING:
296
299
  self,
297
300
  workflow_ref: int | str | Path,
298
301
  ref_is_path: str | None = None,
302
+ status: bool = False,
299
303
  ) -> None:
300
304
  ...
301
305
 
@@ -766,6 +770,15 @@ class BaseApp(metaclass=Singleton):
766
770
  """
767
771
  return self._get_app_core_class("ValueSequence")
768
772
 
773
+ @property
774
+ def MultiPathSequence(self) -> type[MultiPathSequence]:
775
+ """
776
+ The :class:`MultiPathSequence` class.
777
+
778
+ :meta private:
779
+ """
780
+ return self._get_app_core_class("MultiPathSequence")
781
+
769
782
  @property
770
783
  def SchemaInput(self) -> type[SchemaInput]:
771
784
  """
@@ -1172,7 +1185,7 @@ class BaseApp(metaclass=Singleton):
1172
1185
  return self._get_app_core_class("Jobscript")
1173
1186
 
1174
1187
  @property
1175
- def Submission(self) -> type[Submission]:
1188
+ def Submission(self) -> type[_Submission]:
1176
1189
  """
1177
1190
  The :class:`Submission` class.
1178
1191
 
@@ -1265,11 +1278,15 @@ class BaseApp(metaclass=Singleton):
1265
1278
  String variables to substitute in `template_file_or_str`.
1266
1279
  status: bool
1267
1280
  If True, display a live status to track workflow creation progress.
1281
+ add_submission
1282
+ If True, add a submission to the workflow (but do not submit).
1268
1283
 
1269
1284
  Returns
1270
1285
  -------
1271
1286
  Workflow
1272
- The created workflow.
1287
+ The created workflow, if `add_submission` is `False`.
1288
+ Submission
1289
+ The created submission object, if `add_submission` is `True`.
1273
1290
  """
1274
1291
  return self.__get_app_func("make_workflow")
1275
1292
 
@@ -1310,11 +1327,15 @@ class BaseApp(metaclass=Singleton):
1310
1327
  String variables to substitute in the demo workflow template file.
1311
1328
  status: bool
1312
1329
  If True, display a live status to track workflow creation progress.
1330
+ add_submission
1331
+ If True, add a submission to the workflow (but do not submit).
1313
1332
 
1314
1333
  Returns
1315
1334
  -------
1316
1335
  Workflow
1317
- The created workflow.
1336
+ The created workflow, if `add_submission` is `False`.
1337
+ Submission
1338
+ The created submission object, if `add_submission` is `True`.
1318
1339
  """
1319
1340
  return self.__get_app_func("make_demo_workflow")
1320
1341
 
@@ -1580,6 +1601,8 @@ class BaseApp(metaclass=Singleton):
1580
1601
  Which workflow to cancel, by ID or path.
1581
1602
  ref_is_path: str
1582
1603
  One of "``id``", "``path``" or "``assume-id``" (the default)
1604
+ status: bool
1605
+ Whether to show a live status during cancel.
1583
1606
  """
1584
1607
  return self.__get_app_func("cancel")
1585
1608
 
@@ -2133,10 +2156,12 @@ class BaseApp(metaclass=Singleton):
2133
2156
  **overrides,
2134
2157
  )
2135
2158
  self.log.update_console_level(self.config.get("log_console_level"))
2136
- self.log.add_file_logger(
2137
- path=self.config.get("log_file_path"),
2138
- level=self.config.get("log_file_level"),
2139
- )
2159
+ log_file_path = self.config.get("log_file_path")
2160
+ if log_file_path:
2161
+ self.log.add_file_logger(
2162
+ path=log_file_path,
2163
+ level=self.config.get("log_file_level"),
2164
+ )
2140
2165
  self.logger.info(f"Configuration loaded from: {self.config.config_file_path}")
2141
2166
  self._ensure_user_data_hostname_dir()
2142
2167
 
@@ -2212,7 +2237,7 @@ class BaseApp(metaclass=Singleton):
2212
2237
  """
2213
2238
  if warn and not self.is_config_loaded:
2214
2239
  warnings.warn("Configuration is not loaded; loading.")
2215
- self.log.remove_file_handlers()
2240
+ self.log.remove_file_handler()
2216
2241
  self._config_files = {}
2217
2242
  self._load_config(config_dir, config_key, **overrides)
2218
2243
 
@@ -2651,7 +2676,8 @@ class BaseApp(metaclass=Singleton):
2651
2676
  store_kwargs: dict[str, Any] | None = None,
2652
2677
  variables: dict[str, str] | None = None,
2653
2678
  status: bool = True,
2654
- ) -> _Workflow:
2679
+ add_submission: bool = False,
2680
+ ) -> _Workflow | _Submission | None:
2655
2681
  """
2656
2682
  Generate a new {app_name} workflow from a file or string containing a workflow
2657
2683
  template parametrisation.
@@ -2690,11 +2716,15 @@ class BaseApp(metaclass=Singleton):
2690
2716
  String variables to substitute in `template_file_or_str`.
2691
2717
  status
2692
2718
  If True, display a live status to track workflow creation progress.
2719
+ add_submission
2720
+ If True, add a submission to the workflow (but do not submit).
2693
2721
 
2694
2722
  Returns
2695
2723
  -------
2696
2724
  Workflow
2697
- The created workflow.
2725
+ The created workflow, if `add_submission` is `False`.
2726
+ Submission
2727
+ The created submission object, if `add_submission` is `True`.
2698
2728
  """
2699
2729
  self.API_logger.info("make_workflow called")
2700
2730
 
@@ -2703,44 +2733,31 @@ class BaseApp(metaclass=Singleton):
2703
2733
  )
2704
2734
 
2705
2735
  with status_context as status_:
2736
+
2737
+ common: MakeWorkflowCommonArgs = {
2738
+ "path": str(path) if path else None,
2739
+ "name": name,
2740
+ "overwrite": overwrite,
2741
+ "store": store,
2742
+ "ts_fmt": ts_fmt,
2743
+ "ts_name_fmt": ts_name_fmt,
2744
+ "store_kwargs": store_kwargs,
2745
+ "variables": variables,
2746
+ "status": status_,
2747
+ }
2706
2748
  if not is_string:
2707
- return self.Workflow.from_file(
2749
+ wk = self.Workflow.from_file(
2708
2750
  template_path=template_file_or_str,
2709
2751
  template_format=template_format,
2710
- path=str(path) if path else None,
2711
- name=name,
2712
- overwrite=overwrite,
2713
- store=store,
2714
- ts_fmt=ts_fmt,
2715
- ts_name_fmt=ts_name_fmt,
2716
- store_kwargs=store_kwargs,
2717
- variables=variables,
2718
- status=status_,
2752
+ **common,
2719
2753
  )
2720
2754
  elif template_format == "json":
2721
- return self.Workflow.from_JSON_string(
2722
- JSON_str=str(template_file_or_str),
2723
- path=str(path) if path else None,
2724
- name=name,
2725
- overwrite=overwrite,
2726
- store=store,
2727
- ts_fmt=ts_fmt,
2728
- ts_name_fmt=ts_name_fmt,
2729
- store_kwargs=store_kwargs,
2730
- variables=variables,
2731
- status=status_,
2755
+ wk = self.Workflow.from_JSON_string(
2756
+ JSON_str=str(template_file_or_str), **common
2732
2757
  )
2733
2758
  elif template_format == "yaml":
2734
- return self.Workflow.from_YAML_string(
2735
- YAML_str=str(template_file_or_str),
2736
- path=str(path) if path else None,
2737
- name=name,
2738
- overwrite=overwrite,
2739
- store=store,
2740
- ts_fmt=ts_fmt,
2741
- ts_name_fmt=ts_name_fmt,
2742
- store_kwargs=store_kwargs,
2743
- variables=variables,
2759
+ wk = self.Workflow.from_YAML_string(
2760
+ YAML_str=str(template_file_or_str), **common
2744
2761
  )
2745
2762
  elif not template_format:
2746
2763
  raise ValueError(
@@ -2752,6 +2769,11 @@ class BaseApp(metaclass=Singleton):
2752
2769
  f"Template format {template_format!r} not understood. Available template "
2753
2770
  f"formats are {ALL_TEMPLATE_FORMATS!r}."
2754
2771
  )
2772
+ if add_submission:
2773
+ with wk._store.cached_load(), wk.batch_update():
2774
+ return wk._add_submission(status=status_)
2775
+
2776
+ return wk
2755
2777
 
2756
2778
  def _make_and_submit_workflow(
2757
2779
  self,
@@ -2766,7 +2788,7 @@ class BaseApp(metaclass=Singleton):
2766
2788
  ts_name_fmt: str | None = None,
2767
2789
  store_kwargs: dict[str, Any] | None = None,
2768
2790
  variables: dict[str, str] | None = None,
2769
- JS_parallelism: bool | None = None,
2791
+ JS_parallelism: bool | Literal["direct", "scheduled"] | None = None,
2770
2792
  wait: bool = False,
2771
2793
  add_to_known: bool = True,
2772
2794
  return_idx: bool = False,
@@ -2812,9 +2834,12 @@ class BaseApp(metaclass=Singleton):
2812
2834
  variables
2813
2835
  String variables to substitute in `template_file_or_str`.
2814
2836
  JS_parallelism
2815
- If True, allow multiple jobscripts to execute simultaneously. Raises if set to
2816
- True but the store type does not support the `jobscript_parallelism` feature. If
2817
- not set, jobscript parallelism will be used if the store type supports it.
2837
+ If True, allow multiple jobscripts to execute simultaneously. If
2838
+ 'scheduled'/'direct', only allow simultaneous execution of scheduled/direct
2839
+ jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store
2840
+ type does not support the `jobscript_parallelism` feature. If not set,
2841
+ jobscript parallelism will be used if the store type supports it, for
2842
+ scheduled jobscripts only.
2818
2843
  wait
2819
2844
  If True, this command will block until the workflow execution is complete.
2820
2845
  add_to_known
@@ -2855,6 +2880,7 @@ class BaseApp(metaclass=Singleton):
2855
2880
  variables=variables,
2856
2881
  status=status,
2857
2882
  )
2883
+ assert isinstance(wk, _Workflow)
2858
2884
  submitted_js = wk.submit(
2859
2885
  JS_parallelism=JS_parallelism,
2860
2886
  wait=wait,
@@ -2882,7 +2908,8 @@ class BaseApp(metaclass=Singleton):
2882
2908
  store_kwargs: dict[str, Any] | None = None,
2883
2909
  variables: dict[str, str] | None = None,
2884
2910
  status: bool = True,
2885
- ) -> _Workflow:
2911
+ add_submission: bool = False,
2912
+ ) -> _Workflow | _Submission | None:
2886
2913
  """
2887
2914
  Generate a new {app_name} workflow from a builtin demo workflow template.
2888
2915
 
@@ -2918,11 +2945,15 @@ class BaseApp(metaclass=Singleton):
2918
2945
  String variables to substitute in the demo workflow template file.
2919
2946
  status
2920
2947
  If True, display a live status to track workflow creation progress.
2948
+ add_submission
2949
+ If True, add a submission to the workflow (but do not submit).
2921
2950
 
2922
2951
  Returns
2923
2952
  -------
2924
2953
  Workflow
2925
- The created workflow.
2954
+ The created workflow, if `add_submission` is `False`.
2955
+ Submission
2956
+ The created submission object, if `add_submission` is `True`.
2926
2957
  """
2927
2958
  self.API_logger.info("make_demo_workflow called")
2928
2959
 
@@ -2933,7 +2964,7 @@ class BaseApp(metaclass=Singleton):
2933
2964
  with status_context as status_, self.get_demo_workflow_template_file(
2934
2965
  workflow_name
2935
2966
  ) as template_path:
2936
- return self.Workflow.from_file(
2967
+ wk = self.Workflow.from_file(
2937
2968
  template_path=template_path,
2938
2969
  template_format=template_format,
2939
2970
  path=str(path) if path else None,
@@ -2946,6 +2977,11 @@ class BaseApp(metaclass=Singleton):
2946
2977
  variables=variables,
2947
2978
  status=status_,
2948
2979
  )
2980
+ if add_submission:
2981
+ with wk._store.cached_load():
2982
+ with wk.batch_update():
2983
+ return wk._add_submission(status=status_)
2984
+ return wk
2949
2985
 
2950
2986
  def _make_and_submit_demo_workflow(
2951
2987
  self,
@@ -2959,7 +2995,7 @@ class BaseApp(metaclass=Singleton):
2959
2995
  ts_name_fmt: str | None = None,
2960
2996
  store_kwargs: dict[str, Any] | None = None,
2961
2997
  variables: dict[str, str] | None = None,
2962
- JS_parallelism: bool | None = None,
2998
+ JS_parallelism: bool | Literal["direct", "scheduled"] | None = None,
2963
2999
  wait: bool = False,
2964
3000
  add_to_known: bool = True,
2965
3001
  return_idx: bool = False,
@@ -3002,9 +3038,12 @@ class BaseApp(metaclass=Singleton):
3002
3038
  variables
3003
3039
  String variables to substitute in the demo workflow template file.
3004
3040
  JS_parallelism
3005
- If True, allow multiple jobscripts to execute simultaneously. Raises if set to
3006
- True but the store type does not support the `jobscript_parallelism` feature. If
3007
- not set, jobscript parallelism will be used if the store type supports it.
3041
+ If True, allow multiple jobscripts to execute simultaneously. If
3042
+ 'scheduled'/'direct', only allow simultaneous execution of scheduled/direct
3043
+ jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store
3044
+ type does not support the `jobscript_parallelism` feature. If not set,
3045
+ jobscript parallelism will be used if the store type supports it, for
3046
+ scheduled jobscripts only.
3008
3047
  wait
3009
3048
  If True, this command will block until the workflow execution is complete.
3010
3049
  add_to_known
@@ -3042,6 +3081,7 @@ class BaseApp(metaclass=Singleton):
3042
3081
  store_kwargs=store_kwargs,
3043
3082
  variables=variables,
3044
3083
  )
3084
+ assert isinstance(wk, _Workflow)
3045
3085
  submitted_js = wk.submit(
3046
3086
  JS_parallelism=JS_parallelism,
3047
3087
  wait=wait,
@@ -3059,7 +3099,7 @@ class BaseApp(metaclass=Singleton):
3059
3099
  def _submit_workflow(
3060
3100
  self,
3061
3101
  workflow_path: PathLike,
3062
- JS_parallelism: bool | None = None,
3102
+ JS_parallelism: bool | Literal["direct", "scheduled"] | None = None,
3063
3103
  wait: bool = False,
3064
3104
  return_idx: bool = False,
3065
3105
  tasks: list[int] | None = None,
@@ -3072,9 +3112,12 @@ class BaseApp(metaclass=Singleton):
3072
3112
  workflow_path:
3073
3113
  Path to an existing workflow.
3074
3114
  JS_parallelism:
3075
- If True, allow multiple jobscripts to execute simultaneously. Raises if set to
3076
- True but the store type does not support the `jobscript_parallelism` feature. If
3077
- not set, jobscript parallelism will be used if the store type supports it.
3115
+ If True, allow multiple jobscripts to execute simultaneously. If
3116
+ 'scheduled'/'direct', only allow simultaneous execution of scheduled/direct
3117
+ jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store
3118
+ type does not support the `jobscript_parallelism` feature. If not set,
3119
+ jobscript parallelism will be used if the store type supports it, for
3120
+ scheduled jobscripts only.
3078
3121
  wait:
3079
3122
  Whether to wait for the submission to complete.
3080
3123
  return_idx:
@@ -3193,7 +3236,8 @@ class BaseApp(metaclass=Singleton):
3193
3236
 
3194
3237
  # keys are (workflow path, submission index)
3195
3238
  active_jobscripts: dict[
3196
- tuple[str, int], Mapping[int, Mapping[int, JobscriptElementState]]
3239
+ tuple[str, int],
3240
+ Mapping[int, Mapping[int, Mapping[int, JobscriptElementState]]],
3197
3241
  ] = {}
3198
3242
  loaded_workflows: dict[str, _Workflow] = {} # keys are workflow path
3199
3243
 
@@ -3295,7 +3339,9 @@ class BaseApp(metaclass=Singleton):
3295
3339
  if file_dat_i["is_active"]:
3296
3340
  # check it really is active:
3297
3341
  run_key = (file_dat_i["path"], file_dat_i["sub_idx"])
3298
- act_i_js: Mapping[int, Mapping[int, JobscriptElementState]]
3342
+ act_i_js: Mapping[
3343
+ int, Mapping[int, Mapping[int, JobscriptElementState]]
3344
+ ]
3299
3345
  if run_key in active_jobscripts:
3300
3346
  act_i_js = active_jobscripts[run_key]
3301
3347
  else:
@@ -3521,25 +3567,27 @@ class BaseApp(metaclass=Singleton):
3521
3567
  style_wk_name = "grey42 strike" if deleted else style
3522
3568
  style_it = "italic grey42" if (no_access or not act_js) else "italic"
3523
3569
 
3524
- all_cells: dict[str, str | Text | Padding] = {}
3525
- if "status" in columns:
3526
- if act_js:
3527
- act_js_states = set(
3528
- js_state
3529
- for jsinf in act_js.values()
3530
- for js_state in jsinf.values()
3531
- )
3532
- all_cells["status"] = "/".join(
3533
- js_state.rich_repr for js_state in act_js_states
3534
- )
3570
+ all_cells: dict[str, str | Text | Padding] = {}
3571
+ if "status" in columns:
3572
+ if act_js:
3573
+ act_js_states = set(
3574
+ state_i
3575
+ for js_dat in act_js.values()
3576
+ for block_dat in js_dat.values()
3577
+ for state_i in block_dat.values()
3578
+ )
3579
+ all_cells["status"] = "/".join(
3580
+ js_state.rich_repr
3581
+ for js_state in sorted(act_js_states, key=lambda x: x.value)
3582
+ )
3583
+ else:
3584
+ if deleted:
3585
+ txt = "deleted"
3586
+ elif unloadable:
3587
+ txt = "unloadable"
3535
3588
  else:
3536
- if deleted:
3537
- txt = "deleted"
3538
- elif unloadable:
3539
- txt = "unloadable"
3540
- else:
3541
- txt = "inactive"
3542
- all_cells["status"] = Text(txt, style=style_it)
3589
+ txt = "inactive"
3590
+ all_cells["status"] = Text(txt, style=style_it)
3543
3591
 
3544
3592
  if "id" in columns:
3545
3593
  all_cells["id"] = Text(str(dat_i["local_id"]), style=style)
@@ -3591,7 +3639,9 @@ class BaseApp(metaclass=Singleton):
3591
3639
  )
3592
3640
  all_cells["actions_compact"] = " | ".join(
3593
3641
  f"[{k.colour}]{k.symbol}[/{k.colour}]:{v}" # type: ignore
3594
- for k, v in EAR_stat_count.items()
3642
+ for k, v in dict(
3643
+ sorted(EAR_stat_count.items(), key=lambda x: x[0].value)
3644
+ ).items()
3595
3645
  )
3596
3646
  else:
3597
3647
  all_cells["actions_compact"] = ""
@@ -3711,18 +3761,29 @@ class BaseApp(metaclass=Singleton):
3711
3761
  return path.resolve()
3712
3762
 
3713
3763
  def _cancel(
3714
- self, workflow_ref: int | str | Path, ref_is_path: str | None = None
3764
+ self,
3765
+ workflow_ref: int | str | Path,
3766
+ ref_is_path: str | None = None,
3767
+ status: bool = True,
3715
3768
  ) -> None:
3716
3769
  """
3717
3770
  Cancel the execution of a workflow submission.
3718
3771
 
3719
3772
  Parameters
3720
3773
  ----------
3721
- ref_is_path
3722
- One of "id", "path" or "assume-id" (the default)
3774
+ workflow_ref: int | str | Path
3775
+ Which workflow to cancel, by ID or path.
3776
+ ref_is_path: str
3777
+ One of "``id``", "``path``" or "``assume-id``" (the default)
3778
+ status: bool
3779
+ Whether to show a live status during cancel.
3723
3780
  """
3724
3781
  path = self._resolve_workflow_reference(str(workflow_ref), ref_is_path)
3725
- self.Workflow(path).cancel()
3782
+ self.Workflow(path).cancel(status=status)
3783
+
3784
+ @staticmethod
3785
+ def redirect_std_to_file(*args, **kwargs):
3786
+ return redirect_std_to_file_hpcflow(*args, **kwargs)
3726
3787
 
3727
3788
  def configure_env(
3728
3789
  self,