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