hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a200__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 (132) 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 +166 -92
  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 +136 -19
  57. hpcflow/sdk/core/workflow.py +1587 -356
  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/benchmark_script_runner.yaml +26 -0
  88. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  89. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  90. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  91. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  92. hpcflow/tests/scripts/test_main_scripts.py +821 -70
  93. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  94. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  95. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
  96. hpcflow/tests/unit/test_action.py +176 -0
  97. hpcflow/tests/unit/test_app.py +20 -0
  98. hpcflow/tests/unit/test_cache.py +46 -0
  99. hpcflow/tests/unit/test_cli.py +133 -0
  100. hpcflow/tests/unit/test_config.py +122 -1
  101. hpcflow/tests/unit/test_element_iteration.py +47 -0
  102. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  103. hpcflow/tests/unit/test_loop.py +1332 -27
  104. hpcflow/tests/unit/test_meta_task.py +325 -0
  105. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  106. hpcflow/tests/unit/test_parameter.py +13 -0
  107. hpcflow/tests/unit/test_persistence.py +190 -8
  108. hpcflow/tests/unit/test_run.py +109 -3
  109. hpcflow/tests/unit/test_run_directories.py +29 -0
  110. hpcflow/tests/unit/test_shell.py +20 -0
  111. hpcflow/tests/unit/test_submission.py +5 -76
  112. hpcflow/tests/unit/test_workflow_template.py +31 -0
  113. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  114. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  115. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  116. hpcflow/tests/unit/utils/test_patches.py +5 -0
  117. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  118. hpcflow/tests/workflows/__init__.py +0 -0
  119. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  120. hpcflow/tests/workflows/test_jobscript.py +332 -0
  121. hpcflow/tests/workflows/test_run_status.py +198 -0
  122. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  123. hpcflow/tests/workflows/test_submission.py +140 -0
  124. hpcflow/tests/workflows/test_workflows.py +142 -2
  125. hpcflow/tests/workflows/test_zip.py +18 -0
  126. hpcflow/viz_demo.ipynb +6587 -3
  127. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/METADATA +7 -4
  128. hpcflow_new2-0.2.0a200.dist-info/RECORD +222 -0
  129. hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
  130. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/LICENSE +0 -0
  131. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/WHEEL +0 -0
  132. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.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
 
@@ -2345,10 +2370,23 @@ class BaseApp(metaclass=Singleton):
2345
2370
  else:
2346
2371
  print(contents)
2347
2372
 
2348
- def load_demo_workflow(self, name: str) -> _WorkflowTemplate:
2349
- """Load a WorkflowTemplate object from a builtin demo template file."""
2373
+ def load_demo_workflow(
2374
+ self, name: str, variables: dict[str, str] | Literal[False] | None = None
2375
+ ) -> _WorkflowTemplate:
2376
+ """Load a WorkflowTemplate object from a builtin demo template file.
2377
+
2378
+ Parameters
2379
+ ----------
2380
+ name:
2381
+ Name of the demo workflow to load.
2382
+ variables:
2383
+ String variables to substitute in the demo workflow. Substitutions will be
2384
+ attempted if the file looks to contain variable references (like
2385
+ "<<var:name>>"). If set to `False`, no substitutions will occur, which may
2386
+ result in an invalid workflow template!
2387
+ """
2350
2388
  with self.get_demo_workflow_template_file(name) as path:
2351
- return self.WorkflowTemplate.from_file(path)
2389
+ return self.WorkflowTemplate.from_file(path, variables=variables)
2352
2390
 
2353
2391
  def template_components_from_json_like(
2354
2392
  self, json_like: dict[str, dict]
@@ -2651,7 +2689,8 @@ class BaseApp(metaclass=Singleton):
2651
2689
  store_kwargs: dict[str, Any] | None = None,
2652
2690
  variables: dict[str, str] | None = None,
2653
2691
  status: bool = True,
2654
- ) -> _Workflow:
2692
+ add_submission: bool = False,
2693
+ ) -> _Workflow | _Submission | None:
2655
2694
  """
2656
2695
  Generate a new {app_name} workflow from a file or string containing a workflow
2657
2696
  template parametrisation.
@@ -2690,11 +2729,15 @@ class BaseApp(metaclass=Singleton):
2690
2729
  String variables to substitute in `template_file_or_str`.
2691
2730
  status
2692
2731
  If True, display a live status to track workflow creation progress.
2732
+ add_submission
2733
+ If True, add a submission to the workflow (but do not submit).
2693
2734
 
2694
2735
  Returns
2695
2736
  -------
2696
2737
  Workflow
2697
- The created workflow.
2738
+ The created workflow, if `add_submission` is `False`.
2739
+ Submission
2740
+ The created submission object, if `add_submission` is `True`.
2698
2741
  """
