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.
Files changed (275) hide show
  1. hpcflow/__init__.py +2 -11
  2. hpcflow/__pyinstaller/__init__.py +5 -0
  3. hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
  4. hpcflow/_version.py +1 -1
  5. hpcflow/app.py +43 -0
  6. hpcflow/cli.py +2 -461
  7. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  8. hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
  9. hpcflow/data/jinja_templates/test/test_template.txt +8 -0
  10. hpcflow/data/programs/hello_world/README.md +1 -0
  11. hpcflow/data/programs/hello_world/hello_world.c +87 -0
  12. hpcflow/data/programs/hello_world/linux/hello_world +0 -0
  13. hpcflow/data/programs/hello_world/macos/hello_world +0 -0
  14. hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
  15. hpcflow/data/scripts/__init__.py +1 -0
  16. hpcflow/data/scripts/bad_script.py +2 -0
  17. hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
  18. hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
  19. hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
  20. hpcflow/data/scripts/do_nothing.py +2 -0
  21. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  22. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  23. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  24. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  25. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  26. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  27. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  28. hpcflow/data/scripts/generate_t1_file_01.py +7 -0
  29. hpcflow/data/scripts/import_future_script.py +7 -0
  30. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  31. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  32. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  33. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  34. hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
  35. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  36. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  37. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  38. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  39. hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
  40. hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
  41. hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
  42. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  43. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  44. hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
  45. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
  46. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  47. hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
  48. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
  49. hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
  50. hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
  51. hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
  52. hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
  53. hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
  54. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  55. hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
  56. hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
  57. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  58. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  59. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  60. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  61. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  62. hpcflow/data/scripts/parse_t1_file_01.py +4 -0
  63. hpcflow/data/scripts/script_exit_test.py +5 -0
  64. hpcflow/data/template_components/__init__.py +1 -0
  65. hpcflow/data/template_components/command_files.yaml +26 -0
  66. hpcflow/data/template_components/environments.yaml +13 -0
  67. hpcflow/data/template_components/parameters.yaml +14 -0
  68. hpcflow/data/template_components/task_schemas.yaml +139 -0
  69. hpcflow/data/workflows/workflow_1.yaml +5 -0
  70. hpcflow/examples.ipynb +1037 -0
  71. hpcflow/sdk/__init__.py +149 -0
  72. hpcflow/sdk/app.py +4266 -0
  73. hpcflow/sdk/cli.py +1479 -0
  74. hpcflow/sdk/cli_common.py +385 -0
  75. hpcflow/sdk/config/__init__.py +5 -0
  76. hpcflow/sdk/config/callbacks.py +246 -0
  77. hpcflow/sdk/config/cli.py +388 -0
  78. hpcflow/sdk/config/config.py +1410 -0
  79. hpcflow/sdk/config/config_file.py +501 -0
  80. hpcflow/sdk/config/errors.py +272 -0
  81. hpcflow/sdk/config/types.py +150 -0
  82. hpcflow/sdk/core/__init__.py +38 -0
  83. hpcflow/sdk/core/actions.py +3857 -0
  84. hpcflow/sdk/core/app_aware.py +25 -0
  85. hpcflow/sdk/core/cache.py +224 -0
  86. hpcflow/sdk/core/command_files.py +814 -0
  87. hpcflow/sdk/core/commands.py +424 -0
  88. hpcflow/sdk/core/element.py +2071 -0
  89. hpcflow/sdk/core/enums.py +221 -0
  90. hpcflow/sdk/core/environment.py +256 -0
  91. hpcflow/sdk/core/errors.py +1043 -0
  92. hpcflow/sdk/core/execute.py +207 -0
  93. hpcflow/sdk/core/json_like.py +809 -0
  94. hpcflow/sdk/core/loop.py +1320 -0
  95. hpcflow/sdk/core/loop_cache.py +282 -0
  96. hpcflow/sdk/core/object_list.py +933 -0
  97. hpcflow/sdk/core/parameters.py +3371 -0
  98. hpcflow/sdk/core/rule.py +196 -0
  99. hpcflow/sdk/core/run_dir_files.py +57 -0
  100. hpcflow/sdk/core/skip_reason.py +7 -0
  101. hpcflow/sdk/core/task.py +3792 -0
  102. hpcflow/sdk/core/task_schema.py +993 -0
  103. hpcflow/sdk/core/test_utils.py +538 -0
  104. hpcflow/sdk/core/types.py +447 -0
  105. hpcflow/sdk/core/utils.py +1207 -0
  106. hpcflow/sdk/core/validation.py +87 -0
  107. hpcflow/sdk/core/values.py +477 -0
  108. hpcflow/sdk/core/workflow.py +4820 -0
  109. hpcflow/sdk/core/zarr_io.py +206 -0
  110. hpcflow/sdk/data/__init__.py +13 -0
  111. hpcflow/sdk/data/config_file_schema.yaml +34 -0
  112. hpcflow/sdk/data/config_schema.yaml +260 -0
  113. hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
  114. hpcflow/sdk/data/files_spec_schema.yaml +5 -0
  115. hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
  116. hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
  117. hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
  118. hpcflow/sdk/demo/__init__.py +3 -0
  119. hpcflow/sdk/demo/cli.py +242 -0
  120. hpcflow/sdk/helper/__init__.py +3 -0
  121. hpcflow/sdk/helper/cli.py +137 -0
  122. hpcflow/sdk/helper/helper.py +300 -0
  123. hpcflow/sdk/helper/watcher.py +192 -0
  124. hpcflow/sdk/log.py +288 -0
  125. hpcflow/sdk/persistence/__init__.py +18 -0
  126. hpcflow/sdk/persistence/base.py +2817 -0
  127. hpcflow/sdk/persistence/defaults.py +6 -0
  128. hpcflow/sdk/persistence/discovery.py +39 -0
  129. hpcflow/sdk/persistence/json.py +954 -0
  130. hpcflow/sdk/persistence/pending.py +948 -0
  131. hpcflow/sdk/persistence/store_resource.py +203 -0
  132. hpcflow/sdk/persistence/types.py +309 -0
  133. hpcflow/sdk/persistence/utils.py +73 -0
  134. hpcflow/sdk/persistence/zarr.py +2388 -0
  135. hpcflow/sdk/runtime.py +320 -0
  136. hpcflow/sdk/submission/__init__.py +3 -0
  137. hpcflow/sdk/submission/enums.py +70 -0
  138. hpcflow/sdk/submission/jobscript.py +2379 -0
  139. hpcflow/sdk/submission/schedulers/__init__.py +281 -0
  140. hpcflow/sdk/submission/schedulers/direct.py +233 -0
  141. hpcflow/sdk/submission/schedulers/sge.py +376 -0
  142. hpcflow/sdk/submission/schedulers/slurm.py +598 -0
  143. hpcflow/sdk/submission/schedulers/utils.py +25 -0
  144. hpcflow/sdk/submission/shells/__init__.py +52 -0
  145. hpcflow/sdk/submission/shells/base.py +229 -0
  146. hpcflow/sdk/submission/shells/bash.py +504 -0
  147. hpcflow/sdk/submission/shells/os_version.py +115 -0
  148. hpcflow/sdk/submission/shells/powershell.py +352 -0
  149. hpcflow/sdk/submission/submission.py +1402 -0
  150. hpcflow/sdk/submission/types.py +140 -0
  151. hpcflow/sdk/typing.py +194 -0
  152. hpcflow/sdk/utils/arrays.py +69 -0
  153. hpcflow/sdk/utils/deferred_file.py +55 -0
  154. hpcflow/sdk/utils/hashing.py +16 -0
  155. hpcflow/sdk/utils/patches.py +31 -0
  156. hpcflow/sdk/utils/strings.py +69 -0
  157. hpcflow/tests/api/test_api.py +32 -0
  158. hpcflow/tests/conftest.py +123 -0
  159. hpcflow/tests/data/__init__.py +0 -0
  160. hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
  161. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  162. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  163. hpcflow/tests/data/workflow_1.json +10 -0
  164. hpcflow/tests/data/workflow_1.yaml +5 -0
  165. hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
  166. hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
  167. hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
  168. hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
  169. hpcflow/tests/programs/test_programs.py +180 -0
  170. hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
  171. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  172. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
  173. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  174. hpcflow/tests/scripts/test_main_scripts.py +1361 -0
  175. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  176. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  177. hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
  178. hpcflow/tests/unit/test_action.py +1066 -0
  179. hpcflow/tests/unit/test_action_rule.py +24 -0
  180. hpcflow/tests/unit/test_app.py +132 -0
  181. hpcflow/tests/unit/test_cache.py +46 -0
  182. hpcflow/tests/unit/test_cli.py +172 -0
  183. hpcflow/tests/unit/test_command.py +377 -0
  184. hpcflow/tests/unit/test_config.py +195 -0
  185. hpcflow/tests/unit/test_config_file.py +162 -0
  186. hpcflow/tests/unit/test_element.py +666 -0
  187. hpcflow/tests/unit/test_element_iteration.py +88 -0
  188. hpcflow/tests/unit/test_element_set.py +158 -0
  189. hpcflow/tests/unit/test_group.py +115 -0
  190. hpcflow/tests/unit/test_input_source.py +1479 -0
  191. hpcflow/tests/unit/test_input_value.py +398 -0
  192. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  193. hpcflow/tests/unit/test_json_like.py +1247 -0
  194. hpcflow/tests/unit/test_loop.py +2674 -0
  195. hpcflow/tests/unit/test_meta_task.py +325 -0
  196. hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
  197. hpcflow/tests/unit/test_object_list.py +116 -0
  198. hpcflow/tests/unit/test_parameter.py +243 -0
  199. hpcflow/tests/unit/test_persistence.py +664 -0
  200. hpcflow/tests/unit/test_resources.py +243 -0
  201. hpcflow/tests/unit/test_run.py +286 -0
  202. hpcflow/tests/unit/test_run_directories.py +29 -0
  203. hpcflow/tests/unit/test_runtime.py +9 -0
  204. hpcflow/tests/unit/test_schema_input.py +372 -0
  205. hpcflow/tests/unit/test_shell.py +129 -0
  206. hpcflow/tests/unit/test_slurm.py +39 -0
  207. hpcflow/tests/unit/test_submission.py +502 -0
  208. hpcflow/tests/unit/test_task.py +2560 -0
  209. hpcflow/tests/unit/test_task_schema.py +182 -0
  210. hpcflow/tests/unit/test_utils.py +616 -0
  211. hpcflow/tests/unit/test_value_sequence.py +549 -0
  212. hpcflow/tests/unit/test_values.py +91 -0
  213. hpcflow/tests/unit/test_workflow.py +827 -0
  214. hpcflow/tests/unit/test_workflow_template.py +186 -0
  215. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  216. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  217. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  218. hpcflow/tests/unit/utils/test_patches.py +5 -0
  219. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  220. hpcflow/tests/unit/utils/test_strings.py +97 -0
  221. hpcflow/tests/workflows/__init__.py +0 -0
  222. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  223. hpcflow/tests/workflows/test_jobscript.py +355 -0
  224. hpcflow/tests/workflows/test_run_status.py +198 -0
  225. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  226. hpcflow/tests/workflows/test_submission.py +140 -0
  227. hpcflow/tests/workflows/test_workflows.py +564 -0
  228. hpcflow/tests/workflows/test_zip.py +18 -0
  229. hpcflow/viz_demo.ipynb +6794 -0
  230. hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
  231. hpcflow-0.2.0a271.dist-info/METADATA +65 -0
  232. hpcflow-0.2.0a271.dist-info/RECORD +237 -0
  233. {hpcflow-0.1.15.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
  234. hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
  235. hpcflow/api.py +0 -490
  236. hpcflow/archive/archive.py +0 -307
  237. hpcflow/archive/cloud/cloud.py +0 -45
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -427
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -233
  243. hpcflow/copytree.py +0 -66
  244. hpcflow/data/examples/_config.yml +0 -14
  245. hpcflow/data/examples/damask/demo/1.run.yml +0 -4
  246. hpcflow/data/examples/damask/demo/2.process.yml +0 -29
  247. hpcflow/data/examples/damask/demo/geom.geom +0 -2052
  248. hpcflow/data/examples/damask/demo/load.load +0 -1
  249. hpcflow/data/examples/damask/demo/material.config +0 -185
  250. hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
  251. hpcflow/data/examples/damask/inputs/load.load +0 -1
  252. hpcflow/data/examples/damask/inputs/material.config +0 -185
  253. hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
  254. hpcflow/data/examples/damask/profiles/damask.yml +0 -4
  255. hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
  256. hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
  257. hpcflow/data/examples/damask/profiles/default.yml +0 -6
  258. hpcflow/data/examples/thinking.yml +0 -177
  259. hpcflow/errors.py +0 -2
  260. hpcflow/init_db.py +0 -37
  261. hpcflow/models.py +0 -2595
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -322
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -166
  268. hpcflow/variables.py +0 -543
  269. hpcflow-0.1.15.dist-info/METADATA +0 -168
  270. hpcflow-0.1.15.dist-info/RECORD +0 -45
  271. hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.15.dist-info/top_level.txt +0 -1
  273. /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
  274. /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
  275. /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