hpcflow-new2 0.2.0a162__py3-none-any.whl → 0.2.0a164__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/_version.py +1 -1
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
- hpcflow/sdk/app.py +29 -42
- hpcflow/sdk/cli.py +1 -1
- hpcflow/sdk/core/actions.py +87 -21
- hpcflow/sdk/core/command_files.py +6 -4
- hpcflow/sdk/core/commands.py +21 -2
- hpcflow/sdk/core/element.py +39 -8
- hpcflow/sdk/core/errors.py +16 -0
- hpcflow/sdk/core/object_list.py +26 -14
- hpcflow/sdk/core/parameters.py +21 -3
- hpcflow/sdk/core/task.py +111 -4
- hpcflow/sdk/core/task_schema.py +17 -2
- hpcflow/sdk/core/test_utils.py +5 -2
- hpcflow/sdk/core/workflow.py +93 -5
- hpcflow/sdk/data/workflow_spec_schema.yaml +14 -58
- hpcflow/sdk/demo/cli.py +1 -1
- hpcflow/sdk/persistence/base.py +6 -0
- hpcflow/sdk/persistence/zarr.py +2 -0
- hpcflow/sdk/submission/submission.py +21 -10
- hpcflow/tests/scripts/test_main_scripts.py +60 -0
- hpcflow/tests/unit/test_action.py +186 -0
- hpcflow/tests/unit/test_element.py +27 -25
- hpcflow/tests/unit/test_element_set.py +32 -0
- hpcflow/tests/unit/test_parameter.py +11 -9
- hpcflow/tests/unit/test_persistence.py +4 -1
- hpcflow/tests/unit/test_resources.py +7 -9
- hpcflow/tests/unit/test_schema_input.py +8 -8
- hpcflow/tests/unit/test_task.py +26 -27
- hpcflow/tests/unit/test_task_schema.py +39 -8
- hpcflow/tests/unit/test_value_sequence.py +5 -0
- hpcflow/tests/unit/test_workflow.py +4 -9
- hpcflow/tests/unit/test_workflow_template.py +122 -1
- {hpcflow_new2-0.2.0a162.dist-info → hpcflow_new2-0.2.0a164.dist-info}/METADATA +1 -1
- {hpcflow_new2-0.2.0a162.dist-info → hpcflow_new2-0.2.0a164.dist-info}/RECORD +37 -36
- {hpcflow_new2-0.2.0a162.dist-info → hpcflow_new2-0.2.0a164.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a162.dist-info → hpcflow_new2-0.2.0a164.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/object_list.py
CHANGED
@@ -4,6 +4,10 @@ from types import SimpleNamespace
|
|
4
4
|
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
5
5
|
|
6
6
|
|
7
|
+
class ObjectListMultipleMatchError(ValueError):
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
7
11
|
class ObjectList(JSONLike):
|
8
12
|
"""A list-like class that provides item access via a `get` method according to
|
9
13
|
attributes or dict-keys.
|
@@ -116,14 +120,22 @@ class ObjectList(JSONLike):
|
|
116
120
|
if not result:
|
117
121
|
available = []
|
118
122
|
for obj in self._objects:
|
119
|
-
|
123
|
+
attr_vals = {}
|
124
|
+
for k in kwargs:
|
125
|
+
try:
|
126
|
+
attr_vals[k] = self._get_obj_attr(obj, k)
|
127
|
+
except (AttributeError, KeyError):
|
128
|
+
continue
|
129
|
+
available.append(attr_vals)
|
120
130
|
raise ValueError(
|
121
131
|
f"No {self._descriptor} objects with attributes: {kwargs}. Available "
|
122
132
|
f"objects have attributes: {tuple(available)!r}."
|
123
133
|
)
|
124
134
|
|
125
135
|
elif len(result) > 1:
|
126
|
-
raise
|
136
|
+
raise ObjectListMultipleMatchError(
|
137
|
+
f"Multiple objects with attributes: {kwargs}."
|
138
|
+
)
|
127
139
|
|
128
140
|
return result[0]
|
129
141
|
|
@@ -571,23 +583,23 @@ class ResourceList(ObjectList):
|
|
571
583
|
def get_scopes(self):
|
572
584
|
return tuple(i.scope for i in self._objects)
|
573
585
|
|
574
|
-
def
|
575
|
-
"""Merge lower-precedence
|
576
|
-
for scope_i in
|
586
|
+
def merge_other(self, other):
|
587
|
+
"""Merge lower-precedence other resource list into this resource list."""
|
588
|
+
for scope_i in other.get_scopes():
|
577
589
|
try:
|
578
|
-
|
590
|
+
self_scoped = self.get(scope=scope_i)
|
579
591
|
except ValueError:
|
580
|
-
|
592
|
+
in_self = False
|
581
593
|
else:
|
582
|
-
|
594
|
+
in_self = True
|
583
595
|
|
584
|
-
|
585
|
-
if
|
586
|
-
for k, v in
|
587
|
-
if getattr(
|
588
|
-
setattr(
|
596
|
+
other_scoped = other.get(scope=scope_i)
|
597
|
+
if in_self:
|
598
|
+
for k, v in other_scoped._get_members().items():
|
599
|
+
if getattr(self_scoped, k) is None:
|
600
|
+
setattr(self_scoped, f"_{k}", copy.deepcopy(v))
|
589
601
|
else:
|
590
|
-
self.add_object(copy.deepcopy(
|
602
|
+
self.add_object(copy.deepcopy(other_scoped))
|
591
603
|
|
592
604
|
|
593
605
|
def index(obj_lst, obj):
|
hpcflow/sdk/core/parameters.py
CHANGED
@@ -678,9 +678,11 @@ class ValueSequence(JSONLike):
|
|
678
678
|
)
|
679
679
|
path_l = path.lower()
|
680
680
|
path_split = path_l.split(".")
|
681
|
-
|
681
|
+
allowed_path_start = ("inputs", "resources", "environments", "env_preset")
|
682
|
+
if not path_split[0] in allowed_path_start:
|
682
683
|
raise MalformedParameterPathError(
|
683
|
-
f
|
684
|
+
f"`path` must start with one of: "
|
685
|
+
f'{", ".join(f"{i!r}" for i in allowed_path_start)}, but given path '
|
684
686
|
f"is: {path!r}."
|
685
687
|
)
|
686
688
|
|
@@ -703,7 +705,7 @@ class ValueSequence(JSONLike):
|
|
703
705
|
elif label_from_path:
|
704
706
|
label = label_from_path
|
705
707
|
|
706
|
-
|
708
|
+
elif path_split[0] == "resources":
|
707
709
|
if label_from_path or label_arg:
|
708
710
|
raise ValueError(
|
709
711
|
f"{self.__class__.__name__} `label` argument ({label_arg!r}) and/or "
|
@@ -728,6 +730,14 @@ class ValueSequence(JSONLike):
|
|
728
730
|
f"resource item names are: {allowed_keys_str}."
|
729
731
|
)
|
730
732
|
|
733
|
+
elif path_split[0] == "environments":
|
734
|
+
# rewrite as a resources path:
|
735
|
+
path = f"resources.any.{path}"
|
736
|
+
|
737
|
+
# note: `env_preset` paths also need to be transformed into `resources` paths, but
|
738
|
+
# we cannot do that until the sequence is part of a task, since the available
|
739
|
+
# environment presets are defined in the task schema.
|
740
|
+
|
731
741
|
return path, label
|
732
742
|
|
733
743
|
def to_dict(self):
|
@@ -1330,6 +1340,7 @@ class ResourceSpec(JSONLike):
|
|
1330
1340
|
"scheduler_args",
|
1331
1341
|
"shell_args",
|
1332
1342
|
"os_name",
|
1343
|
+
"environments",
|
1333
1344
|
"SGE_parallel_env",
|
1334
1345
|
"SLURM_partition",
|
1335
1346
|
"SLURM_num_tasks",
|
@@ -1364,6 +1375,7 @@ class ResourceSpec(JSONLike):
|
|
1364
1375
|
scheduler_args: Optional[Dict] = None,
|
1365
1376
|
shell_args: Optional[Dict] = None,
|
1366
1377
|
os_name: Optional[str] = None,
|
1378
|
+
environments: Optional[Dict] = None,
|
1367
1379
|
SGE_parallel_env: Optional[str] = None,
|
1368
1380
|
SLURM_partition: Optional[str] = None,
|
1369
1381
|
SLURM_num_tasks: Optional[str] = None,
|
@@ -1392,6 +1404,7 @@ class ResourceSpec(JSONLike):
|
|
1392
1404
|
self._scheduler = self._process_string(scheduler)
|
1393
1405
|
self._shell = self._process_string(shell)
|
1394
1406
|
self._os_name = self._process_string(os_name)
|
1407
|
+
self._environments = environments
|
1395
1408
|
self._use_job_array = use_job_array
|
1396
1409
|
self._max_array_items = max_array_items
|
1397
1410
|
self._time_limit = time_limit
|
@@ -1526,6 +1539,7 @@ class ResourceSpec(JSONLike):
|
|
1526
1539
|
self._scheduler_args = None
|
1527
1540
|
self._shell_args = None
|
1528
1541
|
self._os_name = None
|
1542
|
+
self._environments = None
|
1529
1543
|
|
1530
1544
|
return (self.normalised_path, [data_ref], is_new)
|
1531
1545
|
|
@@ -1625,6 +1639,10 @@ class ResourceSpec(JSONLike):
|
|
1625
1639
|
def os_name(self):
|
1626
1640
|
return self._get_value("os_name")
|
1627
1641
|
|
1642
|
+
@property
|
1643
|
+
def environments(self):
|
1644
|
+
return self._get_value("environments")
|
1645
|
+
|
1628
1646
|
@property
|
1629
1647
|
def SGE_parallel_env(self):
|
1630
1648
|
return self._get_value("SGE_parallel_env")
|
hpcflow/sdk/core/task.py
CHANGED
@@ -25,6 +25,7 @@ from .errors import (
|
|
25
25
|
TaskTemplateMultipleSchemaObjectives,
|
26
26
|
TaskTemplateUnexpectedInput,
|
27
27
|
TaskTemplateUnexpectedSequenceInput,
|
28
|
+
UnknownEnvironmentPresetError,
|
28
29
|
UnrequiredInputSources,
|
29
30
|
UnsetParameterDataError,
|
30
31
|
)
|
@@ -126,8 +127,11 @@ class ElementSet(JSONLike):
|
|
126
127
|
groups: Optional[List[app.ElementGroup]] = None,
|
127
128
|
input_sources: Optional[Dict[str, app.InputSource]] = None,
|
128
129
|
nesting_order: Optional[List] = None,
|
130
|
+
env_preset: Optional[str] = None,
|
131
|
+
environments: Optional[Dict[str, Dict[str, Any]]] = None,
|
129
132
|
sourceable_elem_iters: Optional[List[int]] = None,
|
130
133
|
allow_non_coincident_task_sources: Optional[bool] = False,
|
134
|
+
merge_envs: Optional[bool] = True,
|
131
135
|
):
|
132
136
|
"""
|
133
137
|
Parameters
|
@@ -140,7 +144,10 @@ class ElementSet(JSONLike):
|
|
140
144
|
If True, if more than one parameter is sourced from the same task, then allow
|
141
145
|
these sources to come from distinct element sub-sets. If False (default),
|
142
146
|
only the intersection of element sub-sets for all parameters are included.
|
143
|
-
|
147
|
+
merge_envs
|
148
|
+
If True, merge `environments` into `resources` using the "any" scope. If
|
149
|
+
False, `environments` are ignored. This is required on first initialisation,
|
150
|
+
but not on subsequent re-initialisation from a persistent workflow.
|
144
151
|
"""
|
145
152
|
|
146
153
|
self.inputs = inputs or []
|
@@ -151,8 +158,11 @@ class ElementSet(JSONLike):
|
|
151
158
|
self.sequences = sequences or []
|
152
159
|
self.input_sources = input_sources or {}
|
153
160
|
self.nesting_order = nesting_order or {}
|
161
|
+
self.env_preset = env_preset
|
162
|
+
self.environments = environments
|
154
163
|
self.sourceable_elem_iters = sourceable_elem_iters
|
155
164
|
self.allow_non_coincident_task_sources = allow_non_coincident_task_sources
|
165
|
+
self.merge_envs = merge_envs
|
156
166
|
|
157
167
|
self._validate()
|
158
168
|
self._set_parent_refs()
|
@@ -161,6 +171,18 @@ class ElementSet(JSONLike):
|
|
161
171
|
self._defined_input_types = None # assigned on _task_template assignment
|
162
172
|
self._element_local_idx_range = None # assigned by WorkflowTask._add_element_set
|
163
173
|
|
174
|
+
# merge `environments` into element set resources (this mutates `resources`, and
|
175
|
+
# should only happen on creation of the element set, not re-initialisation from a
|
176
|
+
# persistent workflow):
|
177
|
+
if self.environments and self.merge_envs:
|
178
|
+
envs_res = self.app.ResourceList(
|
179
|
+
[self.app.ResourceSpec(scope="any", environments=self.environments)]
|
180
|
+
)
|
181
|
+
self.resources.merge_other(envs_res)
|
182
|
+
self.merge_envs = False
|
183
|
+
|
184
|
+
# note: `env_preset` is merged into resources by the Task init.
|
185
|
+
|
164
186
|
def __deepcopy__(self, memo):
|
165
187
|
dct = self.to_dict()
|
166
188
|
orig_inp = dct.pop("original_input_sources", None)
|
@@ -278,6 +300,10 @@ class ElementSet(JSONLike):
|
|
278
300
|
f"provided for parameter {src_key!r}."
|
279
301
|
)
|
280
302
|
|
303
|
+
# disallow both `env_preset` and `environments` specifications:
|
304
|
+
if self.env_preset and self.environments:
|
305
|
+
raise ValueError("Specify at most one of `env_preset` and `environments`.")
|
306
|
+
|
281
307
|
def _validate_against_template(self):
|
282
308
|
unexpected_types = (
|
283
309
|
set(self.input_types) - self.task_template.all_schema_input_types
|
@@ -330,6 +356,8 @@ class ElementSet(JSONLike):
|
|
330
356
|
groups=None,
|
331
357
|
input_sources=None,
|
332
358
|
nesting_order=None,
|
359
|
+
env_preset=None,
|
360
|
+
environments=None,
|
333
361
|
element_sets=None,
|
334
362
|
sourceable_elem_iters=None,
|
335
363
|
):
|
@@ -342,6 +370,8 @@ class ElementSet(JSONLike):
|
|
342
370
|
groups,
|
343
371
|
input_sources,
|
344
372
|
nesting_order,
|
373
|
+
env_preset,
|
374
|
+
environments,
|
345
375
|
)
|
346
376
|
args_not_none = [i is not None for i in args]
|
347
377
|
|
@@ -520,9 +550,12 @@ class Task(JSONLike):
|
|
520
550
|
sequences: Optional[List[app.ValueSequence]] = None,
|
521
551
|
input_sources: Optional[Dict[str, app.InputSource]] = None,
|
522
552
|
nesting_order: Optional[List] = None,
|
553
|
+
env_preset: Optional[str] = None,
|
554
|
+
environments: Optional[Dict[str, Dict[str, Any]]] = None,
|
523
555
|
element_sets: Optional[List[app.ElementSet]] = None,
|
524
556
|
output_labels: Optional[List[app.OutputLabel]] = None,
|
525
557
|
sourceable_elem_iters: Optional[List[int]] = None,
|
558
|
+
merge_envs: Optional[bool] = True,
|
526
559
|
):
|
527
560
|
"""
|
528
561
|
Parameters
|
@@ -532,7 +565,11 @@ class Task(JSONLike):
|
|
532
565
|
schema names that uniquely identify a task schema. If strings are provided,
|
533
566
|
the `TaskSchema` object will be fetched from the known task schemas loaded by
|
534
567
|
the app configuration.
|
535
|
-
|
568
|
+
merge_envs
|
569
|
+
If True, merge environment presets (set via the element set `env_preset` key)
|
570
|
+
into `resources` using the "any" scope. If False, these presets are ignored.
|
571
|
+
This is required on first initialisation, but not on subsequent
|
572
|
+
re-initialisation from a persistent workflow.
|
536
573
|
"""
|
537
574
|
|
538
575
|
# TODO: allow init via specifying objective and/or method and/or implementation
|
@@ -576,10 +613,13 @@ class Task(JSONLike):
|
|
576
613
|
groups=groups,
|
577
614
|
input_sources=input_sources,
|
578
615
|
nesting_order=nesting_order,
|
616
|
+
env_preset=env_preset,
|
617
|
+
environments=environments,
|
579
618
|
element_sets=element_sets,
|
580
619
|
sourceable_elem_iters=sourceable_elem_iters,
|
581
620
|
)
|
582
621
|
self._output_labels = output_labels or []
|
622
|
+
self.merge_envs = merge_envs
|
583
623
|
|
584
624
|
# appended to when new element sets are added and reset on dump to disk:
|
585
625
|
self._pending_element_sets = []
|
@@ -591,8 +631,73 @@ class Task(JSONLike):
|
|
591
631
|
self._insert_ID = None
|
592
632
|
self._dir_name = None
|
593
633
|
|
634
|
+
if self.merge_envs:
|
635
|
+
self._merge_envs_into_resources()
|
636
|
+
|
637
|
+
# TODO: consider adding a new element_set; will need to merge new environments?
|
638
|
+
|
594
639
|
self._set_parent_refs({"schema": "schemas"})
|
595
640
|
|
641
|
+
def _merge_envs_into_resources(self):
|
642
|
+
# for each element set, merge `env_preset` into `resources` (this mutates
|
643
|
+
# `resources`, and should only happen on creation of the task, not
|
644
|
+
# re-initialisation from a persistent workflow):
|
645
|
+
self.merge_envs = False
|
646
|
+
|
647
|
+
# TODO: required so we don't raise below; can be removed once we consider multiple
|
648
|
+
# schemas:
|
649
|
+
has_presets = False
|
650
|
+
for es in self.element_sets:
|
651
|
+
if es.env_preset:
|
652
|
+
has_presets = True
|
653
|
+
break
|
654
|
+
for seq in es.sequences:
|
655
|
+
if seq.path == "env_preset":
|
656
|
+
has_presets = True
|
657
|
+
break
|
658
|
+
if has_presets:
|
659
|
+
break
|
660
|
+
|
661
|
+
if not has_presets:
|
662
|
+
return
|
663
|
+
try:
|
664
|
+
env_presets = self.schema.environment_presets
|
665
|
+
except ValueError:
|
666
|
+
# TODO: consider multiple schemas
|
667
|
+
raise NotImplementedError(
|
668
|
+
"Cannot merge environment presets into a task with multiple schemas."
|
669
|
+
)
|
670
|
+
|
671
|
+
for es in self.element_sets:
|
672
|
+
if es.env_preset:
|
673
|
+
# retrieve env specifiers from presets defined in the schema:
|
674
|
+
try:
|
675
|
+
env_specs = env_presets[es.env_preset]
|
676
|
+
except (TypeError, KeyError):
|
677
|
+
raise UnknownEnvironmentPresetError(
|
678
|
+
f"There is no environment preset named {es.env_preset!r} "
|
679
|
+
f"defined in the task schema {self.schema.name}."
|
680
|
+
)
|
681
|
+
envs_res = self.app.ResourceList(
|
682
|
+
[self.app.ResourceSpec(scope="any", environments=env_specs)]
|
683
|
+
)
|
684
|
+
es.resources.merge_other(envs_res)
|
685
|
+
|
686
|
+
for seq in es.sequences:
|
687
|
+
if seq.path == "env_preset":
|
688
|
+
# change to a resources path:
|
689
|
+
seq.path = f"resources.any.environments"
|
690
|
+
_values = []
|
691
|
+
for i in seq.values:
|
692
|
+
try:
|
693
|
+
_values.append(env_presets[i])
|
694
|
+
except (TypeError, KeyError):
|
695
|
+
raise UnknownEnvironmentPresetError(
|
696
|
+
f"There is no environment preset named {i!r} defined "
|
697
|
+
f"in the task schema {self.schema.name}."
|
698
|
+
)
|
699
|
+
seq._values = _values
|
700
|
+
|
596
701
|
def _reset_pending_element_sets(self):
|
597
702
|
self._pending_element_sets = []
|
598
703
|
|
@@ -1839,8 +1944,8 @@ class WorkflowTask:
|
|
1839
1944
|
# run-specific data index to `test_rules` and `generate_data_index`
|
1840
1945
|
# (e.g. if we wanted to increase the memory requirements of a action because
|
1841
1946
|
# it previously failed)
|
1842
|
-
|
1843
|
-
if
|
1947
|
+
act_valid, cmds_idx = action.test_rules(element_iter=element_iter)
|
1948
|
+
if act_valid:
|
1844
1949
|
self.app.logger.info(f"All action rules evaluated to true {log_common}")
|
1845
1950
|
EAR_ID = self.workflow.num_EARs + count
|
1846
1951
|
param_source = {
|
@@ -1864,6 +1969,7 @@ class WorkflowTask:
|
|
1864
1969
|
run_0 = {
|
1865
1970
|
"elem_iter_ID": element_iter.id_,
|
1866
1971
|
"action_idx": act_idx,
|
1972
|
+
"commands_idx": cmds_idx,
|
1867
1973
|
"metadata": {},
|
1868
1974
|
}
|
1869
1975
|
action_runs[(act_idx, EAR_ID)] = run_0
|
@@ -1877,6 +1983,7 @@ class WorkflowTask:
|
|
1877
1983
|
self.workflow._store.add_EAR(
|
1878
1984
|
elem_iter_ID=element_iter.id_,
|
1879
1985
|
action_idx=act_idx,
|
1986
|
+
commands_idx=run["commands_idx"],
|
1880
1987
|
data_idx=all_data_idx[(act_idx, EAR_ID_i)],
|
1881
1988
|
metadata={},
|
1882
1989
|
)
|
hpcflow/sdk/core/task_schema.py
CHANGED
@@ -2,7 +2,7 @@ from contextlib import contextmanager
|
|
2
2
|
import copy
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from importlib import import_module
|
5
|
-
from typing import Dict, List, Optional, Tuple, Union
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
6
6
|
from html import escape
|
7
7
|
|
8
8
|
from rich import print as rich_print
|
@@ -13,6 +13,7 @@ from rich.markup import escape as rich_esc
|
|
13
13
|
from rich.text import Text
|
14
14
|
|
15
15
|
from hpcflow.sdk import app
|
16
|
+
from hpcflow.sdk.core.errors import EnvironmentPresetUnknownEnvironmentError
|
16
17
|
from hpcflow.sdk.core.parameters import Parameter
|
17
18
|
from .json_like import ChildObjectSpec, JSONLike
|
18
19
|
from .parameters import NullDefault, ParameterPropagationMode, SchemaInput
|
@@ -89,6 +90,7 @@ class TaskSchema(JSONLike):
|
|
89
90
|
version: Optional[str] = None,
|
90
91
|
parameter_class_modules: Optional[List[str]] = None,
|
91
92
|
web_doc: Optional[bool] = True,
|
93
|
+
environment_presets: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None,
|
92
94
|
_hash_value: Optional[str] = None,
|
93
95
|
):
|
94
96
|
self.objective = objective
|
@@ -99,6 +101,7 @@ class TaskSchema(JSONLike):
|
|
99
101
|
self.outputs = outputs or []
|
100
102
|
self.parameter_class_modules = parameter_class_modules or []
|
101
103
|
self.web_doc = web_doc
|
104
|
+
self.environment_presets = environment_presets
|
102
105
|
self._hash_value = _hash_value
|
103
106
|
|
104
107
|
self._set_parent_refs()
|
@@ -114,6 +117,18 @@ class TaskSchema(JSONLike):
|
|
114
117
|
|
115
118
|
self._update_parameter_value_classes()
|
116
119
|
|
120
|
+
if self.environment_presets:
|
121
|
+
# validate against env names in actions:
|
122
|
+
env_names = {act.get_environment_name() for act in self.actions}
|
123
|
+
preset_envs = {i for v in self.environment_presets.values() for i in v.keys()}
|
124
|
+
bad_envs = preset_envs - env_names
|
125
|
+
if bad_envs:
|
126
|
+
raise EnvironmentPresetUnknownEnvironmentError(
|
127
|
+
f"Task schema {self.name} has environment presets that refer to one "
|
128
|
+
f"or more environments that are not referenced in any of the task "
|
129
|
+
f"schema's actions: {', '.join(f'{i!r}' for i in bad_envs)}."
|
130
|
+
)
|
131
|
+
|
117
132
|
# if version is not None: # TODO: this seems fragile
|
118
133
|
# self.assign_versions(
|
119
134
|
# version=version,
|
@@ -513,7 +528,7 @@ class TaskSchema(JSONLike):
|
|
513
528
|
f'</tr><tr><td class="action-header-cell">scope:</td>'
|
514
529
|
f"<td><code>{act.get_precise_scope().to_string()}</code></td></tr>"
|
515
530
|
f'<tr><td class="action-header-cell">environment:</td>'
|
516
|
-
f"<td><code>{act.
|
531
|
+
f"<td><code>{act.get_environment_name()}</code></td></tr>"
|
517
532
|
f"{inp_fg_rows}"
|
518
533
|
f"{out_fp_rows}"
|
519
534
|
f"{act_i_script_rows}"
|
hpcflow/sdk/core/test_utils.py
CHANGED
@@ -61,8 +61,11 @@ def make_parameters(num):
|
|
61
61
|
return [hf.Parameter(f"p{i + 1}") for i in range(num)]
|
62
62
|
|
63
63
|
|
64
|
-
def make_actions(
|
65
|
-
|
64
|
+
def make_actions(
|
65
|
+
ins_outs: List[Tuple[Union[Tuple, str], str]],
|
66
|
+
env="env1",
|
67
|
+
) -> List[hf.Action]:
|
68
|
+
act_env = hf.ActionEnvironment(environment=env)
|
66
69
|
actions = []
|
67
70
|
for ins_outs_i in ins_outs:
|
68
71
|
if len(ins_outs_i) == 2:
|
hpcflow/sdk/core/workflow.py
CHANGED
@@ -105,6 +105,7 @@ class WorkflowTemplate(JSONLike):
|
|
105
105
|
"""
|
106
106
|
|
107
107
|
_app_attr = "app"
|
108
|
+
_validation_schema = "workflow_spec_schema.yaml"
|
108
109
|
|
109
110
|
_child_objects = (
|
110
111
|
ChildObjectSpec(
|
@@ -132,9 +133,12 @@ class WorkflowTemplate(JSONLike):
|
|
132
133
|
loops: Optional[List[app.Loop]] = field(default_factory=lambda: [])
|
133
134
|
workflow: Optional[app.Workflow] = None
|
134
135
|
resources: Optional[Dict[str, Dict]] = None
|
136
|
+
environments: Optional[Dict[str, Dict[str, Any]]] = None
|
137
|
+
env_presets: Optional[Union[str, List[str]]] = None
|
135
138
|
source_file: Optional[str] = field(default=None, compare=False)
|
136
139
|
store_kwargs: Optional[Dict] = field(default_factory=lambda: {})
|
137
140
|
merge_resources: Optional[bool] = True
|
141
|
+
merge_envs: Optional[bool] = True
|
138
142
|
|
139
143
|
def __post_init__(self):
|
140
144
|
self.resources = self.app.ResourceList.normalise(self.resources)
|
@@ -146,12 +150,95 @@ class WorkflowTemplate(JSONLike):
|
|
146
150
|
if self.merge_resources:
|
147
151
|
for task in self.tasks:
|
148
152
|
for element_set in task.element_sets:
|
149
|
-
element_set.resources.
|
153
|
+
element_set.resources.merge_other(self.resources)
|
150
154
|
self.merge_resources = False
|
151
155
|
|
156
|
+
if self.merge_envs:
|
157
|
+
self._merge_envs_into_task_resources()
|
158
|
+
|
152
159
|
if self.doc and not isinstance(self.doc, list):
|
153
160
|
self.doc = [self.doc]
|
154
161
|
|
162
|
+
def _merge_envs_into_task_resources(self):
|
163
|
+
|
164
|
+
self.merge_envs = False
|
165
|
+
|
166
|
+
# disallow both `env_presets` and `environments` specifications:
|
167
|
+
if self.env_presets and self.environments:
|
168
|
+
raise ValueError(
|
169
|
+
"Workflow template: specify at most one of `env_presets` and "
|
170
|
+
"`environments`."
|
171
|
+
)
|
172
|
+
|
173
|
+
if not isinstance(self.env_presets, list):
|
174
|
+
self.env_presets = [self.env_presets] if self.env_presets else []
|
175
|
+
|
176
|
+
for task in self.tasks:
|
177
|
+
|
178
|
+
# get applicable environments and environment preset names:
|
179
|
+
try:
|
180
|
+
schema = task.schema
|
181
|
+
except ValueError:
|
182
|
+
# TODO: consider multiple schemas
|
183
|
+
raise NotImplementedError(
|
184
|
+
"Cannot merge environment presets into a task without multiple "
|
185
|
+
"schemas."
|
186
|
+
)
|
187
|
+
schema_presets = schema.environment_presets
|
188
|
+
app_envs = {act.get_environment_name() for act in schema.actions}
|
189
|
+
for es in task.element_sets:
|
190
|
+
app_env_specs_i = None
|
191
|
+
if not es.environments and not es.env_preset:
|
192
|
+
# no task level envs/presets specified, so merge template-level:
|
193
|
+
if self.environments:
|
194
|
+
app_env_specs_i = {
|
195
|
+
k: v for k, v in self.environments.items() if k in app_envs
|
196
|
+
}
|
197
|
+
if app_env_specs_i:
|
198
|
+
self.app.logger.info(
|
199
|
+
f"(task {task.name!r}, element set {es.index}): using "
|
200
|
+
f"template-level requested `environment` specifiers: "
|
201
|
+
f"{app_env_specs_i!r}."
|
202
|
+
)
|
203
|
+
es.environments = app_env_specs_i
|
204
|
+
|
205
|
+
elif self.env_presets:
|
206
|
+
# take only the first applicable preset:
|
207
|
+
app_presets_i = [
|
208
|
+
k for k in self.env_presets if k in schema_presets
|
209
|
+
]
|
210
|
+
if app_presets_i:
|
211
|
+
app_env_specs_i = schema_presets[app_presets_i[0]]
|
212
|
+
self.app.logger.info(
|
213
|
+
f"(task {task.name!r}, element set {es.index}): using "
|
214
|
+
f"template-level requested {app_presets_i[0]!r} "
|
215
|
+
f"`env_preset`: {app_env_specs_i!r}."
|
216
|
+
)
|
217
|
+
es.env_preset = app_presets_i[0]
|
218
|
+
|
219
|
+
else:
|
220
|
+
# no env/preset applicable here (and no env/preset at task level),
|
221
|
+
# so apply a default preset if available:
|
222
|
+
app_env_specs_i = (schema_presets or {}).get("", None)
|
223
|
+
if app_env_specs_i:
|
224
|
+
self.app.logger.info(
|
225
|
+
f"(task {task.name!r}, element set {es.index}): setting "
|
226
|
+
f"to default (empty-string named) `env_preset`: "
|
227
|
+
f"{app_env_specs_i}."
|
228
|
+
)
|
229
|
+
es.env_preset = ""
|
230
|
+
|
231
|
+
if app_env_specs_i:
|
232
|
+
es.resources.merge_other(
|
233
|
+
self.app.ResourceList(
|
234
|
+
[
|
235
|
+
self.app.ResourceSpec(
|
236
|
+
scope="any", environments=app_env_specs_i
|
237
|
+
)
|
238
|
+
]
|
239
|
+
)
|
240
|
+
)
|
241
|
+
|
155
242
|
@classmethod
|
156
243
|
@TimeIt.decorator
|
157
244
|
def _from_data(cls, data: Dict) -> app.WorkflowTemplate:
|
@@ -172,28 +259,29 @@ class WorkflowTemplate(JSONLike):
|
|
172
259
|
}
|
173
260
|
|
174
261
|
# extract out any template components:
|
175
|
-
|
262
|
+
tcs = data.pop("template_components", {})
|
263
|
+
params_dat = tcs.pop("parameters", [])
|
176
264
|
if params_dat:
|
177
265
|
parameters = cls.app.ParametersList.from_json_like(
|
178
266
|
params_dat, shared_data=cls.app.template_components
|
179
267
|
)
|
180
268
|
cls.app.parameters.add_objects(parameters, skip_duplicates=True)
|
181
269
|
|
182
|
-
cmd_files_dat =
|
270
|
+
cmd_files_dat = tcs.pop("command_files", [])
|
183
271
|
if cmd_files_dat:
|
184
272
|
cmd_files = cls.app.CommandFilesList.from_json_like(
|
185
273
|
cmd_files_dat, shared_data=cls.app.template_components
|
186
274
|
)
|
187
275
|
cls.app.command_files.add_objects(cmd_files, skip_duplicates=True)
|
188
276
|
|
189
|
-
envs_dat =
|
277
|
+
envs_dat = tcs.pop("environments", [])
|
190
278
|
if envs_dat:
|
191
279
|
envs = cls.app.EnvironmentsList.from_json_like(
|
192
280
|
envs_dat, shared_data=cls.app.template_components
|
193
281
|
)
|
194
282
|
cls.app.envs.add_objects(envs, skip_duplicates=True)
|
195
283
|
|
196
|
-
ts_dat =
|
284
|
+
ts_dat = tcs.pop("task_schemas", [])
|
197
285
|
if ts_dat:
|
198
286
|
task_schemas = cls.app.TaskSchemasList.from_json_like(
|
199
287
|
ts_dat, shared_data=cls.app.template_components
|
@@ -1,64 +1,20 @@
|
|
1
1
|
rules:
|
2
2
|
- path: []
|
3
|
-
condition:
|
4
|
-
value.allowed_keys: [tasks]
|
5
|
-
|
6
|
-
- path: [tasks]
|
7
|
-
condition: { value.type.equal_to: list }
|
8
|
-
|
9
|
-
- path: [tasks, { type: list_value }]
|
10
3
|
condition:
|
11
4
|
value.allowed_keys:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
]
|
5
|
+
- doc
|
6
|
+
- name
|
7
|
+
- source_file
|
8
|
+
- resources
|
9
|
+
- environments
|
10
|
+
- env_presets
|
11
|
+
- template_components
|
12
|
+
- tasks
|
13
|
+
- loops
|
14
|
+
- store_kwargs
|
15
|
+
- merge_resources
|
16
|
+
- merge_envs
|
17
|
+
- workflow
|
26
18
|
|
27
|
-
- path: [tasks
|
28
|
-
condition: { value.type.equal_to: dict }
|
29
|
-
|
30
|
-
- path: [tasks, { type: list_value }, perturbations]
|
31
|
-
condition: { value.type.equal_to: dict }
|
32
|
-
|
33
|
-
- path: [tasks, { type: list_value }, nesting_order]
|
34
|
-
condition: { value.type.equal_to: dict }
|
35
|
-
|
36
|
-
- path: [tasks, { type: list_value }, sequences]
|
37
|
-
condition: { value.type.equal_to: list }
|
38
|
-
|
39
|
-
- path: [tasks, { type: list_value }, inputs]
|
40
|
-
condition: { value.type.in: [list, dict] }
|
41
|
-
|
42
|
-
- path: [tasks, { type: list_value }, inputs, { type: list_value }]
|
43
|
-
condition:
|
44
|
-
and:
|
45
|
-
- value.required_keys: [parameter, value]
|
46
|
-
- value.allowed_keys: [parameter, value, path]
|
47
|
-
|
48
|
-
- path: [tasks, { type: list_value }, inputs, { type: list_value }, parameter]
|
49
|
-
condition: { value.type.equal_to: str }
|
50
|
-
|
51
|
-
- path: [tasks, { type: list_value }, inputs, { type: list_value }, path]
|
19
|
+
- path: [tasks]
|
52
20
|
condition: { value.type.equal_to: list }
|
53
|
-
|
54
|
-
- path: [tasks, { type: list_value }, sequences, { type: list_value }]
|
55
|
-
condition:
|
56
|
-
and:
|
57
|
-
- value.required_keys: [path, nesting_order]
|
58
|
-
- value.keys_contain_one_of:
|
59
|
-
[
|
60
|
-
values,
|
61
|
-
values.from_linear_space,
|
62
|
-
values.from_grometric_space,
|
63
|
-
values_from_log_space,
|
64
|
-
]
|