2699
2742
  self.API_logger.info("make_workflow called")
2700
2743
 
@@ -2703,44 +2746,31 @@ class BaseApp(metaclass=Singleton):
2703
2746
  )
2704
2747
 
2705
2748
  with status_context as status_:
2749
+
2750
+ common: MakeWorkflowCommonArgs = {
2751
+ "path": str(path) if path else None,
2752
+ "name": name,
2753
+ "overwrite": overwrite,
2754
+ "store": store,
2755
+ "ts_fmt": ts_fmt,
2756
+ "ts_name_fmt": ts_name_fmt,
2757
+ "store_kwargs": store_kwargs,
2758
+ "variables": variables,
2759
+ "status": status_,
2760
+ }
2706
2761
  if not is_string:
2707
- return self.Workflow.from_file(
2762
+ wk = self.Workflow.from_file(
2708
2763
  template_path=template_file_or_str,
2709
2764
  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_,
2765
+ **common,
2719
2766
  )
2720
2767
  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_,
2768
+ wk = self.Workflow.from_JSON_string(
2769
+ JSON_str=str(template_file_or_str), **common
2732
2770
  )
2733
2771
  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,
2772
+ wk = self.Workflow.from_YAML_string(
2773
+ YAML_str=str(template_file_or_str), **common
2744
2774
  )
2745
2775
  elif not template_format:
2746
2776
  raise ValueError(
@@ -2752,6 +2782,11 @@ class BaseApp(metaclass=Singleton):
2752
2782
  f"Template format {template_format!r} not understood. Available template "
2753
2783
  f"formats are {ALL_TEMPLATE_FORMATS!r}."
2754
2784
  )
2785
+ if add_submission:
2786
+ with wk._store.cached_load(), wk.batch_update():
2787
+ return wk._add_submission(status=status_)
2788
+
2789
+ return wk
2755
2790
 
2756
2791
  def _make_and_submit_workflow(
2757
2792
  self,
@@ -2766,7 +2801,7 @@ class BaseApp(metaclass=Singleton):
2766
2801
  ts_name_fmt: str | None = None,
2767
2802
  store_kwargs: dict[str, Any] | None = None,
2768
2803
  variables: dict[str, str] | None = None,
2769
- JS_parallelism: bool | None = None,
2804
+ JS_parallelism: bool | Literal["direct", "scheduled"] | None = None,
2770
2805
  wait: bool = False,
2771
2806
  add_to_known: bool = True,
2772
2807
  return_idx: bool = False,
@@ -2812,9 +2847,12 @@ class BaseApp(metaclass=Singleton):
2812
2847
  variables
2813
2848
  String variables to substitute in `template_file_or_str`.
2814
2849
  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.
2850
+ If True, allow multiple jobscripts to execute simultaneously. If
2851
+ 'scheduled'/'direct', only allow simultaneous execution of scheduled/direct
2852
+ jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store
2853
+ type does not support the `jobscript_parallelism` feature. If not set,
2854
+ jobscript parallelism will be used if the store type supports it, for
2855
+ scheduled jobscripts only.
2818
2856
  wait
2819
2857
  If True, this command will block until the workflow execution is complete.
2820
2858
  add_to_known
@@ -2855,6 +2893,7 @@ class BaseApp(metaclass=Singleton):
2855
2893
  variables=variables,
2856
2894
  status=status,
2857
2895
  )
2896
+ assert isinstance(wk, _Workflow)
2858
2897
  submitted_js = wk.submit(
2859
2898
  JS_parallelism=JS_parallelism,
2860
2899
  wait=wait,
@@ -2882,7 +2921,8 @@ class BaseApp(metaclass=Singleton):
2882
2921
  store_kwargs: dict[str, Any] | None = None,
2883
2922
  variables: dict[str, str] | None = None,
2884
2923
  status: bool = True,
2885
- ) -> _Workflow:
2924
+ add_submission: bool = False,
2925
+ ) -> _Workflow | _Submission | None:
2886
2926
  """
2887
2927
  Generate a new {app_name} workflow from a builtin demo workflow template.
2888
2928
 
@@ -2918,11 +2958,15 @@ class BaseApp(metaclass=Singleton):
2918
2958
  String variables to substitute in the demo workflow template file.
2919
2959
  status
2920
2960
  If True, display a live status to track workflow creation progress.
2961
+ add_submission
2962
+ If True, add a submission to the workflow (but do not submit).
2921
2963
 
2922
2964
  Returns
2923
2965
  -------
2924
2966
  Workflow
2925
- The created workflow.
2967
+ The created workflow, if `add_submission` is `False`.
2968
+ Submission
2969
+ The created submission object, if `add_submission` is `True`.
2926
2970
  """
