hpcflow 0.1.9__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 -462
- 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.9.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 -458
- hpcflow/archive/archive.py +0 -308
- hpcflow/archive/cloud/cloud.py +0 -47
- hpcflow/archive/cloud/errors.py +0 -9
- hpcflow/archive/cloud/providers/dropbox.py +0 -432
- hpcflow/archive/errors.py +0 -5
- hpcflow/base_db.py +0 -4
- hpcflow/config.py +0 -232
- 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 -2549
- hpcflow/nesting.py +0 -9
- hpcflow/profiles.py +0 -455
- hpcflow/project.py +0 -81
- hpcflow/scheduler.py +0 -323
- hpcflow/utils.py +0 -103
- hpcflow/validation.py +0 -167
- hpcflow/variables.py +0 -544
- hpcflow-0.1.9.dist-info/METADATA +0 -168
- hpcflow-0.1.9.dist-info/RECORD +0 -45
- hpcflow-0.1.9.dist-info/entry_points.txt +0 -8
- hpcflow-0.1.9.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,809 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serialization and deserialization mechanism intended to map between a complex
|
|
3
|
+
graph of objects and either JSON or YAML.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from collections.abc import Container, Sequence, Mapping
|
|
10
|
+
import copy
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import enum
|
|
13
|
+
from types import SimpleNamespace
|
|
14
|
+
from typing import overload, Protocol, cast, runtime_checkable, TYPE_CHECKING
|
|
15
|
+
from typing_extensions import final, override
|
|
16
|
+
|
|
17
|
+
from hpcflow.sdk.core.app_aware import AppAware
|
|
18
|
+
from hpcflow.sdk.typing import hydrate
|
|
19
|
+
from hpcflow.sdk import app, get_SDK_logger
|
|
20
|
+
from hpcflow.sdk.core.utils import get_md5_hash
|
|
21
|
+
from hpcflow.sdk.core.validation import get_schema
|
|
22
|
+
from hpcflow.sdk.core.errors import ToJSONLikeChildReferenceError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from typing import Any, ClassVar, Literal
|
|
26
|
+
from typing_extensions import Self, TypeAlias, TypeIs
|
|
27
|
+
from ..app import BaseApp
|
|
28
|
+
from .object_list import ObjectList
|
|
29
|
+
|
|
30
|
+
_BasicJsonTypes: TypeAlias = "int | float | str | None"
|
|
31
|
+
_WriteStructure: TypeAlias = (
|
|
32
|
+
"list[JSONable] | tuple[JSONable, ...] | set[JSONable] | dict[str, JSONable]"
|
|
33
|
+
)
|
|
34
|
+
JSONDocument: TypeAlias = "Sequence[JSONed] | Mapping[str, JSONed]"
|
|
35
|
+
JSONable: TypeAlias = "_WriteStructure | enum.Enum | BaseJSONLike | _BasicJsonTypes"
|
|
36
|
+
JSONed: TypeAlias = "JSONDocument | _BasicJsonTypes"
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
_ChildType: TypeAlias = "type[enum.Enum | JSONLike]"
|
|
40
|
+
_JSONDeserState: TypeAlias = "dict[str, dict[str, JSONed]] | None"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: Primitive types supported by the serialization mechanism.
|
|
44
|
+
PRIMITIVES = (
|
|
45
|
+
int,
|
|
46
|
+
float,
|
|
47
|
+
str,
|
|
48
|
+
type(None),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_SDK_logger = get_SDK_logger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@runtime_checkable
|
|
55
|
+
class _AltConstructFromJson(Protocol):
|
|
56
|
+
@classmethod
|
|
57
|
+
def _json_like_constructor(cls, json_like: Mapping[str, JSONed]) -> Self:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_base_json_like(value: JSONable) -> TypeIs[BaseJSONLike]:
|
|
62
|
+
return value is not None and hasattr(value, "to_json_like")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_MAX_DEPTH = 50
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def to_json_like(
|
|
70
|
+
obj: int,
|
|
71
|
+
shared_data: _JSONDeserState = None,
|
|
72
|
+
parent_refs: dict | None = None,
|
|
73
|
+
path: list | None = None,
|
|
74
|
+
) -> tuple[int, _JSONDeserState]: ...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
def to_json_like(
|
|
79
|
+
obj: float,
|
|
80
|
+
shared_data: _JSONDeserState = None,
|
|
81
|
+
parent_refs: dict | None = None,
|
|
82
|
+
path: list | None = None,
|
|
83
|
+
) -> tuple[float, _JSONDeserState]: ...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@overload
|
|
87
|
+
def to_json_like(
|
|
88
|
+
obj: str,
|
|
89
|
+
shared_data: _JSONDeserState = None,
|
|
90
|
+
parent_refs: dict | None = None,
|
|
91
|
+
path: list | None = None,
|
|
92
|
+
) -> tuple[str, _JSONDeserState]: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def to_json_like(
|
|
97
|
+
obj: None,
|
|
98
|
+
shared_data: _JSONDeserState = None,
|
|
99
|
+
parent_refs: dict | None = None,
|
|
100
|
+
path: list | None = None,
|
|
101
|
+
) -> tuple[None, _JSONDeserState]: ...
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def to_json_like(
|
|
106
|
+
obj: enum.Enum,
|
|
107
|
+
shared_data: _JSONDeserState = None,
|
|
108
|
+
parent_refs: dict | None = None,
|
|
109
|
+
path: list | None = None,
|
|
110
|
+
) -> tuple[str, _JSONDeserState]: ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@overload
|
|
114
|
+
def to_json_like(
|
|
115
|
+
obj: list[JSONable],
|
|
116
|
+
shared_data: _JSONDeserState = None,
|
|
117
|
+
parent_refs: dict | None = None,
|
|
118
|
+
path: list | None = None,
|
|
119
|
+
) -> tuple[Sequence[JSONed], _JSONDeserState]: ...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@overload
|
|
123
|
+
def to_json_like(
|
|
124
|
+
obj: tuple[JSONable, ...],
|
|
125
|
+
shared_data: _JSONDeserState = None,
|
|
126
|
+
parent_refs: dict | None = None,
|
|
127
|
+
path: list | None = None,
|
|
128
|
+
) -> tuple[Sequence[JSONed], _JSONDeserState]: ...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@overload
|
|
132
|
+
def to_json_like(
|
|
133
|
+
obj: set[JSONable],
|
|
134
|
+
shared_data: _JSONDeserState = None,
|
|
135
|
+
parent_refs: dict | None = None,
|
|
136
|
+
path: list | None = None,
|
|
137
|
+
) -> tuple[Sequence[JSONed], _JSONDeserState]: ...
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@overload
|
|
141
|
+
def to_json_like(
|
|
142
|
+
obj: dict[str, JSONable],
|
|
143
|
+
shared_data: _JSONDeserState = None,
|
|
144
|
+
parent_refs: dict | None = None,
|
|
145
|
+
path: list | None = None,
|
|
146
|
+
) -> tuple[Mapping[str, JSONed], _JSONDeserState]: ...
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
def to_json_like(
|
|
151
|
+
obj: BaseJSONLike,
|
|
152
|
+
shared_data: _JSONDeserState = None,
|
|
153
|
+
parent_refs: dict | None = None,
|
|
154
|
+
path: list | None = None,
|
|
155
|
+
) -> tuple[Mapping[str, JSONed], _JSONDeserState]: ...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def to_json_like(
|
|
159
|
+
obj: JSONable,
|
|
160
|
+
shared_data: _JSONDeserState = None,
|
|
161
|
+
parent_refs: dict | None = None,
|
|
162
|
+
path: list | None = None,
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
Convert the object to a JSON-like basic value tree.
|
|
166
|
+
Such trees are trivial to serialize as JSON or YAML.
|
|
167
|
+
"""
|
|
168
|
+
path = path or []
|
|
169
|
+
|
|
170
|
+
if len(path) > _MAX_DEPTH:
|
|
171
|
+
raise RuntimeError(f"I'm in too deep! Path is: {path}")
|
|
172
|
+
|
|
173
|
+
if isinstance(obj, (list, tuple, set)):
|
|
174
|
+
out_list: list[JSONed] = []
|
|
175
|
+
for idx, item in enumerate(obj):
|
|
176
|
+
if _is_base_json_like(item):
|
|
177
|
+
new_item, shared_data = item.to_json_like(
|
|
178
|
+
shared_data=shared_data,
|
|
179
|
+
exclude=frozenset((parent_refs or {}).values()),
|
|
180
|
+
path=[*path, idx],
|
|
181
|
+
)
|
|
182
|
+
out_list.append(new_item)
|
|
183
|
+
else:
|
|
184
|
+
new_std_item, shared_data = to_json_like(
|
|
185
|
+
item, shared_data=shared_data, path=[*path, idx]
|
|
186
|
+
)
|
|
187
|
+
out_list.append(new_std_item)
|
|
188
|
+
if isinstance(obj, tuple):
|
|
189
|
+
return tuple(out_list), shared_data
|
|
190
|
+
elif isinstance(obj, set):
|
|
191
|
+
return set(out_list), shared_data
|
|
192
|
+
else:
|
|
193
|
+
return out_list, shared_data
|
|
194
|
+
|
|
195
|
+
elif isinstance(obj, dict):
|
|
196
|
+
out_map: dict[str, JSONed] = {}
|
|
197
|
+
for dct_key, dct_val in obj.items():
|
|
198
|
+
if _is_base_json_like(dct_val):
|
|
199
|
+
try:
|
|
200
|
+
out_map[dct_key], shared_data = dct_val.to_json_like(
|
|
201
|
+
shared_data=shared_data,
|
|
202
|
+
exclude={(parent_refs or {}).get(dct_key)},
|
|
203
|
+
path=[*path, dct_key],
|
|
204
|
+
)
|
|
205
|
+
except ToJSONLikeChildReferenceError:
|
|
206
|
+
continue
|
|
207
|
+
else:
|
|
208
|
+
out_map[dct_key], shared_data = to_json_like(
|
|
209
|
+
dct_val,
|
|
210
|
+
shared_data=shared_data,
|
|
211
|
+
parent_refs=parent_refs,
|
|
212
|
+
path=[*path, dct_key],
|
|
213
|
+
)
|
|
214
|
+
return out_map, shared_data
|
|
215
|
+
|
|
216
|
+
elif isinstance(obj, PRIMITIVES):
|
|
217
|
+
return obj, shared_data
|
|
218
|
+
|
|
219
|
+
elif isinstance(obj, enum.Enum):
|
|
220
|
+
return obj.name, shared_data
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
return obj.to_json_like(shared_data=shared_data, path=path)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class ChildObjectSpec:
|
|
228
|
+
"""
|
|
229
|
+
Used to describe what the child structure of an class is so that the generic
|
|
230
|
+
deserializer can build the structure.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
#: The name of the attribute.
|
|
234
|
+
name: str
|
|
235
|
+
#: The name of the class (or class of members of a list) used to deserialize the
|
|
236
|
+
#: attribute.
|
|
237
|
+
class_name: str | None = None
|
|
238
|
+
#: The class (or class of members of a list) used to deserialize the
|
|
239
|
+
#: attribute.
|
|
240
|
+
class_obj: type[enum.Enum | BaseJSONLike] | None = None
|
|
241
|
+
# TODO: no need for class_obj/class_name if shared data?
|
|
242
|
+
#: The name of the key used in the JSON document, if different from the attribute
|
|
243
|
+
#: name.
|
|
244
|
+
json_like_name: str | None = None
|
|
245
|
+
#: If true, the attribute is really a list of instances,
|
|
246
|
+
#: or a dictionary if :attr:`dict_key_attr` is set.
|
|
247
|
+
is_multiple: bool = False
|
|
248
|
+
#: If set, the name of an attribute of the object to use as a dictionary key.
|
|
249
|
+
#: Requires that :attr:`is_multiple` be set as well.
|
|
250
|
+
dict_key_attr: str | None = None
|
|
251
|
+
#: If set, the name of an attribute of the object to use as a dictionary value.
|
|
252
|
+
#: If not set but :attr:`dict_key_attr` is set, the whole object is the value.
|
|
253
|
+
#: Requires that :attr:`dict_key_attr` be set as well.
|
|
254
|
+
dict_val_attr: str | None = None
|
|
255
|
+
#: If set, the attribute of the child object that contains a reference to its parent.
|
|
256
|
+
parent_ref: str | None = None
|
|
257
|
+
# TODO: do parent refs make sense when from shared? Prob not.
|
|
258
|
+
#: If true, enables special handling where there can be only one child descriptor
|
|
259
|
+
#: for a containing class.
|
|
260
|
+
is_single_attribute: bool = False
|
|
261
|
+
#: If true, the object is an enum member and should use special serialization rules.
|
|
262
|
+
is_enum: bool = False
|
|
263
|
+
#: If true, the child object is a dict, whose values are of the specified class.
|
|
264
|
+
#: The dict structure will remain.
|
|
265
|
+
is_dict_values: bool = False
|
|
266
|
+
#: If true, values that are not lists are cast to lists and multiple child objects
|
|
267
|
+
#: are instantiated for each dict value.
|
|
268
|
+
is_dict_values_ensure_list: bool = False
|
|
269
|
+
#: What key to look values up under in the shared data cache.
|
|
270
|
+
#: If unspecified, the shared data cache is ignored.
|
|
271
|
+
shared_data_name: str | None = None
|
|
272
|
+
#: What attribute provides the value of the key into the shared data cache.
|
|
273
|
+
#: If unspecified, a hash of the object dictionary is used.
|
|
274
|
+
#: Ignored if :py:attr:`~.shared_data_name` is unspecified.
|
|
275
|
+
shared_data_primary_key: str | None = None
|
|
276
|
+
# shared_data_secondary_keys: tuple[str, ...] | None = None # TODO: what's the point?
|
|
277
|
+
|
|
278
|
+
def __post_init__(self) -> None:
|
|
279
|
+
if self.class_name and self.class_obj:
|
|
280
|
+
raise ValueError("Specify at most one of `class_name` and `class_obj`.")
|
|
281
|
+
|
|
282
|
+
if self.dict_key_attr and not isinstance(self.dict_key_attr, str):
|
|
283
|
+
raise TypeError(
|
|
284
|
+
"`dict_key_attr` must be of type `str`, but has type "
|
|
285
|
+
f"{type(self.dict_key_attr)} with value {self.dict_key_attr}."
|
|
286
|
+
) # TODO: test raise
|
|
287
|
+
if self.dict_val_attr:
|
|
288
|
+
if not self.dict_key_attr:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
"If `dict_val_attr` is specified, `dict_key_attr` must be specified."
|
|
291
|
+
) # TODO: test raise
|
|
292
|
+
if not isinstance(self.dict_val_attr, str):
|
|
293
|
+
raise TypeError(
|
|
294
|
+
"`dict_val_attr` must be of type `str`, but has type "
|
|
295
|
+
f"{type(self.dict_val_attr)} with value {self.dict_val_attr}."
|
|
296
|
+
) # TODO: test raise
|
|
297
|
+
if not self.is_multiple and self.dict_key_attr:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
"If `dict_key_attr` is specified, `is_multiple` must be set to True."
|
|
300
|
+
)
|
|
301
|
+
if not self.is_multiple and self.is_dict_values:
|
|
302
|
+
raise ValueError(
|
|
303
|
+
"If `is_dict_values` is specified, `is_multiple` must be set to True."
|
|
304
|
+
)
|
|
305
|
+
if self.is_dict_values_ensure_list and not self.is_dict_values:
|
|
306
|
+
raise ValueError(
|
|
307
|
+
"If `is_dict_values_ensure_list` is specified, `is_dict_values` must be "
|
|
308
|
+
"set to True."
|
|
309
|
+
)
|
|
310
|
+
if self.parent_ref and not isinstance(self.parent_ref, str):
|
|
311
|
+
raise TypeError(
|
|
312
|
+
"`parent_ref` must be of type `str`, but has type "
|
|
313
|
+
f"{type(self.parent_ref)} with value {self.parent_ref}."
|
|
314
|
+
) # TODO: test raise
|
|
315
|
+
|
|
316
|
+
self.json_like_name = self.json_like_name or self.name
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@hydrate
|
|
320
|
+
class BaseJSONLike:
|
|
321
|
+
"""
|
|
322
|
+
An object that has a serialization as JSON or YAML.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
_class_namespace : namespace
|
|
327
|
+
Namespace whose attributes include the class definitions that might be
|
|
328
|
+
referenced (and so require instantiation) in child objects.
|
|
329
|
+
_shared_data_namespace : namespace
|
|
330
|
+
Namespace whose attributes include the shared data that might be referenced
|
|
331
|
+
in child objects.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
_child_objects: ClassVar[Sequence[ChildObjectSpec]] = ()
|
|
335
|
+
_validation_schema: ClassVar[str | None] = None
|
|
336
|
+
|
|
337
|
+
__class_namespace: ClassVar[dict[str, Any] | SimpleNamespace | BaseApp | None] = None
|
|
338
|
+
_hash_value: str | None
|
|
339
|
+
|
|
340
|
+
@overload
|
|
341
|
+
@classmethod
|
|
342
|
+
def _set_class_namespace(
|
|
343
|
+
cls, value: SimpleNamespace, is_dict: Literal[False] = False
|
|
344
|
+
) -> None: ...
|
|
345
|
+
|
|
346
|
+
@overload
|
|
347
|
+
@classmethod
|
|
348
|
+
def _set_class_namespace(
|
|
349
|
+
cls, value: dict[str, Any], is_dict: Literal[True]
|
|
350
|
+
) -> None: ...
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def _set_class_namespace(
|
|
354
|
+
cls, value: dict[str, Any] | SimpleNamespace, is_dict=False
|
|
355
|
+
) -> None:
|
|
356
|
+
cls.__class_namespace = value
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def _class_namespace(cls) -> dict[str, Any] | SimpleNamespace | BaseApp:
|
|
360
|
+
if (ns := cls.__class_namespace) is None:
|
|
361
|
+
raise ValueError(f"`{cls.__name__}` `class_namespace` must be set!")
|
|
362
|
+
return ns
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def __get_child_class(cls, child_spec: ChildObjectSpec) -> _ChildType | None:
|
|
366
|
+
if child_spec.class_obj:
|
|
367
|
+
return cast("_ChildType", child_spec.class_obj)
|
|
368
|
+
elif child_spec.class_name:
|
|
369
|
+
ns = cls._class_namespace()
|
|
370
|
+
if isinstance(ns, dict):
|
|
371
|
+
return ns[child_spec.class_name]
|
|
372
|
+
else:
|
|
373
|
+
return getattr(ns, child_spec.class_name)
|
|
374
|
+
else:
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def _get_default_shared_data(cls) -> Mapping[str, ObjectList[JSONable]]:
|
|
379
|
+
return {}
|
|
380
|
+
|
|
381
|
+
@overload
|
|
382
|
+
@classmethod
|
|
383
|
+
def from_json_like(
|
|
384
|
+
cls,
|
|
385
|
+
json_like: str,
|
|
386
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
387
|
+
) -> Self | None: ...
|
|
388
|
+
|
|
389
|
+
@overload
|
|
390
|
+
@classmethod
|
|
391
|
+
def from_json_like(
|
|
392
|
+
cls,
|
|
393
|
+
json_like: Sequence[Mapping[str, JSONed]] | Mapping[str, JSONed],
|
|
394
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
395
|
+
) -> Self: ...
|
|
396
|
+
|
|
397
|
+
@overload
|
|
398
|
+
@classmethod
|
|
399
|
+
def from_json_like(
|
|
400
|
+
cls,
|
|
401
|
+
json_like: None,
|
|
402
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
403
|
+
) -> None: ...
|
|
404
|
+
|
|
405
|
+
@classmethod
|
|
406
|
+
def from_json_like(
|
|
407
|
+
cls,
|
|
408
|
+
json_like: str | Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]] | None,
|
|
409
|
+
shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
|
|
410
|
+
) -> Self | None:
|
|
411
|
+
"""
|
|
412
|
+
Make an instance of this class from JSON (or YAML) data.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
json_like:
|
|
417
|
+
The data to deserialise.
|
|
418
|
+
shared_data:
|
|
419
|
+
Shared context data.
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
The deserialised object.
|
|
424
|
+
"""
|
|
425
|
+
shared_data = shared_data or cls._get_default_shared_data()
|
|
426
|
+
if isinstance(json_like, str):
|
|
427
|
+
json_like = cls._parse_from_string(json_like)
|
|
428
|
+
if json_like is None:
|
|
429
|
+
# e.g. optional attributes # TODO: is this still needed?
|
|
430
|
+
return None
|
|
431
|
+
return cls._from_json_like(copy.deepcopy(json_like), shared_data)
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def _parse_from_string(cls, string: str) -> dict[str, str] | None:
|
|
435
|
+
raise TypeError(f"unparseable {cls}: '{string}'")
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def __listify(v: JSONed, spec: ChildObjectSpec) -> list[JSONed]:
|
|
439
|
+
if spec.is_dict_values_ensure_list and isinstance(v, list):
|
|
440
|
+
return v
|
|
441
|
+
return [v]
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def __remap_child_seq(
|
|
445
|
+
cls, spec: ChildObjectSpec, json_like: JSONed
|
|
446
|
+
) -> tuple[list[JSONed], dict[str, list[int]]]:
|
|
447
|
+
if not spec.is_multiple:
|
|
448
|
+
return [json_like], {}
|
|
449
|
+
elif isinstance(json_like, list):
|
|
450
|
+
return json_like, {}
|
|
451
|
+
elif not isinstance(json_like, dict):
|
|
452
|
+
raise TypeError(
|
|
453
|
+
f"Child object {spec.name} of {cls.__name__!r} must be a list or "
|
|
454
|
+
f"dict, but is of type {type(json_like)} with value {json_like!r}."
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
multi_chd_objs: list[JSONed] = []
|
|
458
|
+
|
|
459
|
+
if spec.is_dict_values:
|
|
460
|
+
# (if is_dict_values) indices into multi_chd_objs that enable reconstruction
|
|
461
|
+
# of the source dict:
|
|
462
|
+
is_dict_values_idx: dict[str, list[int]] = defaultdict(list)
|
|
463
|
+
|
|
464
|
+
# keep as a dict
|
|
465
|
+
for k, v in json_like.items():
|
|
466
|
+
for item in cls.__listify(v, spec):
|
|
467
|
+
is_dict_values_idx[k].append(len(multi_chd_objs))
|
|
468
|
+
multi_chd_objs.append(item)
|
|
469
|
+
return multi_chd_objs, is_dict_values_idx
|
|
470
|
+
|
|
471
|
+
# want to cast to a list
|
|
472
|
+
if not spec.dict_key_attr:
|
|
473
|
+
raise ValueError(
|
|
474
|
+
f"{cls.__name__!r}: must specify a `dict_key_attr` for child "
|
|
475
|
+
f"object spec {spec.name!r}."
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
for k, v in json_like.items():
|
|
479
|
+
all_attrs: dict[str, JSONed] = {spec.dict_key_attr: k}
|
|
480
|
+
if spec.dict_val_attr:
|
|
481
|
+
all_attrs[spec.dict_val_attr] = v
|
|
482
|
+
elif isinstance(v, dict):
|
|
483
|
+
all_attrs.update(v)
|
|
484
|
+
else:
|
|
485
|
+
raise TypeError(
|
|
486
|
+
f"Value for key {k!r} must be a dict representing "
|
|
487
|
+
f"attributes of the {spec.name!r} child object "
|
|
488
|
+
f"(parent: {cls.__name__!r}). If it instead "
|
|
489
|
+
f"represents a single attribute, set the "
|
|
490
|
+
f"`dict_val_attr` of the child object spec."
|
|
491
|
+
)
|
|
492
|
+
multi_chd_objs.append(all_attrs)
|
|
493
|
+
|
|
494
|
+
return multi_chd_objs, {}
|
|
495
|
+
|
|
496
|
+
@classmethod
|
|
497
|
+
def __inflate_enum(cls, chd_cls: type[enum.Enum], multi_chd_objs: list[JSONed]):
|
|
498
|
+
out: list[JSONable] = []
|
|
499
|
+
for item in multi_chd_objs:
|
|
500
|
+
if item is None:
|
|
501
|
+
out.append(None)
|
|
502
|
+
elif not isinstance(item, str):
|
|
503
|
+
raise ValueError(
|
|
504
|
+
f"Enumeration {chd_cls!r} has no name {item!r}. Available"
|
|
505
|
+
f" names are: {chd_cls._member_names_!r}."
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
try:
|
|
509
|
+
out.append(getattr(chd_cls, item.upper()))
|
|
510
|
+
except AttributeError:
|
|
511
|
+
raise ValueError(
|
|
512
|
+
f"Enumeration {chd_cls!r} has no name {item!r}. Available"
|
|
513
|
+
f" names are: {chd_cls._member_names_!r}."
|
|
514
|
+
)
|
|
515
|
+
return out
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def _from_json_like(
|
|
519
|
+
cls,
|
|
520
|
+
json_like: Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]],
|
|
521
|
+
shared_data: Mapping[str, ObjectList[JSONable]],
|
|
522
|
+
) -> Self:
|
|
523
|
+
def from_json_like_item(
|
|
524
|
+
child_spec: ChildObjectSpec, json_like_i: JSONed
|
|
525
|
+
) -> JSONable:
|
|
526
|
+
if not (
|
|
527
|
+
child_spec.class_name
|
|
528
|
+
or child_spec.class_obj
|
|
529
|
+
or child_spec.is_multiple
|
|
530
|
+
or child_spec.shared_data_name
|
|
531
|
+
):
|
|
532
|
+
# Nothing to process:
|
|
533
|
+
return cast("JSONable", json_like_i)
|
|
534
|
+
|
|
535
|
+
# (if is_dict_values) indices into multi_chd_objs that enable reconstruction
|
|
536
|
+
# of the source dict:
|
|
537
|
+
multi_chd_objs, is_dict_values_idx = cls.__remap_child_seq(
|
|
538
|
+
child_spec, json_like_i
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
out: list[JSONable] = []
|
|
542
|
+
if child_spec.shared_data_name:
|
|
543
|
+
for i in multi_chd_objs:
|
|
544
|
+
if i is None:
|
|
545
|
+
out.append(i)
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
sd_lookup_kwargs: dict[str, JSONable]
|
|
549
|
+
if isinstance(i, str):
|
|
550
|
+
if i.startswith("hash:"):
|
|
551
|
+
sd_lookup_kwargs = {"_hash_value": i.removeprefix("hash:")}
|
|
552
|
+
else:
|
|
553
|
+
assert child_spec.shared_data_primary_key
|
|
554
|
+
sd_lookup_kwargs = {child_spec.shared_data_primary_key: i}
|
|
555
|
+
elif isinstance(i, dict):
|
|
556
|
+
sd_lookup_kwargs = i
|
|
557
|
+
else:
|
|
558
|
+
raise TypeError(
|
|
559
|
+
"Shared data reference must be a str or a dict."
|
|
560
|
+
) # TODO: test raise
|
|
561
|
+
out.append(
|
|
562
|
+
shared_data[child_spec.shared_data_name].get(**sd_lookup_kwargs)
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
chd_cls = cls.__get_child_class(child_spec)
|
|
566
|
+
assert chd_cls is not None
|
|
567
|
+
if issubclass(chd_cls, enum.Enum):
|
|
568
|
+
out = cls.__inflate_enum(chd_cls, multi_chd_objs)
|
|
569
|
+
else:
|
|
570
|
+
out.extend(
|
|
571
|
+
(
|
|
572
|
+
None
|
|
573
|
+
if item is None
|
|
574
|
+
else chd_cls.from_json_like(
|
|
575
|
+
cast("Any", item), # FIXME: This is "Trust me, bro!" hack
|
|
576
|
+
shared_data,
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
for item in multi_chd_objs
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if child_spec.is_dict_values:
|
|
583
|
+
if child_spec.is_dict_values_ensure_list:
|
|
584
|
+
return {k: [out[i] for i in v] for k, v in is_dict_values_idx.items()}
|
|
585
|
+
else:
|
|
586
|
+
return {k: out[v[0]] for k, v in is_dict_values_idx.items()}
|
|
587
|
+
|
|
588
|
+
elif not child_spec.is_multiple:
|
|
589
|
+
return out[0]
|
|
590
|
+
|
|
591
|
+
return out
|
|
592
|
+
|
|
593
|
+
if cls._validation_schema:
|
|
594
|
+
validation_schema = get_schema(cls._validation_schema)
|
|
595
|
+
validated = validation_schema.validate(json_like)
|
|
596
|
+
if not validated.is_valid:
|
|
597
|
+
raise ValueError(validated.get_failures_string())
|
|
598
|
+
|
|
599
|
+
json_like_copy = copy.deepcopy(json_like)
|
|
600
|
+
|
|
601
|
+
for child_spec in cls._child_objects:
|
|
602
|
+
if child_spec.is_single_attribute:
|
|
603
|
+
if len(cls._child_objects) > 1:
|
|
604
|
+
raise TypeError(
|
|
605
|
+
f"If ChildObjectSpec has `is_single_attribute=True`, only one "
|
|
606
|
+
f"ChildObjectSpec may be specified on the class. Specified child "
|
|
607
|
+
f"objects specs are: {cls._child_objects!r}."
|
|
608
|
+
)
|
|
609
|
+
json_like_copy = {child_spec.name: json_like_copy}
|
|
610
|
+
|
|
611
|
+
assert isinstance(json_like_copy, Mapping)
|
|
612
|
+
if child_spec.json_like_name and child_spec.json_like_name in json_like_copy:
|
|
613
|
+
json_like_copy = dict(json_like_copy)
|
|
614
|
+
json_like_copy[child_spec.name] = cast(
|
|
615
|
+
"JSONed",
|
|
616
|
+
from_json_like_item(
|
|
617
|
+
child_spec, json_like_copy.pop(child_spec.json_like_name)
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
assert isinstance(json_like_copy, Mapping)
|
|
622
|
+
|
|
623
|
+
need_hash = hasattr(cls, "_hash_value") and "_hash_value" not in json_like_copy
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
if issubclass(cls, _AltConstructFromJson):
|
|
627
|
+
obj = cls._json_like_constructor(json_like_copy)
|
|
628
|
+
else:
|
|
629
|
+
obj = cls(**json_like_copy)
|
|
630
|
+
except TypeError as err:
|
|
631
|
+
raise TypeError(
|
|
632
|
+
f"Failed initialisation of class {cls.__name__!r}. Check the signature. "
|
|
633
|
+
f"Caught TypeError: {err}"
|
|
634
|
+
) from err
|
|
635
|
+
|
|
636
|
+
if need_hash:
|
|
637
|
+
obj._set_hash()
|
|
638
|
+
return obj
|
|
639
|
+
|
|
640
|
+
def __set_parent_ref(self, chd_obj: Any, child_spec: ChildObjectSpec):
|
|
641
|
+
if chd_obj is not None:
|
|
642
|
+
assert child_spec.parent_ref
|
|
643
|
+
setattr(chd_obj, child_spec.parent_ref, self)
|
|
644
|
+
|
|
645
|
+
def _set_parent_refs(self, child_name_attrs: Mapping[str, str] | None = None):
|
|
646
|
+
"""Assign references to self on child objects that declare a parent ref
|
|
647
|
+
attribute."""
|
|
648
|
+
child_name_attrs = child_name_attrs or {}
|
|
649
|
+
for child_spec in self._child_objects:
|
|
650
|
+
if child_spec.parent_ref:
|
|
651
|
+
chd_name = child_name_attrs.get(child_spec.name, child_spec.name)
|
|
652
|
+
if child_spec.is_multiple:
|
|
653
|
+
for chd_obj in getattr(self, chd_name):
|
|
654
|
+
self.__set_parent_ref(chd_obj, child_spec)
|
|
655
|
+
else:
|
|
656
|
+
self.__set_parent_ref(getattr(self, chd_name), child_spec)
|
|
657
|
+
|
|
658
|
+
def _get_hash(self) -> str:
|
|
659
|
+
json_like = self.to_json_like()[0]
|
|
660
|
+
hash_val = self._get_hash_from_json_like(json_like)
|
|
661
|
+
return hash_val
|
|
662
|
+
|
|
663
|
+
def _set_hash(self) -> None:
|
|
664
|
+
self._hash_value = self._get_hash()
|
|
665
|
+
|
|
666
|
+
@staticmethod
|
|
667
|
+
def _get_hash_from_json_like(json_like) -> str:
|
|
668
|
+
json_like = copy.deepcopy(json_like)
|
|
669
|
+
json_like.pop("_hash_value", None)
|
|
670
|
+
return get_md5_hash(json_like)
|
|
671
|
+
|
|
672
|
+
@final
|
|
673
|
+
def to_dict(self) -> dict[str, Any]:
|
|
674
|
+
"""
|
|
675
|
+
Serialize this object as a dictionary.
|
|
676
|
+
"""
|
|
677
|
+
if hasattr(self, "__dict__"):
|
|
678
|
+
return self._postprocess_to_dict(dict(self.__dict__))
|
|
679
|
+
elif hasattr(self, "__slots__"):
|
|
680
|
+
return self._postprocess_to_dict(
|
|
681
|
+
{var_name: getattr(self, var_name) for var_name in self.__slots__}
|
|
682
|
+
)
|
|
683
|
+
else:
|
|
684
|
+
return self._postprocess_to_dict({})
|
|
685
|
+
|
|
686
|
+
def _postprocess_to_dict(self, dct: dict[str, Any]) -> dict[str, Any]:
|
|
687
|
+
"""
|
|
688
|
+
Apply any desired postprocessing to the results of :meth:`to_dict`.
|
|
689
|
+
"""
|
|
690
|
+
return dct
|
|
691
|
+
|
|
692
|
+
def to_json_like(
|
|
693
|
+
self,
|
|
694
|
+
dct: dict[str, JSONable] | None = None,
|
|
695
|
+
shared_data: _JSONDeserState = None,
|
|
696
|
+
exclude: Container[str | None] = (),
|
|
697
|
+
path: list | None = None,
|
|
698
|
+
) -> tuple[JSONDocument, _JSONDeserState]:
|
|
699
|
+
"""
|
|
700
|
+
Serialize this object as an object structure that can be trivially converted
|
|
701
|
+
to JSON. Note that YAML can also be produced from the result of this method;
|
|
702
|
+
it just requires a different final serialization step.
|
|
703
|
+
"""
|
|
704
|
+
if dct is None:
|
|
705
|
+
dct_value = {k: v for k, v in self.to_dict().items() if k not in exclude}
|
|
706
|
+
else:
|
|
707
|
+
dct_value = dct
|
|
708
|
+
|
|
709
|
+
parent_refs: dict[str, str] = {}
|
|
710
|
+
if self._child_objects:
|
|
711
|
+
for child_spec in self._child_objects:
|
|
712
|
+
if child_spec.is_single_attribute:
|
|
713
|
+
if len(self._child_objects) > 1:
|
|
714
|
+
raise TypeError(
|
|
715
|
+
"If ChildObjectSpec has `is_single_attribute=True`, only one "
|
|
716
|
+
"ChildObjectSpec may be specified on the class."
|
|
717
|
+
)
|
|
718
|
+
assert child_spec.json_like_name is not None
|
|
719
|
+
dct_value = dct_value[child_spec.json_like_name]
|
|
720
|
+
|
|
721
|
+
if child_spec.parent_ref:
|
|
722
|
+
parent_refs[child_spec.name] = child_spec.parent_ref
|
|
723
|
+
|
|
724
|
+
json_like_, shared_data = to_json_like(
|
|
725
|
+
dct_value, shared_data=shared_data, parent_refs=parent_refs, path=path
|
|
726
|
+
)
|
|
727
|
+
json_like: dict[str, JSONed] | list[JSONed] = cast("Any", json_like_)
|
|
728
|
+
shared_data = shared_data or {}
|
|
729
|
+
|
|
730
|
+
for child_spec in self._child_objects:
|
|
731
|
+
assert child_spec.json_like_name is not None
|
|
732
|
+
if child_spec.name in json_like:
|
|
733
|
+
assert isinstance(json_like, dict)
|
|
734
|
+
json_like[child_spec.json_like_name] = json_like.pop(child_spec.name)
|
|
735
|
+
|
|
736
|
+
if child_spec.shared_data_name:
|
|
737
|
+
assert isinstance(json_like, dict)
|
|
738
|
+
if child_spec.shared_data_name not in shared_data:
|
|
739
|
+
shared_data[child_spec.shared_data_name] = {}
|
|
740
|
+
|
|
741
|
+
chd_obj_js = json_like.pop(child_spec.json_like_name)
|
|
742
|
+
|
|
743
|
+
if not child_spec.is_multiple:
|
|
744
|
+
chd_obj_js = [chd_obj_js]
|
|
745
|
+
|
|
746
|
+
shared_keys: list[JSONed] = []
|
|
747
|
+
assert isinstance(chd_obj_js, (list, tuple, set))
|
|
748
|
+
for i in chd_obj_js:
|
|
749
|
+
if i is None:
|
|
750
|
+
continue
|
|
751
|
+
i.pop("_hash_value", None)
|
|
752
|
+
hash_i = self._get_hash_from_json_like(i)
|
|
753
|
+
shared_keys.append(f"hash:{hash_i}")
|
|
754
|
+
shared_data[child_spec.shared_data_name].setdefault(hash_i, i)
|
|
755
|
+
|
|
756
|
+
if not child_spec.is_multiple:
|
|
757
|
+
try:
|
|
758
|
+
json_like[child_spec.json_like_name] = shared_keys[0]
|
|
759
|
+
except IndexError:
|
|
760
|
+
json_like[child_spec.json_like_name] = None
|
|
761
|
+
else:
|
|
762
|
+
json_like[child_spec.json_like_name] = shared_keys
|
|
763
|
+
|
|
764
|
+
return self._postprocess_to_json(json_like), shared_data
|
|
765
|
+
|
|
766
|
+
def _postprocess_to_json(self, json_like: JSONDocument) -> JSONDocument:
|
|
767
|
+
return json_like
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
@hydrate
|
|
771
|
+
class JSONLike(BaseJSONLike, AppAware):
|
|
772
|
+
"""BaseJSONLike, where the class namespace is the App instance."""
|
|
773
|
+
|
|
774
|
+
__sdk_classes: ClassVar[list[type[BaseJSONLike]]] = []
|
|
775
|
+
|
|
776
|
+
@classmethod
|
|
777
|
+
def _class_namespace(cls) -> BaseApp:
|
|
778
|
+
return getattr(cls, cls._app_attr)
|
|
779
|
+
|
|
780
|
+
@classmethod
|
|
781
|
+
def __get_classes(cls) -> list[type[BaseJSONLike]]:
|
|
782
|
+
"""
|
|
783
|
+
Get the collection of actual SDK classes that conform to BaseJSONLike.
|
|
784
|
+
"""
|
|
785
|
+
if not cls.__sdk_classes:
|
|
786
|
+
for cls_name in app.sdk_classes:
|
|
787
|
+
cls2 = getattr(app, cls_name)
|
|
788
|
+
if isinstance(cls2, type) and issubclass(cls2, BaseJSONLike):
|
|
789
|
+
cls.__sdk_classes.append(cls2)
|
|
790
|
+
return cls.__sdk_classes
|
|
791
|
+
|
|
792
|
+
@override
|
|
793
|
+
def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
|
|
794
|
+
out = super()._postprocess_to_dict(d)
|
|
795
|
+
|
|
796
|
+
# remove parent references:
|
|
797
|
+
for cls in self.__get_classes():
|
|
798
|
+
for child_spec in cls._child_objects:
|
|
799
|
+
if child_spec.parent_ref:
|
|
800
|
+
# _SDK_logger.debug(
|
|
801
|
+
# f"removing parent reference {chd.parent_ref!r} from child "
|
|
802
|
+
# f"object {chd!r}."
|
|
803
|
+
# )
|
|
804
|
+
if (
|
|
805
|
+
self.__class__.__name__ == child_spec.class_name
|
|
806
|
+
or self.__class__ is child_spec.class_obj
|
|
807
|
+
):
|
|
808
|
+
out.pop(child_spec.parent_ref, None)
|
|
809
|
+
return out
|