hpcflow 0.1.15__py3-none-any.whl → 0.2.0a271__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hpcflow/__init__.py +2 -11
- hpcflow/__pyinstaller/__init__.py +5 -0
- hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
- hpcflow/_version.py +1 -1
- hpcflow/app.py +43 -0
- hpcflow/cli.py +2 -461
- hpcflow/data/demo_data_manifest/__init__.py +3 -0
- hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
- hpcflow/data/jinja_templates/test/test_template.txt +8 -0
- hpcflow/data/programs/hello_world/README.md +1 -0
- hpcflow/data/programs/hello_world/hello_world.c +87 -0
- hpcflow/data/programs/hello_world/linux/hello_world +0 -0
- hpcflow/data/programs/hello_world/macos/hello_world +0 -0
- hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
- hpcflow/data/scripts/__init__.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
- hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/generate_t1_file_01.py +7 -0
- hpcflow/data/scripts/import_future_script.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
- hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
- hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/parse_t1_file_01.py +4 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/__init__.py +1 -0
- hpcflow/data/template_components/command_files.yaml +26 -0
- hpcflow/data/template_components/environments.yaml +13 -0
- hpcflow/data/template_components/parameters.yaml +14 -0
- hpcflow/data/template_components/task_schemas.yaml +139 -0
- hpcflow/data/workflows/workflow_1.yaml +5 -0
- hpcflow/examples.ipynb +1037 -0
- hpcflow/sdk/__init__.py +149 -0
- hpcflow/sdk/app.py +4266 -0
- hpcflow/sdk/cli.py +1479 -0
- hpcflow/sdk/cli_common.py +385 -0
- hpcflow/sdk/config/__init__.py +5 -0
- hpcflow/sdk/config/callbacks.py +246 -0
- hpcflow/sdk/config/cli.py +388 -0
- hpcflow/sdk/config/config.py +1410 -0
- hpcflow/sdk/config/config_file.py +501 -0
- hpcflow/sdk/config/errors.py +272 -0
- hpcflow/sdk/config/types.py +150 -0
- hpcflow/sdk/core/__init__.py +38 -0
- hpcflow/sdk/core/actions.py +3857 -0
- hpcflow/sdk/core/app_aware.py +25 -0
- hpcflow/sdk/core/cache.py +224 -0
- hpcflow/sdk/core/command_files.py +814 -0
- hpcflow/sdk/core/commands.py +424 -0
- hpcflow/sdk/core/element.py +2071 -0
- hpcflow/sdk/core/enums.py +221 -0
- hpcflow/sdk/core/environment.py +256 -0
- hpcflow/sdk/core/errors.py +1043 -0
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +809 -0
- hpcflow/sdk/core/loop.py +1320 -0
- hpcflow/sdk/core/loop_cache.py +282 -0
- hpcflow/sdk/core/object_list.py +933 -0
- hpcflow/sdk/core/parameters.py +3371 -0
- hpcflow/sdk/core/rule.py +196 -0
- hpcflow/sdk/core/run_dir_files.py +57 -0
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +3792 -0
- hpcflow/sdk/core/task_schema.py +993 -0
- hpcflow/sdk/core/test_utils.py +538 -0
- hpcflow/sdk/core/types.py +447 -0
- hpcflow/sdk/core/utils.py +1207 -0
- hpcflow/sdk/core/validation.py +87 -0
- hpcflow/sdk/core/values.py +477 -0
- hpcflow/sdk/core/workflow.py +4820 -0
- hpcflow/sdk/core/zarr_io.py +206 -0
- hpcflow/sdk/data/__init__.py +13 -0
- hpcflow/sdk/data/config_file_schema.yaml +34 -0
- hpcflow/sdk/data/config_schema.yaml +260 -0
- hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
- hpcflow/sdk/data/files_spec_schema.yaml +5 -0
- hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
- hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
- hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
- hpcflow/sdk/demo/__init__.py +3 -0
- hpcflow/sdk/demo/cli.py +242 -0
- hpcflow/sdk/helper/__init__.py +3 -0
- hpcflow/sdk/helper/cli.py +137 -0
- hpcflow/sdk/helper/helper.py +300 -0
- hpcflow/sdk/helper/watcher.py +192 -0
- hpcflow/sdk/log.py +288 -0
- hpcflow/sdk/persistence/__init__.py +18 -0
- hpcflow/sdk/persistence/base.py +2817 -0
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +39 -0
- hpcflow/sdk/persistence/json.py +954 -0
- hpcflow/sdk/persistence/pending.py +948 -0
- hpcflow/sdk/persistence/store_resource.py +203 -0
- hpcflow/sdk/persistence/types.py +309 -0
- hpcflow/sdk/persistence/utils.py +73 -0
- hpcflow/sdk/persistence/zarr.py +2388 -0
- hpcflow/sdk/runtime.py +320 -0
- hpcflow/sdk/submission/__init__.py +3 -0
- hpcflow/sdk/submission/enums.py +70 -0
- hpcflow/sdk/submission/jobscript.py +2379 -0
- hpcflow/sdk/submission/schedulers/__init__.py +281 -0
- hpcflow/sdk/submission/schedulers/direct.py +233 -0
- hpcflow/sdk/submission/schedulers/sge.py +376 -0
- hpcflow/sdk/submission/schedulers/slurm.py +598 -0
- hpcflow/sdk/submission/schedulers/utils.py +25 -0
- hpcflow/sdk/submission/shells/__init__.py +52 -0
- hpcflow/sdk/submission/shells/base.py +229 -0
- hpcflow/sdk/submission/shells/bash.py +504 -0
- hpcflow/sdk/submission/shells/os_version.py +115 -0
- hpcflow/sdk/submission/shells/powershell.py +352 -0
- hpcflow/sdk/submission/submission.py +1402 -0
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +194 -0
- hpcflow/sdk/utils/arrays.py +69 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +31 -0
- hpcflow/sdk/utils/strings.py +69 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +123 -0
- hpcflow/tests/data/__init__.py +0 -0
- hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
- hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_1.json +10 -0
- hpcflow/tests/data/workflow_1.yaml +5 -0
- hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
- hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
- hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
- hpcflow/tests/programs/test_programs.py +180 -0
- hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +1361 -0
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
- hpcflow/tests/unit/test_action.py +1066 -0
- hpcflow/tests/unit/test_action_rule.py +24 -0
- hpcflow/tests/unit/test_app.py +132 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +172 -0
- hpcflow/tests/unit/test_command.py +377 -0
- hpcflow/tests/unit/test_config.py +195 -0
- hpcflow/tests/unit/test_config_file.py +162 -0
- hpcflow/tests/unit/test_element.py +666 -0
- hpcflow/tests/unit/test_element_iteration.py +88 -0
- hpcflow/tests/unit/test_element_set.py +158 -0
- hpcflow/tests/unit/test_group.py +115 -0
- hpcflow/tests/unit/test_input_source.py +1479 -0
- hpcflow/tests/unit/test_input_value.py +398 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +1247 -0
- hpcflow/tests/unit/test_loop.py +2674 -0
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
- hpcflow/tests/unit/test_object_list.py +116 -0
- hpcflow/tests/unit/test_parameter.py +243 -0
- hpcflow/tests/unit/test_persistence.py +664 -0
- hpcflow/tests/unit/test_resources.py +243 -0
- hpcflow/tests/unit/test_run.py +286 -0
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +9 -0
- hpcflow/tests/unit/test_schema_input.py +372 -0
- hpcflow/tests/unit/test_shell.py +129 -0
- hpcflow/tests/unit/test_slurm.py +39 -0
- hpcflow/tests/unit/test_submission.py +502 -0
- hpcflow/tests/unit/test_task.py +2560 -0
- hpcflow/tests/unit/test_task_schema.py +182 -0
- hpcflow/tests/unit/test_utils.py +616 -0
- hpcflow/tests/unit/test_value_sequence.py +549 -0
- hpcflow/tests/unit/test_values.py +91 -0
- hpcflow/tests/unit/test_workflow.py +827 -0
- hpcflow/tests/unit/test_workflow_template.py +186 -0
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/unit/utils/test_strings.py +97 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +355 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +564 -0
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6794 -0
- hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
- hpcflow-0.2.0a271.dist-info/METADATA +65 -0
- hpcflow-0.2.0a271.dist-info/RECORD +237 -0
- {hpcflow-0.1.15.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
- hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
- hpcflow/api.py +0 -490
- hpcflow/archive/archive.py +0 -307
- hpcflow/archive/cloud/cloud.py +0 -45
- hpcflow/archive/cloud/errors.py +0 -9
- hpcflow/archive/cloud/providers/dropbox.py +0 -427
- hpcflow/archive/errors.py +0 -5
- hpcflow/base_db.py +0 -4
- hpcflow/config.py +0 -233
- hpcflow/copytree.py +0 -66
- hpcflow/data/examples/_config.yml +0 -14
- hpcflow/data/examples/damask/demo/1.run.yml +0 -4
- hpcflow/data/examples/damask/demo/2.process.yml +0 -29
- hpcflow/data/examples/damask/demo/geom.geom +0 -2052
- hpcflow/data/examples/damask/demo/load.load +0 -1
- hpcflow/data/examples/damask/demo/material.config +0 -185
- hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
- hpcflow/data/examples/damask/inputs/load.load +0 -1
- hpcflow/data/examples/damask/inputs/material.config +0 -185
- hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
- hpcflow/data/examples/damask/profiles/damask.yml +0 -4
- hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
- hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
- hpcflow/data/examples/damask/profiles/default.yml +0 -6
- hpcflow/data/examples/thinking.yml +0 -177
- hpcflow/errors.py +0 -2
- hpcflow/init_db.py +0 -37
- hpcflow/models.py +0 -2595
- hpcflow/nesting.py +0 -9
- hpcflow/profiles.py +0 -455
- hpcflow/project.py +0 -81
- hpcflow/scheduler.py +0 -322
- hpcflow/utils.py +0 -103
- hpcflow/validation.py +0 -166
- hpcflow/variables.py +0 -543
- hpcflow-0.1.15.dist-info/METADATA +0 -168
- hpcflow-0.1.15.dist-info/RECORD +0 -45
- hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
- hpcflow-0.1.15.dist-info/top_level.txt +0 -1
- /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
- /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
- /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
"""
|
|
2
|
+
General model of a searchable serializable list.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
import copy
|
|
9
|
+
import sys
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from typing import Generic, TypeVar, cast, overload, TYPE_CHECKING
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
14
|
+
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterable, Iterator
|
|
18
|
+
from typing import Any, ClassVar, Literal
|
|
19
|
+
from typing_extensions import Self, TypeIs
|
|
20
|
+
from zarr import Group # type: ignore
|
|
21
|
+
from .actions import ActionScope
|
|
22
|
+
from .command_files import FileSpec
|
|
23
|
+
from .environment import Environment, Executable
|
|
24
|
+
from .loop import WorkflowLoop
|
|
25
|
+
from .json_like import JSONable, JSONed
|
|
26
|
+
from .parameters import Parameter, ResourceSpec
|
|
27
|
+
from .task import Task, TaskTemplate, TaskSchema, WorkflowTask, ElementSet
|
|
28
|
+
from .types import Resources
|
|
29
|
+
from .workflow import WorkflowTemplate
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ObjectListMultipleMatchError(ValueError):
|
|
35
|
+
"""
|
|
36
|
+
Thrown when an object looked up by unique attribute ends up with multiple objects
|
|
37
|
+
being matched.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ObjectList(JSONLike, Generic[T]):
|
|
42
|
+
"""
|
|
43
|
+
A list-like class that provides item access via a `get` method according to
|
|
44
|
+
attributes or dict-keys.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
objects : sequence
|
|
49
|
+
List of values of some type.
|
|
50
|
+
descriptor : str
|
|
51
|
+
Descriptive name for objects in the list.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# This would be in the docstring except it renders really wrongly!
|
|
55
|
+
# Type Parameters
|
|
56
|
+
# ---------------
|
|
57
|
+
# T
|
|
58
|
+
# The type of elements of the list.
|
|
59
|
+
|
|
60
|
+
def __init__(self, objects: Iterable[T], descriptor: str | None = None):
|
|
61
|
+
self._objects = list(objects)
|
|
62
|
+
self._descriptor = descriptor or "object"
|
|
63
|
+
self._object_is_dict: bool = False
|
|
64
|
+
self._validate()
|
|
65
|
+
|
|
66
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
|
|
67
|
+
obj = self.__class__(copy.deepcopy(self._objects, memo))
|
|
68
|
+
obj._descriptor = self._descriptor
|
|
69
|
+
obj._object_is_dict = self._object_is_dict
|
|
70
|
+
return obj
|
|
71
|
+
|
|
72
|
+
def _validate(self):
|
|
73
|
+
for idx, obj in enumerate(self._objects):
|
|
74
|
+
if isinstance(obj, dict):
|
|
75
|
+
obj = SimpleNamespace(**obj)
|
|
76
|
+
self._object_is_dict = True
|
|
77
|
+
self._objects[idx] = obj
|
|
78
|
+
|
|
79
|
+
def __len__(self):
|
|
80
|
+
return len(self._objects)
|
|
81
|
+
|
|
82
|
+
def __repr__(self):
|
|
83
|
+
return repr(self._objects)
|
|
84
|
+
|
|
85
|
+
def __str__(self):
|
|
86
|
+
return str([self._get_item(obj) for obj in self._objects])
|
|
87
|
+
|
|
88
|
+
def __iter__(self) -> Iterator[T]:
|
|
89
|
+
if self._object_is_dict:
|
|
90
|
+
return iter(self._get_item(obj) for obj in self._objects)
|
|
91
|
+
else:
|
|
92
|
+
return self._objects.__iter__()
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def __getitem__(self, key: int) -> T: ...
|
|
96
|
+
|
|
97
|
+
@overload
|
|
98
|
+
def __getitem__(self, key: slice) -> list[T]: ...
|
|
99
|
+
|
|
100
|
+
def __getitem__(self, key: int | slice) -> T | list[T]:
|
|
101
|
+
"""Provide list-like index access."""
|
|
102
|
+
if isinstance(key, slice):
|
|
103
|
+
return list(map(self._get_item, self._objects.__getitem__(key)))
|
|
104
|
+
else:
|
|
105
|
+
return self._get_item(self._objects.__getitem__(key))
|
|
106
|
+
|
|
107
|
+
def __contains__(self, item: T) -> bool:
|
|
108
|
+
if self._objects:
|
|
109
|
+
if type(item) is type(self._get_item(self._objects[0])):
|
|
110
|
+
return self._objects.__contains__(item)
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def __eq__(self, other: Any) -> bool:
|
|
114
|
+
return isinstance(other, self.__class__) and self._objects == other._objects
|
|
115
|
+
|
|
116
|
+
def _get_item(self, obj: T):
|
|
117
|
+
if self._object_is_dict:
|
|
118
|
+
return obj.__dict__
|
|
119
|
+
else:
|
|
120
|
+
return obj
|
|
121
|
+
|
|
122
|
+
def _get_obj_attr(self, obj: T, attr: str):
|
|
123
|
+
"""Overriding this function allows control over how the `get` functions behave."""
|
|
124
|
+
return getattr(obj, attr)
|
|
125
|
+
|
|
126
|
+
def __specified_objs(self, objs: Iterable[T], kwargs: dict[str, Any]) -> Iterator[T]:
|
|
127
|
+
for obj in objs:
|
|
128
|
+
for k, v in kwargs.items():
|
|
129
|
+
try:
|
|
130
|
+
if self._get_obj_attr(obj, k) != v:
|
|
131
|
+
break
|
|
132
|
+
except (AttributeError, KeyError):
|
|
133
|
+
break
|
|
134
|
+
else:
|
|
135
|
+
yield obj
|
|
136
|
+
|
|
137
|
+
def _get_all_from_objs(self, objs: Iterable[T], **kwargs):
|
|
138
|
+
# narrow down according to kwargs:
|
|
139
|
+
return [self._get_item(obj) for obj in self.__specified_objs(objs, kwargs)]
|
|
140
|
+
|
|
141
|
+
def get_all(self, **kwargs):
|
|
142
|
+
"""Get one or more objects from the object list, by specifying the value of the
|
|
143
|
+
access attribute, and optionally additional keyword-argument attribute values."""
|
|
144
|
+
|
|
145
|
+
return self._get_all_from_objs(self._objects, **kwargs)
|
|
146
|
+
|
|
147
|
+
def _handle_multi_results(
|
|
148
|
+
self, result: Sequence[T], kwargs: dict[str, Any]
|
|
149
|
+
) -> Sequence[T]:
|
|
150
|
+
if len(result) > 1:
|
|
151
|
+
raise ObjectListMultipleMatchError(
|
|
152
|
+
f"Multiple objects with attributes: {kwargs}."
|
|
153
|
+
)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def _validate_get(self, result: Sequence[T], kwargs: dict[str, Any]):
|
|
157
|
+
if not result:
|
|
158
|
+
available: list[dict[str, Any]] = []
|
|
159
|
+
for obj in self._objects:
|
|
160
|
+
attr_vals: dict[str, Any] = {}
|
|
161
|
+
for k in kwargs:
|
|
162
|
+
try:
|
|
163
|
+
attr_vals[k] = self._get_obj_attr(obj, k)
|
|
164
|
+
except (AttributeError, KeyError):
|
|
165
|
+
continue
|
|
166
|
+
available.append(attr_vals)
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"No {self._descriptor} objects with attributes: {kwargs}. Available "
|
|
169
|
+
f"objects have attributes: {tuple(available)!r}."
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
result = self._handle_multi_results(result, kwargs)
|
|
173
|
+
assert len(result) == 1
|
|
174
|
+
return result[0]
|
|
175
|
+
|
|
176
|
+
def get(self, **kwargs):
|
|
177
|
+
"""Get a single object from the object list, by specifying the value of the access
|
|
178
|
+
attribute, and optionally additional keyword-argument attribute values."""
|
|
179
|
+
return self._validate_get(self.get_all(**kwargs), kwargs)
|
|
180
|
+
|
|
181
|
+
@overload
|
|
182
|
+
def add_object(
|
|
183
|
+
self, obj: T, index: int = -1, *, skip_duplicates: Literal[False] = False
|
|
184
|
+
) -> int: ...
|
|
185
|
+
|
|
186
|
+
@overload
|
|
187
|
+
def add_object(
|
|
188
|
+
self, obj: T, index: int = -1, *, skip_duplicates: Literal[True]
|
|
189
|
+
) -> int | None: ...
|
|
190
|
+
|
|
191
|
+
def add_object(
|
|
192
|
+
self, obj: T, index: int = -1, *, skip_duplicates: bool = False
|
|
193
|
+
) -> None | int:
|
|
194
|
+
"""
|
|
195
|
+
Add an object to this object list.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
obj:
|
|
200
|
+
The object to add.
|
|
201
|
+
index:
|
|
202
|
+
Where to add it. Omit to append.
|
|
203
|
+
skip_duplicates:
|
|
204
|
+
If true, don't add the object if it is already in the list.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
The index of the added object, or ``None`` if the object was not added.
|
|
209
|
+
"""
|
|
210
|
+
if skip_duplicates and obj in self:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
if index < 0:
|
|
214
|
+
index += len(self) + 1
|
|
215
|
+
|
|
216
|
+
if self._object_is_dict:
|
|
217
|
+
obj = cast("T", SimpleNamespace(**cast("dict", obj)))
|
|
218
|
+
|
|
219
|
+
self._objects = self._objects[:index] + [obj] + self._objects[index:]
|
|
220
|
+
self._validate()
|
|
221
|
+
return index
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class DotAccessAttributeError(AttributeError):
|
|
225
|
+
def __init__(self, name: str, obj: DotAccessObjectList) -> None:
|
|
226
|
+
msg = f"{obj._descriptor.title()} {name!r} does not exist. "
|
|
227
|
+
if obj._objects:
|
|
228
|
+
attr = obj._access_attribute
|
|
229
|
+
obj_list = (f'"{getattr(obj, attr)}"' for obj in obj._objects)
|
|
230
|
+
msg += f"Available {obj._descriptor}s are: {', '.join(obj_list)}."
|
|
231
|
+
else:
|
|
232
|
+
msg += "The object list is empty."
|
|
233
|
+
if sys.version_info >= (3, 10):
|
|
234
|
+
super().__init__(msg, name=name, obj=obj)
|
|
235
|
+
else:
|
|
236
|
+
super().__init__(msg)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class DotAccessObjectList(ObjectList[T], Generic[T]):
|
|
240
|
+
"""
|
|
241
|
+
Provide dot-notation access via an access attribute for the case where the access
|
|
242
|
+
attribute uniquely identifies a single object.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
_objects:
|
|
247
|
+
The objects in the list.
|
|
248
|
+
access_attribute:
|
|
249
|
+
The main attribute for selection and filtering. A unique property.
|
|
250
|
+
descriptor: str
|
|
251
|
+
Descriptive name for the objects in the list.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
# This would be in the docstring except it renders really wrongly!
|
|
255
|
+
# Type Parameters
|
|
256
|
+
# ---------------
|
|
257
|
+
# T
|
|
258
|
+
# The type of elements of the list.
|
|
259
|
+
|
|
260
|
+
# access attributes must not be named after any "public" methods, to avoid confusion!
|
|
261
|
+
_pub_methods: ClassVar[tuple[str, ...]] = (
|
|
262
|
+
"get",
|
|
263
|
+
"get_all",
|
|
264
|
+
"add_object",
|
|
265
|
+
"add_objects",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self, _objects: Iterable[T], access_attribute: str, descriptor: str | None = None
|
|
270
|
+
):
|
|
271
|
+
self._access_attribute = access_attribute
|
|
272
|
+
self._index: Mapping[str, Sequence[int]]
|
|
273
|
+
super().__init__(_objects, descriptor=descriptor)
|
|
274
|
+
self._update_index()
|
|
275
|
+
|
|
276
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
|
|
277
|
+
obj = self.__class__(copy.deepcopy(self._objects, memo), self._access_attribute)
|
|
278
|
+
obj._descriptor = self._descriptor
|
|
279
|
+
obj._object_is_dict = self._object_is_dict
|
|
280
|
+
return obj
|
|
281
|
+
|
|
282
|
+
def _validate(self) -> None:
|
|
283
|
+
for idx, obj in enumerate(self._objects):
|
|
284
|
+
if not hasattr(obj, self._access_attribute):
|
|
285
|
+
raise TypeError(
|
|
286
|
+
f"Object {idx} does not have attribute {self._access_attribute!r}."
|
|
287
|
+
)
|
|
288
|
+
value = getattr(obj, self._access_attribute)
|
|
289
|
+
if value in self._pub_methods:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Access attribute {self._access_attribute!r} for object index {idx} "
|
|
292
|
+
f"cannot be the same as any of the methods of "
|
|
293
|
+
f"{self.__class__.__name__!r}, which are: {self._pub_methods!r}."
|
|
294
|
+
)
|
|
295
|
+
super()._validate()
|
|
296
|
+
|
|
297
|
+
def _update_index(self) -> None:
|
|
298
|
+
"""For quick look-up by access attribute."""
|
|
299
|
+
|
|
300
|
+
_index: dict[str, list[int]] = defaultdict(list)
|
|
301
|
+
for idx, obj in enumerate(self._objects):
|
|
302
|
+
attr_val: str = getattr(obj, self._access_attribute)
|
|
303
|
+
try:
|
|
304
|
+
_index[attr_val].append(idx)
|
|
305
|
+
except TypeError:
|
|
306
|
+
raise TypeError(
|
|
307
|
+
f"Access attribute values ({self._access_attribute!r}) must be hashable."
|
|
308
|
+
)
|
|
309
|
+
self._index = _index
|
|
310
|
+
|
|
311
|
+
def __getattr__(self, attribute: str):
|
|
312
|
+
if idx := self._index.get(attribute):
|
|
313
|
+
if len(idx) > 1:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Multiple objects with access attribute: {attribute!r}."
|
|
316
|
+
)
|
|
317
|
+
return self._get_item(self._objects[idx[0]])
|
|
318
|
+
elif not attribute.startswith("__"):
|
|
319
|
+
raise DotAccessAttributeError(attribute, self)
|
|
320
|
+
else:
|
|
321
|
+
raise AttributeError
|
|
322
|
+
|
|
323
|
+
def __dir__(self) -> Iterator[str]:
|
|
324
|
+
yield from super().__dir__()
|
|
325
|
+
yield from (getattr(obj, self._access_attribute) for obj in self._objects)
|
|
326
|
+
|
|
327
|
+
def list_attrs(self) -> tuple[str, ...]:
|
|
328
|
+
"""Get a tuple of the unique access-attribute values of the constituent objects."""
|
|
329
|
+
return tuple(self._index)
|
|
330
|
+
|
|
331
|
+
def get(self, access_attribute_value: str | None = None, **kwargs) -> T:
|
|
332
|
+
"""
|
|
333
|
+
Get an object from this list that matches the given criteria.
|
|
334
|
+
"""
|
|
335
|
+
vld_get_kwargs = kwargs
|
|
336
|
+
if access_attribute_value is not None:
|
|
337
|
+
vld_get_kwargs = {self._access_attribute: access_attribute_value, **kwargs}
|
|
338
|
+
|
|
339
|
+
return self._validate_get(
|
|
340
|
+
self.get_all(access_attribute_value=access_attribute_value, **kwargs),
|
|
341
|
+
vld_get_kwargs,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def get_all(self, access_attribute_value: str | None = None, **kwargs):
|
|
345
|
+
"""
|
|
346
|
+
Get all objects in this list that match the given criteria.
|
|
347
|
+
"""
|
|
348
|
+
# use the index to narrow down the search first:
|
|
349
|
+
if access_attribute_value is not None:
|
|
350
|
+
if (all_idx := self._index.get(access_attribute_value)) is None:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
f"Value {access_attribute_value!r} does not match the value of any "
|
|
353
|
+
f"object's attribute {self._access_attribute!r}. Available attribute "
|
|
354
|
+
f"values are: {self.list_attrs()!r}."
|
|
355
|
+
)
|
|
356
|
+
all_objs: Iterable[T] = (self._objects[idx] for idx in all_idx)
|
|
357
|
+
else:
|
|
358
|
+
all_objs = self._objects
|
|
359
|
+
|
|
360
|
+
return self._get_all_from_objs(all_objs, **kwargs)
|
|
361
|
+
|
|
362
|
+
@overload
|
|
363
|
+
def add_object(
|
|
364
|
+
self, obj: T, index: int = -1, *, skip_duplicates: Literal[False] = False
|
|
365
|
+
) -> int: ...
|
|
366
|
+
|
|
367
|
+
@overload
|
|
368
|
+
def add_object(
|
|
369
|
+
self, obj: T, index: int = -1, *, skip_duplicates: Literal[True]
|
|
370
|
+
) -> int | None: ...
|
|
371
|
+
|
|
372
|
+
def add_object(
|
|
373
|
+
self, obj: T, index: int = -1, *, skip_duplicates: bool = False
|
|
374
|
+
) -> int | None:
|
|
375
|
+
"""
|
|
376
|
+
Add an object to this list.
|
|
377
|
+
"""
|
|
378
|
+
if skip_duplicates:
|
|
379
|
+
new_index = super().add_object(obj, index, skip_duplicates=True)
|
|
380
|
+
else:
|
|
381
|
+
new_index = super().add_object(obj, index)
|
|
382
|
+
self._update_index()
|
|
383
|
+
return new_index
|
|
384
|
+
|
|
385
|
+
def add_objects(
|
|
386
|
+
self, objs: Iterable[T], index: int = -1, *, skip_duplicates: bool = False
|
|
387
|
+
) -> int:
|
|
388
|
+
"""
|
|
389
|
+
Add multiple objects to the list.
|
|
390
|
+
"""
|
|
391
|
+
if skip_duplicates:
|
|
392
|
+
for obj in objs:
|
|
393
|
+
if (i := self.add_object(obj, index, skip_duplicates=True)) is not None:
|
|
394
|
+
index = i + 1
|
|
395
|
+
else:
|
|
396
|
+
for obj in objs:
|
|
397
|
+
index = self.add_object(obj, index) + 1
|
|
398
|
+
return index
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class AppDataList(DotAccessObjectList[T], Generic[T]):
|
|
402
|
+
"""
|
|
403
|
+
An application-aware object list.
|
|
404
|
+
|
|
405
|
+
Type Parameters
|
|
406
|
+
---------------
|
|
407
|
+
T
|
|
408
|
+
The type of elements of the list.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
@override
|
|
412
|
+
def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
|
|
413
|
+
d = super()._postprocess_to_dict(d)
|
|
414
|
+
return {"_objects": d["_objects"]}
|
|
415
|
+
|
|
416
|
+
@classmethod
|
|
417
|
+
def _get_default_shared_data(cls) -> Mapping[str, ObjectList[JSONable]]:
|
|
418
|
+
return cls._app._shared_data
|
|
419
|
+
|
|
420
|
+
@overload
|
|
421
|
+
@classmethod
|
|
422
|
+
def from_json_like(
|
|
423
|
+
cls,
|
|
424
|
+
json_like: str,
|
|
425
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
426
|
+
is_hashed: bool = False,
|
|
427
|
+
) -> Self | None: ...
|
|
428
|
+
|
|
429
|
+
@overload
|
|
430
|
+
@classmethod
|
|
431
|
+
def from_json_like(
|
|
432
|
+
cls,
|
|
433
|
+
json_like: Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]],
|
|
434
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
435
|
+
is_hashed: bool = False,
|
|
436
|
+
) -> Self: ...
|
|
437
|
+
|
|
438
|
+
@overload
|
|
439
|
+
@classmethod
|
|
440
|
+
def from_json_like(
|
|
441
|
+
cls,
|
|
442
|
+
json_like: None,
|
|
443
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
444
|
+
is_hashed: bool = False,
|
|
445
|
+
) -> None: ...
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def from_json_like(
|
|
449
|
+
cls,
|
|
450
|
+
json_like: str | Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]] | None,
|
|
451
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
452
|
+
is_hashed: bool = False,
|
|
453
|
+
) -> Self | None:
|
|
454
|
+
"""
|
|
455
|
+
Make an instance of this class from JSON (or YAML) data.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
json_like:
|
|
460
|
+
The data to deserialise.
|
|
461
|
+
shared_data:
|
|
462
|
+
Shared context data.
|
|
463
|
+
is_hashed:
|
|
464
|
+
If True, accept a dict whose keys are hashes of the dict values.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
The deserialised object.
|
|
469
|
+
"""
|
|
470
|
+
if is_hashed:
|
|
471
|
+
assert isinstance(json_like, Mapping)
|
|
472
|
+
return super().from_json_like(
|
|
473
|
+
[
|
|
474
|
+
{**cast("Mapping", obj_js), "_hash_value": hash_val}
|
|
475
|
+
for hash_val, obj_js in json_like.items()
|
|
476
|
+
],
|
|
477
|
+
shared_data=shared_data,
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
return super().from_json_like(json_like, shared_data=shared_data)
|
|
481
|
+
|
|
482
|
+
def _remove_object(self, index: int):
|
|
483
|
+
self._objects.pop(index)
|
|
484
|
+
self._update_index()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class TaskList(AppDataList["Task"]):
|
|
488
|
+
"""A list-like container for a task-like list with dot-notation access by task
|
|
489
|
+
unique-name.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
_objects: list[~hpcflow.app.Task]
|
|
494
|
+
The tasks in this list.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
498
|
+
ChildObjectSpec(
|
|
499
|
+
name="_objects",
|
|
500
|
+
class_name="Task",
|
|
501
|
+
is_multiple=True,
|
|
502
|
+
is_single_attribute=True,
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def __init__(self, _objects: Iterable[Task]):
|
|
507
|
+
super().__init__(_objects, access_attribute="unique_name", descriptor="task")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class TaskTemplateList(AppDataList["TaskTemplate"]):
|
|
511
|
+
"""A list-like container for a task-like list with dot-notation access by task
|
|
512
|
+
unique-name.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
_objects: list[~hpcflow.app.TaskTemplate]
|
|
517
|
+
The task templates in this list.
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
521
|
+
ChildObjectSpec(
|
|
522
|
+
name="_objects",
|
|
523
|
+
class_name="TaskTemplate",
|
|
524
|
+
is_multiple=True,
|
|
525
|
+
is_single_attribute=True,
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
def __init__(self, _objects: Iterable[TaskTemplate]):
|
|
530
|
+
super().__init__(_objects, access_attribute="name", descriptor="task template")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class TaskSchemasList(AppDataList["TaskSchema"]):
|
|
534
|
+
"""A list-like container for a task schema list with dot-notation access by task
|
|
535
|
+
schema unique-name.
|
|
536
|
+
|
|
537
|
+
Parameters
|
|
538
|
+
----------
|
|
539
|
+
_objects: list[~hpcflow.app.TaskSchema]
|
|
540
|
+
The task schemas in this list.
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
544
|
+
ChildObjectSpec(
|
|
545
|
+
name="_objects",
|
|
546
|
+
class_name="TaskSchema",
|
|
547
|
+
is_multiple=True,
|
|
548
|
+
is_single_attribute=True,
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def __init__(self, _objects: Iterable[TaskSchema]):
|
|
553
|
+
super().__init__(_objects, access_attribute="name", descriptor="task schema")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class GroupList(AppDataList["Group"]):
|
|
557
|
+
"""A list-like container for the task schema group list with dot-notation access by
|
|
558
|
+
group name.
|
|
559
|
+
|
|
560
|
+
Parameters
|
|
561
|
+
----------
|
|
562
|
+
_objects: list[Group]
|
|
563
|
+
The groups in this list.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
567
|
+
ChildObjectSpec(
|
|
568
|
+
name="_objects",
|
|
569
|
+
class_name="Group",
|
|
570
|
+
is_multiple=True,
|
|
571
|
+
is_single_attribute=True,
|
|
572
|
+
),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
def __init__(self, _objects: Iterable[Group]):
|
|
576
|
+
super().__init__(_objects, access_attribute="name", descriptor="group")
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class EnvironmentsList(AppDataList["Environment"]):
|
|
580
|
+
"""
|
|
581
|
+
A list-like container for environments with dot-notation access by name.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
_objects: list[~hpcflow.app.Environment]
|
|
586
|
+
The environments in this list.
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
590
|
+
ChildObjectSpec(
|
|
591
|
+
name="_objects",
|
|
592
|
+
class_name="Environment",
|
|
593
|
+
is_multiple=True,
|
|
594
|
+
is_single_attribute=True,
|
|
595
|
+
),
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def __init__(self, _objects: Iterable[Environment]):
|
|
599
|
+
super().__init__(_objects, access_attribute="name", descriptor="environment")
|
|
600
|
+
|
|
601
|
+
def _get_obj_attr(self, obj: Environment, attr: str):
|
|
602
|
+
"""Overridden to lookup objects via the `specifiers` dict attribute"""
|
|
603
|
+
if attr in ("name", "_hash_value"):
|
|
604
|
+
return getattr(obj, attr)
|
|
605
|
+
else:
|
|
606
|
+
return obj.specifiers[attr]
|
|
607
|
+
|
|
608
|
+
def _handle_multi_results(
|
|
609
|
+
self, result: Sequence[Environment], kwargs: dict[str, Any]
|
|
610
|
+
) -> Sequence[Environment]:
|
|
611
|
+
"""If no specifiers were provided, match the environment with no specifiers,
|
|
612
|
+
if one exists."""
|
|
613
|
+
if len(result) > 1:
|
|
614
|
+
specifiers = {k: v for k, v in kwargs.items() if k != "name"}
|
|
615
|
+
if not specifiers:
|
|
616
|
+
for res_i in result:
|
|
617
|
+
if not res_i.specifiers:
|
|
618
|
+
return [res_i]
|
|
619
|
+
raise ObjectListMultipleMatchError(
|
|
620
|
+
f"Multiple objects with attributes: {kwargs}."
|
|
621
|
+
)
|
|
622
|
+
return result
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class ExecutablesList(AppDataList["Executable"]):
|
|
626
|
+
"""
|
|
627
|
+
A list-like container for environment executables with dot-notation access by
|
|
628
|
+
executable label.
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
_objects: list[~hpcflow.app.Executable]
|
|
633
|
+
The executables in this list.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
#: The environment containing these executables.
|
|
637
|
+
environment: Environment | None = None
|
|
638
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
639
|
+
ChildObjectSpec(
|
|
640
|
+
name="_objects",
|
|
641
|
+
class_name="Executable",
|
|
642
|
+
is_multiple=True,
|
|
643
|
+
is_single_attribute=True,
|
|
644
|
+
parent_ref="_executables_list",
|
|
645
|
+
),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
def __init__(self, _objects: Iterable[Executable]):
|
|
649
|
+
super().__init__(_objects, access_attribute="label", descriptor="executable")
|
|
650
|
+
self._set_parent_refs()
|
|
651
|
+
|
|
652
|
+
def __deepcopy__(self, memo: dict[int, Any]):
|
|
653
|
+
obj = super().__deepcopy__(memo)
|
|
654
|
+
obj.environment = self.environment
|
|
655
|
+
return obj
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class ParametersList(AppDataList["Parameter"]):
|
|
659
|
+
"""
|
|
660
|
+
A list-like container for parameters with dot-notation access by parameter type.
|
|
661
|
+
|
|
662
|
+
Parameters
|
|
663
|
+
----------
|
|
664
|
+
_objects: list[~hpcflow.app.Parameter]
|
|
665
|
+
The parameters in this list.
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
669
|
+
ChildObjectSpec(
|
|
670
|
+
name="_objects",
|
|
671
|
+
class_name="Parameter",
|
|
672
|
+
is_multiple=True,
|
|
673
|
+
is_single_attribute=True,
|
|
674
|
+
),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
def __init__(self, _objects: Iterable[Parameter]):
|
|
678
|
+
super().__init__(_objects, access_attribute="typ", descriptor="parameter")
|
|
679
|
+
|
|
680
|
+
def __getattr__(self, attribute: str) -> Parameter:
|
|
681
|
+
"""Overridden to provide a default Parameter object if none exists."""
|
|
682
|
+
try:
|
|
683
|
+
if not attribute.startswith("__"):
|
|
684
|
+
return super().__getattr__(attribute)
|
|
685
|
+
except (AttributeError, ValueError):
|
|
686
|
+
return self._app.Parameter(typ=attribute)
|
|
687
|
+
raise AttributeError
|
|
688
|
+
|
|
689
|
+
def get_all(self, access_attribute_value=None, **kwargs):
|
|
690
|
+
"""Overridden to provide a default Parameter object if none exists."""
|
|
691
|
+
typ = access_attribute_value if access_attribute_value else kwargs.get("typ")
|
|
692
|
+
try:
|
|
693
|
+
all_out = super().get_all(access_attribute_value, **kwargs)
|
|
694
|
+
except ValueError:
|
|
695
|
+
return [self._app.Parameter(typ=typ)]
|
|
696
|
+
else:
|
|
697
|
+
# `get_all` will not raise `ValueError` if `access_attribute_value` is
|
|
698
|
+
# None and the parameter `typ` is specified in `kwargs` instead:
|
|
699
|
+
return all_out or [self._app.Parameter(typ=typ)]
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class CommandFilesList(AppDataList["FileSpec"]):
|
|
703
|
+
"""
|
|
704
|
+
A list-like container for command files with dot-notation access by label.
|
|
705
|
+
|
|
706
|
+
Parameters
|
|
707
|
+
----------
|
|
708
|
+
_objects: list[~hpcflow.app.FileSpec]
|
|
709
|
+
The files in this list.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
713
|
+
ChildObjectSpec(
|
|
714
|
+
name="_objects",
|
|
715
|
+
class_name="FileSpec",
|
|
716
|
+
is_multiple=True,
|
|
717
|
+
is_single_attribute=True,
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def __init__(self, _objects: Iterable[FileSpec]):
|
|
722
|
+
super().__init__(_objects, access_attribute="label", descriptor="command file")
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class WorkflowTaskList(DotAccessObjectList["WorkflowTask"]):
|
|
726
|
+
"""
|
|
727
|
+
A list-like container for workflow tasks with dot-notation access by unique name.
|
|
728
|
+
|
|
729
|
+
Parameters
|
|
730
|
+
----------
|
|
731
|
+
_objects: list[~hpcflow.app.WorkflowTask]
|
|
732
|
+
The tasks in this list.
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
def __init__(self, _objects: Iterable[WorkflowTask]):
|
|
736
|
+
super().__init__(_objects, access_attribute="unique_name", descriptor="task")
|
|
737
|
+
|
|
738
|
+
def _reindex(self) -> None:
|
|
739
|
+
"""Re-assign the WorkflowTask index attributes so they match their order."""
|
|
740
|
+
for idx, item in enumerate(self._objects):
|
|
741
|
+
item._index = idx
|
|
742
|
+
self._update_index()
|
|
743
|
+
|
|
744
|
+
def add_object(
|
|
745
|
+
self, obj: WorkflowTask, index: int = -1, skip_duplicates=False
|
|
746
|
+
) -> int:
|
|
747
|
+
index = super().add_object(obj, index)
|
|
748
|
+
self._reindex()
|
|
749
|
+
return index
|
|
750
|
+
|
|
751
|
+
def _remove_object(self, index: int):
|
|
752
|
+
self._objects.pop(index)
|
|
753
|
+
self._reindex()
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
class WorkflowLoopList(DotAccessObjectList["WorkflowLoop"]):
|
|
757
|
+
"""
|
|
758
|
+
A list-like container for workflow loops with dot-notation access by name.
|
|
759
|
+
|
|
760
|
+
Parameters
|
|
761
|
+
----------
|
|
762
|
+
_objects: list[~hpcflow.app.WorkflowLoop]
|
|
763
|
+
The loops in this list.
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
def __init__(self, _objects: Iterable[WorkflowLoop]):
|
|
767
|
+
super().__init__(_objects, access_attribute="name", descriptor="loop")
|
|
768
|
+
|
|
769
|
+
def _remove_object(self, index: int):
|
|
770
|
+
self._objects.pop(index)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
class ResourceList(ObjectList["ResourceSpec"]):
|
|
774
|
+
"""
|
|
775
|
+
A list-like container for resources.
|
|
776
|
+
Each contained resource must have a unique scope.
|
|
777
|
+
|
|
778
|
+
Parameters
|
|
779
|
+
----------
|
|
780
|
+
_objects: list[~hpcflow.app.ResourceSpec]
|
|
781
|
+
The resource descriptions in this list.
|
|
782
|
+
"""
|
|
783
|
+
|
|
784
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
785
|
+
ChildObjectSpec(
|
|
786
|
+
name="_objects",
|
|
787
|
+
class_name="ResourceSpec",
|
|
788
|
+
is_multiple=True,
|
|
789
|
+
is_single_attribute=True,
|
|
790
|
+
dict_key_attr="scope",
|
|
791
|
+
parent_ref="_resource_list",
|
|
792
|
+
),
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
def __init__(self, _objects: Iterable[ResourceSpec]):
|
|
796
|
+
super().__init__(_objects, descriptor="resource specification")
|
|
797
|
+
self._element_set: ElementSet | None = None # assigned by parent ElementSet
|
|
798
|
+
self._workflow_template: WorkflowTemplate | None = (
|
|
799
|
+
None # assigned by parent WorkflowTemplate
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
# check distinct scopes for each item:
|
|
803
|
+
scopes = [scope.to_string() for scope in self.get_scopes()]
|
|
804
|
+
if len(set(scopes)) < len(scopes):
|
|
805
|
+
raise ValueError(
|
|
806
|
+
"Multiple `ResourceSpec` objects have the same scope. The scopes are "
|
|
807
|
+
f"{scopes!r}."
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
self._set_parent_refs()
|
|
811
|
+
|
|
812
|
+
def __deepcopy__(self, memo: dict[int, Any]):
|
|
813
|
+
obj = super().__deepcopy__(memo)
|
|
814
|
+
obj._element_set = self._element_set
|
|
815
|
+
obj._workflow_template = self._workflow_template
|
|
816
|
+
return obj
|
|
817
|
+
|
|
818
|
+
@property
|
|
819
|
+
def element_set(self) -> ElementSet | None:
|
|
820
|
+
"""
|
|
821
|
+
The parent element set, if a child of an element set.
|
|
822
|
+
"""
|
|
823
|
+
return self._element_set
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
def workflow_template(self) -> WorkflowTemplate | None:
|
|
827
|
+
"""
|
|
828
|
+
The parent workflow template, if a child of a workflow template.
|
|
829
|
+
"""
|
|
830
|
+
return self._workflow_template
|
|
831
|
+
|
|
832
|
+
def _postprocess_to_json(self, json_like):
|
|
833
|
+
"""Convert JSON doc to a dict keyed by action scope (like as can be
|
|
834
|
+
specified in the input YAML) instead of list."""
|
|
835
|
+
return {
|
|
836
|
+
self._app.ActionScope.from_json_like(
|
|
837
|
+
res_spec_js.pop("scope")
|
|
838
|
+
).to_string(): res_spec_js
|
|
839
|
+
for res_spec_js in json_like
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
@staticmethod
|
|
843
|
+
def __ensure_non_persistent(resource_spec: ResourceSpec) -> ResourceSpec:
|
|
844
|
+
"""
|
|
845
|
+
For any resources that are persistent, if they have a
|
|
846
|
+
`_resource_list` attribute, this means they are sourced from some
|
|
847
|
+
other persistent workflow, rather than, say, a workflow being
|
|
848
|
+
loaded right now, so make a non-persistent copy
|
|
849
|
+
|
|
850
|
+
Part of `normalise`.
|
|
851
|
+
"""
|
|
852
|
+
if resource_spec._value_group_idx is not None and (
|
|
853
|
+
resource_spec._resource_list is not None
|
|
854
|
+
):
|
|
855
|
+
return resource_spec.copy_non_persistent()
|
|
856
|
+
return resource_spec
|
|
857
|
+
|
|
858
|
+
@classmethod
|
|
859
|
+
def __is_ResourceSpec(cls, value) -> TypeIs[ResourceSpec]:
|
|
860
|
+
return isinstance(value, cls._app.ResourceSpec)
|
|
861
|
+
|
|
862
|
+
@classmethod
|
|
863
|
+
def normalise(cls, resources: Resources) -> Self:
|
|
864
|
+
"""Generate from resource-specs specified in potentially several ways."""
|
|
865
|
+
|
|
866
|
+
if not resources:
|
|
867
|
+
return cls([cls._app.ResourceSpec()])
|
|
868
|
+
elif isinstance(resources, ResourceList):
|
|
869
|
+
# Already a ResourceList
|
|
870
|
+
return cast("Self", resources)
|
|
871
|
+
elif isinstance(resources, dict):
|
|
872
|
+
return cls.from_json_like(cast("dict", resources))
|
|
873
|
+
elif cls.__is_ResourceSpec(resources):
|
|
874
|
+
return cls([resources])
|
|
875
|
+
else:
|
|
876
|
+
return cls(
|
|
877
|
+
(
|
|
878
|
+
cls._app.ResourceSpec.from_json_like(cast("dict", res_i))
|
|
879
|
+
if isinstance(res_i, dict)
|
|
880
|
+
else cls.__ensure_non_persistent(res_i)
|
|
881
|
+
)
|
|
882
|
+
for res_i in resources
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def get_scopes(self) -> Iterator[ActionScope]:
|
|
886
|
+
"""
|
|
887
|
+
Get the scopes of the contained resources.
|
|
888
|
+
"""
|
|
889
|
+
for rs in self._objects:
|
|
890
|
+
if rs.scope is not None:
|
|
891
|
+
yield rs.scope
|
|
892
|
+
|
|
893
|
+
def __get_for_scope(self, scope: ActionScope):
|
|
894
|
+
try:
|
|
895
|
+
return self.get(scope=scope)
|
|
896
|
+
except ValueError:
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
def __merge(self, our_spec: ResourceSpec | None, other_spec: ResourceSpec):
|
|
900
|
+
"""
|
|
901
|
+
Merge two resource specs that have the same scope, or just add the other one to
|
|
902
|
+
the list if we didn't already have it.
|
|
903
|
+
"""
|
|
904
|
+
if our_spec is not None:
|
|
905
|
+
for k, v in other_spec._get_members().items():
|
|
906
|
+
if getattr(our_spec, k, None) is None:
|
|
907
|
+
setattr(our_spec, f"_{k}", copy.deepcopy(v))
|
|
908
|
+
else:
|
|
909
|
+
self.add_object(copy.deepcopy(other_spec))
|
|
910
|
+
|
|
911
|
+
def merge_other(self, other: ResourceList):
|
|
912
|
+
"""Merge lower-precedence other resource list into this resource list."""
|
|
913
|
+
for scope_i in other.get_scopes():
|
|
914
|
+
self.__merge(self.__get_for_scope(scope_i), other.get(scope=scope_i))
|
|
915
|
+
|
|
916
|
+
def merge_one(self, other: ResourceSpec):
|
|
917
|
+
"""Merge lower-precedence other resource spec into this resource list.
|
|
918
|
+
|
|
919
|
+
This is a simplified version of :py:meth:`merge_other`.
|
|
920
|
+
"""
|
|
921
|
+
if other.scope is not None:
|
|
922
|
+
self.__merge(self.__get_for_scope(other.scope), other)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def index(obj_lst: ObjectList[T], obj: T) -> int:
|
|
926
|
+
"""
|
|
927
|
+
Get the index of the object in the list.
|
|
928
|
+
The item is checked for by object identity, not equality.
|
|
929
|
+
"""
|
|
930
|
+
for idx, item in enumerate(obj_lst._objects):
|
|
931
|
+
if obj is item:
|
|
932
|
+
return idx
|
|
933
|
+
raise ValueError(f"{obj!r} not in list.")
|