2927
2971
  self.API_logger.info("make_demo_workflow called")
2928
2972
 
@@ -2933,7 +2977,7 @@ class BaseApp(metaclass=Singleton):
2933
2977
  with status_context as status_, self.get_demo_workflow_template_file(
2934
2978
  workflow_name
2935
2979
  ) as template_path:
2936
- return self.Workflow.from_file(
2980
+ wk = self.Workflow.from_file(
2937
2981
  template_path=template_path,
2938
2982
  template_format=template_format,
2939
2983
  path=str(path) if path else None,
@@ -2946,6 +2990,11 @@ class BaseApp(metaclass=Singleton):
2946
2990
  variables=variables,
2947
2991
  status=status_,
2948
2992
  )
2993
+ if add_submission:
2994
+ with wk._store.cached_load():
2995
+ with wk.batch_update():
2996
+ return wk._add_submission(status=status_)
2997
+ return wk
2949
2998
 
2950
2999
  def _make_and_submit_demo_workflow(
2951
3000
  self,
@@ -2959,7 +3008,7 @@ class BaseApp(metaclass=Singleton):
2959
3008
  ts_name_fmt: str | None = None,
2960
3009
  store_kwargs: dict[str, Any] | None = None,
2961
3010
  variables: dict[str, str] | None = None,
2962
- JS_parallelism: bool | None = None,
3011
+ JS_parallelism: bool | Literal["direct", "scheduled"] | None = None,
2963
3012
  wait: bool = False,
2964
3013
  add_to_known: bool = True,
2965
3014
  return_idx: bool = False,
@@ -3002,9 +3051,12 @@ class BaseApp(metaclass=Singleton):
3002
3051
  variables
3003
3052
  String variables to substitute in the demo workflow template file.
3004
3053
  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.
3054
+ If True, allow multiple jobscripts to execute simultaneously. If
3055
+ 'scheduled'/'direct', only allow simultaneous execution of scheduled/direct
3056
+ jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store
3057
+ type does not support the `jobscript_parallelism` feature. If not set,
3058
+ jobscript parallelism will be used if the store type supports it, for
3059
+ scheduled jobscripts only.
3008
3060
  wait
3009
3061
  If True, this command will block until the workflow execution is complete.
3010
3062
  add_to_known
@@ -3042,6 +3094,7 @@ class BaseApp(metaclass=Singleton):
3042
3094
  store_kwargs=store_kwargs,
3043
3095
  variables=variables,
3044
3096
  )
3097
+ assert isinstance(wk, _Workflow)
3045
3098
  submitted_js = wk.submit(
3046
3099
  JS_parallelism=JS_parallelism,
3047
3100
  wait=wait,
@@ -3059,7 +3112,7 @@ class BaseApp(metaclass=Singleton):
3059
3112
  def _submit_workflow(
3060
3113
  self,
3061
3114
  workflow_path: PathLike,
3062
- JS_parallelism: bool | None = None,
3115
+ JS_parallelism: bool | Literal["direct", "scheduled"] | None = None,
3063
3116
  wait: bool = False,
3064
3117
  return_idx: bool = False,
3065
3118
  tasks: list[int] | None = None,
@@ -3072,9 +3125,12 @@ class BaseApp(metaclass=Singleton):
3072
3125
  workflow_path:
3073
3126
  Path to an existing workflow.
3074
3127
  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.
3128
+ If True, allow multiple jobscripts to execute simultaneously. If
3129
+ 'scheduled'/'direct', only allow simultaneous execution of scheduled/direct
3130
+ jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store
3131
+ type does not support the `jobscript_parallelism` feature. If not set,
3132
+ jobscript parallelism will be used if the store type supports it, for
3133
+ scheduled jobscripts only.
3078
3134
  wait:
3079
3135
  Whether to wait for the submission to complete.
3080
3136
  return_idx:
@@ -3193,7 +3249,8 @@ class BaseApp(metaclass=Singleton):
3193
3249
 
3194
3250
  # keys are (workflow path, submission index)
3195
3251
  active_jobscripts: dict[
3196
- tuple[str, int], Mapping[int, Mapping[int, JobscriptElementState]]
3252
+ tuple[str, int],
3253
+ Mapping[int, Mapping[int, Mapping[int, JobscriptElementState]]],
3197
3254
  ] = {}
3198
3255
  loaded_workflows: dict[str, _Workflow] = {} # keys are workflow path
3199
3256
 
@@ -3295,7 +3352,9 @@ class BaseApp(metaclass=Singleton):
3295
3352
  if file_dat_i["is_active"]:
3296
3353
  # check it really is active:
3297
3354
  run_key = (file_dat_i["path"], file_dat_i["sub_idx"])
3298
- act_i_js: Mapping[int, Mapping[int, JobscriptElementState]]
3355
+ act_i_js: Mapping[
3356
+ int, Mapping[int, Mapping[int, JobscriptElementState]]
3357
+ ]
3299
3358
  if run_key in active_jobscripts:
3300
3359
  act_i_js = active_jobscripts[run_key]
3301
3360
  else:
@@ -3521,25 +3580,27 @@ class BaseApp(metaclass=Singleton):
3521
3580
  style_wk_name = "grey42 strike" if deleted else style
3522
3581
  style_it = "italic grey42" if (no_access or not act_js) else "italic"
3523
3582
 
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
- )
3583
+ all_cells: dict[str, str | Text | Padding] = {}
3584
+ if "status" in columns:
3585
+ if act_js:
3586
+ act_js_states = set(
3587
+ state_i
3588
+ for js_dat in act_js.values()
3589
+ for block_dat in js_dat.values()
3590
+ for state_i in block_dat.values()
3591
+ )
3592
+ all_cells["status"] = "/".join(
3593
+ js_state.rich_repr
3594
+ for js_state in sorted(act_js_states, key=lambda x: x.value)
3595
+ )
3596
+ else:
3597
+ if deleted:
3598
+ txt = "deleted"
3599
+ elif unloadable:
3600
+ txt = "unloadable"
3535
3601
  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)
3602
+ txt = "inactive"
3603
+ all_cells["status"] = Text(txt, style=style_it)
3543
3604
 
3544
3605
  if "id" in columns:
3545
3606
  all_cells["id"] = Text(str(dat_i["local_id"]), style=style)
@@ -3591,7 +3652,9 @@ class BaseApp(metaclass=Singleton):
3591
3652
  )
3592
3653
  all_cells["actions_compact"] = " | ".join(
3593
3654
  f"[{k.colour}]{k.symbol}[/{k.colour}]:{v}" # type: ignore
3594
- for k, v in EAR_stat_count.items()
3655
+ for k, v in dict(
3656
+ sorted(EAR_stat_count.items(), key=lambda x: x[0].value)
3657
+ ).items()
3595
3658
  )
3596
3659
  else:
3597
3660
  all_cells["actions_compact"] = ""
@@ -3711,18 +3774,29 @@ class BaseApp(metaclass=Singleton):
3711
3774
  return path.resolve()
3712
3775
 
3713
3776
  def _cancel(
3714
- self, workflow_ref: int | str | Path, ref_is_path: str | None = None
3777
+ self,
3778
+ workflow_ref: int | str | Path,
3779
+ ref_is_path: str | None = None,
3780
+ status: bool = True,
3715
3781
  ) -> None:
3716
3782
  """
3717
3783
  Cancel the execution of a workflow submission.
3718
3784
 
3719
3785
  Parameters
3720
3786
  ----------
3721
- ref_is_path
3722
- One of "id", "path" or "assume-id" (the default)
3787
+ workflow_ref: int | str | Path
3788
+ Which workflow to cancel, by ID or path.
3789
+ ref_is_path: str
3790
+ One of "``id``", "``path``" or "``assume-id``" (the default)
3791
+ status: bool
3792
+ Whether to show a live status during cancel.
3723
3793
  """
3724
3794
  path = self._resolve_workflow_reference(str(workflow_ref), ref_is_path)
3725
- self.Workflow(path).cancel()
3795
+ self.Workflow(path).cancel(status=status)
3796
+
3797
+ @staticmethod
3798
+ def redirect_std_to_file(*args, **kwargs):
3799
+ return redirect_std_to_file_hpcflow(*args, **kwargs)
3726
3800
 
3727
3801
  def configure_env(
3728
3802
  self,