sierra-research 1.3.6__py3-none-any.whl → 1.5.0__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 (254) hide show
  1. sierra/__init__.py +3 -3
  2. sierra/core/__init__.py +3 -3
  3. sierra/core/batchroot.py +223 -0
  4. sierra/core/cmdline.py +681 -1057
  5. sierra/core/compare.py +11 -0
  6. sierra/core/config.py +96 -88
  7. sierra/core/engine.py +306 -0
  8. sierra/core/execenv.py +380 -0
  9. sierra/core/expdef.py +11 -0
  10. sierra/core/experiment/__init__.py +1 -0
  11. sierra/core/experiment/bindings.py +150 -101
  12. sierra/core/experiment/definition.py +414 -245
  13. sierra/core/experiment/spec.py +83 -85
  14. sierra/core/exproot.py +44 -0
  15. sierra/core/generators/__init__.py +10 -0
  16. sierra/core/generators/experiment.py +528 -0
  17. sierra/core/generators/generator_factory.py +138 -137
  18. sierra/core/graphs/__init__.py +23 -0
  19. sierra/core/graphs/bcbridge.py +94 -0
  20. sierra/core/graphs/heatmap.py +245 -324
  21. sierra/core/graphs/pathset.py +27 -0
  22. sierra/core/graphs/schema.py +77 -0
  23. sierra/core/graphs/stacked_line.py +341 -0
  24. sierra/core/graphs/summary_line.py +506 -0
  25. sierra/core/logging.py +3 -2
  26. sierra/core/models/__init__.py +3 -1
  27. sierra/core/models/info.py +19 -0
  28. sierra/core/models/interface.py +52 -122
  29. sierra/core/pipeline/__init__.py +2 -5
  30. sierra/core/pipeline/pipeline.py +228 -126
  31. sierra/core/pipeline/stage1/__init__.py +10 -0
  32. sierra/core/pipeline/stage1/pipeline_stage1.py +45 -31
  33. sierra/core/pipeline/stage2/__init__.py +10 -0
  34. sierra/core/pipeline/stage2/pipeline_stage2.py +8 -11
  35. sierra/core/pipeline/stage2/runner.py +401 -0
  36. sierra/core/pipeline/stage3/__init__.py +12 -0
  37. sierra/core/pipeline/stage3/gather.py +321 -0
  38. sierra/core/pipeline/stage3/pipeline_stage3.py +37 -84
  39. sierra/core/pipeline/stage4/__init__.py +12 -2
  40. sierra/core/pipeline/stage4/pipeline_stage4.py +36 -354
  41. sierra/core/pipeline/stage5/__init__.py +12 -0
  42. sierra/core/pipeline/stage5/pipeline_stage5.py +33 -208
  43. sierra/core/pipeline/yaml.py +48 -0
  44. sierra/core/plugin.py +529 -62
  45. sierra/core/proc.py +11 -0
  46. sierra/core/prod.py +11 -0
  47. sierra/core/ros1/__init__.py +5 -1
  48. sierra/core/ros1/callbacks.py +22 -21
  49. sierra/core/ros1/cmdline.py +59 -88
  50. sierra/core/ros1/generators.py +159 -175
  51. sierra/core/ros1/variables/__init__.py +3 -0
  52. sierra/core/ros1/variables/exp_setup.py +122 -116
  53. sierra/core/startup.py +106 -76
  54. sierra/core/stat_kernels.py +4 -5
  55. sierra/core/storage.py +13 -32
  56. sierra/core/trampoline.py +30 -0
  57. sierra/core/types.py +116 -71
  58. sierra/core/utils.py +103 -106
  59. sierra/core/variables/__init__.py +1 -1
  60. sierra/core/variables/base_variable.py +12 -17
  61. sierra/core/variables/batch_criteria.py +387 -481
  62. sierra/core/variables/builtin.py +135 -0
  63. sierra/core/variables/exp_setup.py +19 -39
  64. sierra/core/variables/population_size.py +72 -76
  65. sierra/core/variables/variable_density.py +44 -68
  66. sierra/core/vector.py +1 -1
  67. sierra/main.py +256 -88
  68. sierra/plugins/__init__.py +119 -0
  69. sierra/plugins/compare/__init__.py +14 -0
  70. sierra/plugins/compare/graphs/__init__.py +19 -0
  71. sierra/plugins/compare/graphs/cmdline.py +120 -0
  72. sierra/plugins/compare/graphs/comparator.py +291 -0
  73. sierra/plugins/compare/graphs/inter_controller.py +531 -0
  74. sierra/plugins/compare/graphs/inter_scenario.py +297 -0
  75. sierra/plugins/compare/graphs/namecalc.py +53 -0
  76. sierra/plugins/compare/graphs/outputroot.py +73 -0
  77. sierra/plugins/compare/graphs/plugin.py +147 -0
  78. sierra/plugins/compare/graphs/preprocess.py +172 -0
  79. sierra/plugins/compare/graphs/schema.py +37 -0
  80. sierra/plugins/engine/__init__.py +14 -0
  81. sierra/plugins/engine/argos/__init__.py +18 -0
  82. sierra/plugins/{platform → engine}/argos/cmdline.py +144 -151
  83. sierra/plugins/{platform/argos/variables → engine/argos/generators}/__init__.py +5 -0
  84. sierra/plugins/engine/argos/generators/engine.py +394 -0
  85. sierra/plugins/engine/argos/plugin.py +393 -0
  86. sierra/plugins/{platform/argos/generators → engine/argos/variables}/__init__.py +5 -0
  87. sierra/plugins/engine/argos/variables/arena_shape.py +183 -0
  88. sierra/plugins/engine/argos/variables/cameras.py +240 -0
  89. sierra/plugins/engine/argos/variables/constant_density.py +112 -0
  90. sierra/plugins/engine/argos/variables/exp_setup.py +82 -0
  91. sierra/plugins/{platform → engine}/argos/variables/physics_engines.py +83 -87
  92. sierra/plugins/engine/argos/variables/population_constant_density.py +178 -0
  93. sierra/plugins/engine/argos/variables/population_size.py +115 -0
  94. sierra/plugins/engine/argos/variables/population_variable_density.py +123 -0
  95. sierra/plugins/engine/argos/variables/rendering.py +108 -0
  96. sierra/plugins/engine/ros1gazebo/__init__.py +18 -0
  97. sierra/plugins/engine/ros1gazebo/cmdline.py +175 -0
  98. sierra/plugins/{platform/ros1robot → engine/ros1gazebo}/generators/__init__.py +5 -0
  99. sierra/plugins/engine/ros1gazebo/generators/engine.py +125 -0
  100. sierra/plugins/engine/ros1gazebo/plugin.py +404 -0
  101. sierra/plugins/engine/ros1gazebo/variables/__init__.py +15 -0
  102. sierra/plugins/engine/ros1gazebo/variables/population_size.py +214 -0
  103. sierra/plugins/engine/ros1robot/__init__.py +18 -0
  104. sierra/plugins/engine/ros1robot/cmdline.py +159 -0
  105. sierra/plugins/{platform/ros1gazebo → engine/ros1robot}/generators/__init__.py +4 -0
  106. sierra/plugins/engine/ros1robot/generators/engine.py +95 -0
  107. sierra/plugins/engine/ros1robot/plugin.py +410 -0
  108. sierra/plugins/{hpc/local → engine/ros1robot/variables}/__init__.py +5 -0
  109. sierra/plugins/engine/ros1robot/variables/population_size.py +146 -0
  110. sierra/plugins/execenv/__init__.py +11 -0
  111. sierra/plugins/execenv/hpc/__init__.py +18 -0
  112. sierra/plugins/execenv/hpc/adhoc/__init__.py +18 -0
  113. sierra/plugins/execenv/hpc/adhoc/cmdline.py +30 -0
  114. sierra/plugins/execenv/hpc/adhoc/plugin.py +131 -0
  115. sierra/plugins/execenv/hpc/cmdline.py +137 -0
  116. sierra/plugins/execenv/hpc/local/__init__.py +18 -0
  117. sierra/plugins/execenv/hpc/local/cmdline.py +31 -0
  118. sierra/plugins/execenv/hpc/local/plugin.py +145 -0
  119. sierra/plugins/execenv/hpc/pbs/__init__.py +18 -0
  120. sierra/plugins/execenv/hpc/pbs/cmdline.py +30 -0
  121. sierra/plugins/execenv/hpc/pbs/plugin.py +121 -0
  122. sierra/plugins/execenv/hpc/slurm/__init__.py +18 -0
  123. sierra/plugins/execenv/hpc/slurm/cmdline.py +30 -0
  124. sierra/plugins/execenv/hpc/slurm/plugin.py +133 -0
  125. sierra/plugins/execenv/prefectserver/__init__.py +18 -0
  126. sierra/plugins/execenv/prefectserver/cmdline.py +66 -0
  127. sierra/plugins/execenv/prefectserver/dockerremote/__init__.py +18 -0
  128. sierra/plugins/execenv/prefectserver/dockerremote/cmdline.py +66 -0
  129. sierra/plugins/execenv/prefectserver/dockerremote/plugin.py +132 -0
  130. sierra/plugins/execenv/prefectserver/flow.py +66 -0
  131. sierra/plugins/execenv/prefectserver/local/__init__.py +18 -0
  132. sierra/plugins/execenv/prefectserver/local/cmdline.py +29 -0
  133. sierra/plugins/execenv/prefectserver/local/plugin.py +133 -0
  134. sierra/plugins/{hpc/adhoc → execenv/robot}/__init__.py +1 -0
  135. sierra/plugins/execenv/robot/turtlebot3/__init__.py +18 -0
  136. sierra/plugins/execenv/robot/turtlebot3/plugin.py +204 -0
  137. sierra/plugins/expdef/__init__.py +14 -0
  138. sierra/plugins/expdef/json/__init__.py +14 -0
  139. sierra/plugins/expdef/json/plugin.py +504 -0
  140. sierra/plugins/expdef/xml/__init__.py +14 -0
  141. sierra/plugins/expdef/xml/plugin.py +386 -0
  142. sierra/{core/hpc → plugins/proc}/__init__.py +1 -1
  143. sierra/plugins/proc/collate/__init__.py +15 -0
  144. sierra/plugins/proc/collate/cmdline.py +47 -0
  145. sierra/plugins/proc/collate/plugin.py +271 -0
  146. sierra/plugins/proc/compress/__init__.py +18 -0
  147. sierra/plugins/proc/compress/cmdline.py +47 -0
  148. sierra/plugins/proc/compress/plugin.py +123 -0
  149. sierra/plugins/proc/decompress/__init__.py +18 -0
  150. sierra/plugins/proc/decompress/plugin.py +96 -0
  151. sierra/plugins/proc/imagize/__init__.py +15 -0
  152. sierra/plugins/proc/imagize/cmdline.py +49 -0
  153. sierra/plugins/proc/imagize/plugin.py +270 -0
  154. sierra/plugins/proc/modelrunner/__init__.py +16 -0
  155. sierra/plugins/proc/modelrunner/plugin.py +250 -0
  156. sierra/plugins/proc/statistics/__init__.py +15 -0
  157. sierra/plugins/proc/statistics/cmdline.py +64 -0
  158. sierra/plugins/proc/statistics/plugin.py +390 -0
  159. sierra/plugins/{hpc → prod}/__init__.py +1 -0
  160. sierra/plugins/prod/graphs/__init__.py +18 -0
  161. sierra/plugins/prod/graphs/cmdline.py +269 -0
  162. sierra/plugins/prod/graphs/collate.py +279 -0
  163. sierra/plugins/prod/graphs/inter/__init__.py +13 -0
  164. sierra/plugins/prod/graphs/inter/generate.py +83 -0
  165. sierra/plugins/prod/graphs/inter/heatmap.py +86 -0
  166. sierra/plugins/prod/graphs/inter/line.py +134 -0
  167. sierra/plugins/prod/graphs/intra/__init__.py +15 -0
  168. sierra/plugins/prod/graphs/intra/generate.py +202 -0
  169. sierra/plugins/prod/graphs/intra/heatmap.py +74 -0
  170. sierra/plugins/prod/graphs/intra/line.py +114 -0
  171. sierra/plugins/prod/graphs/plugin.py +103 -0
  172. sierra/plugins/prod/graphs/targets.py +63 -0
  173. sierra/plugins/prod/render/__init__.py +18 -0
  174. sierra/plugins/prod/render/cmdline.py +72 -0
  175. sierra/plugins/prod/render/plugin.py +282 -0
  176. sierra/plugins/storage/__init__.py +5 -0
  177. sierra/plugins/storage/arrow/__init__.py +18 -0
  178. sierra/plugins/storage/arrow/plugin.py +38 -0
  179. sierra/plugins/storage/csv/__init__.py +9 -0
  180. sierra/plugins/storage/csv/plugin.py +12 -5
  181. sierra/version.py +3 -2
  182. sierra_research-1.5.0.dist-info/METADATA +238 -0
  183. sierra_research-1.5.0.dist-info/RECORD +186 -0
  184. {sierra_research-1.3.6.dist-info → sierra_research-1.5.0.dist-info}/WHEEL +1 -2
  185. sierra/core/experiment/xml.py +0 -454
  186. sierra/core/generators/controller_generator_parser.py +0 -34
  187. sierra/core/generators/exp_creator.py +0 -351
  188. sierra/core/generators/exp_generators.py +0 -142
  189. sierra/core/graphs/scatterplot2D.py +0 -109
  190. sierra/core/graphs/stacked_line_graph.py +0 -249
  191. sierra/core/graphs/stacked_surface_graph.py +0 -220
  192. sierra/core/graphs/summary_line_graph.py +0 -369
  193. sierra/core/hpc/cmdline.py +0 -142
  194. sierra/core/models/graphs.py +0 -87
  195. sierra/core/pipeline/stage2/exp_runner.py +0 -286
  196. sierra/core/pipeline/stage3/imagizer.py +0 -149
  197. sierra/core/pipeline/stage3/run_collator.py +0 -317
  198. sierra/core/pipeline/stage3/statistics_calculator.py +0 -478
  199. sierra/core/pipeline/stage4/graph_collator.py +0 -319
  200. sierra/core/pipeline/stage4/inter_exp_graph_generator.py +0 -240
  201. sierra/core/pipeline/stage4/intra_exp_graph_generator.py +0 -317
  202. sierra/core/pipeline/stage4/model_runner.py +0 -168
  203. sierra/core/pipeline/stage4/rendering.py +0 -283
  204. sierra/core/pipeline/stage4/yaml_config_loader.py +0 -103
  205. sierra/core/pipeline/stage5/inter_scenario_comparator.py +0 -328
  206. sierra/core/pipeline/stage5/intra_scenario_comparator.py +0 -989
  207. sierra/core/platform.py +0 -493
  208. sierra/core/plugin_manager.py +0 -369
  209. sierra/core/root_dirpath_generator.py +0 -241
  210. sierra/plugins/hpc/adhoc/plugin.py +0 -125
  211. sierra/plugins/hpc/local/plugin.py +0 -81
  212. sierra/plugins/hpc/pbs/__init__.py +0 -9
  213. sierra/plugins/hpc/pbs/plugin.py +0 -126
  214. sierra/plugins/hpc/slurm/__init__.py +0 -9
  215. sierra/plugins/hpc/slurm/plugin.py +0 -130
  216. sierra/plugins/platform/__init__.py +0 -9
  217. sierra/plugins/platform/argos/__init__.py +0 -9
  218. sierra/plugins/platform/argos/generators/platform_generators.py +0 -383
  219. sierra/plugins/platform/argos/plugin.py +0 -337
  220. sierra/plugins/platform/argos/variables/arena_shape.py +0 -145
  221. sierra/plugins/platform/argos/variables/cameras.py +0 -243
  222. sierra/plugins/platform/argos/variables/constant_density.py +0 -136
  223. sierra/plugins/platform/argos/variables/exp_setup.py +0 -113
  224. sierra/plugins/platform/argos/variables/population_constant_density.py +0 -175
  225. sierra/plugins/platform/argos/variables/population_size.py +0 -102
  226. sierra/plugins/platform/argos/variables/population_variable_density.py +0 -132
  227. sierra/plugins/platform/argos/variables/rendering.py +0 -104
  228. sierra/plugins/platform/ros1gazebo/__init__.py +0 -9
  229. sierra/plugins/platform/ros1gazebo/cmdline.py +0 -213
  230. sierra/plugins/platform/ros1gazebo/generators/platform_generators.py +0 -137
  231. sierra/plugins/platform/ros1gazebo/plugin.py +0 -335
  232. sierra/plugins/platform/ros1gazebo/variables/__init__.py +0 -10
  233. sierra/plugins/platform/ros1gazebo/variables/population_size.py +0 -204
  234. sierra/plugins/platform/ros1robot/__init__.py +0 -9
  235. sierra/plugins/platform/ros1robot/cmdline.py +0 -175
  236. sierra/plugins/platform/ros1robot/generators/platform_generators.py +0 -112
  237. sierra/plugins/platform/ros1robot/plugin.py +0 -373
  238. sierra/plugins/platform/ros1robot/variables/__init__.py +0 -10
  239. sierra/plugins/platform/ros1robot/variables/population_size.py +0 -146
  240. sierra/plugins/robot/__init__.py +0 -9
  241. sierra/plugins/robot/turtlebot3/__init__.py +0 -9
  242. sierra/plugins/robot/turtlebot3/plugin.py +0 -194
  243. sierra_research-1.3.6.data/data/share/man/man1/sierra-cli.1 +0 -2349
  244. sierra_research-1.3.6.data/data/share/man/man7/sierra-examples.7 +0 -488
  245. sierra_research-1.3.6.data/data/share/man/man7/sierra-exec-envs.7 +0 -331
  246. sierra_research-1.3.6.data/data/share/man/man7/sierra-glossary.7 +0 -285
  247. sierra_research-1.3.6.data/data/share/man/man7/sierra-platforms.7 +0 -358
  248. sierra_research-1.3.6.data/data/share/man/man7/sierra-usage.7 +0 -725
  249. sierra_research-1.3.6.data/data/share/man/man7/sierra.7 +0 -78
  250. sierra_research-1.3.6.dist-info/METADATA +0 -500
  251. sierra_research-1.3.6.dist-info/RECORD +0 -133
  252. sierra_research-1.3.6.dist-info/top_level.txt +0 -1
  253. {sierra_research-1.3.6.dist-info → sierra_research-1.5.0.dist-info}/entry_points.txt +0 -0
  254. {sierra_research-1.3.6.dist-info → sierra_research-1.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,504 @@
1
+ # Copyright 2024 John Harwell, All rights reserved.
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Plugin for parsing and manipulating template input files in XML format."""
5
+
6
+ # Core packages
7
+ import pathlib
8
+ import logging
9
+ import typing as tp
10
+ import json
11
+
12
+ # 3rd party packages
13
+ import implements
14
+ from jsonpath_ng.ext import parse as jpparse
15
+
16
+ # Project packages
17
+ from sierra.core.experiment import definition
18
+ from sierra.core import types, utils
19
+
20
+
21
+ class Writer:
22
+ """Write the XML experiment to the filesystem according to configuration.
23
+
24
+ More than one file may be written, as specified.
25
+ """
26
+
27
+ def __init__(self, tree: types.JSON) -> None:
28
+ self.tree = tree
29
+ self.logger = logging.getLogger(__name__)
30
+
31
+ def __call__(
32
+ self, write_config: definition.WriterConfig, base_opath: pathlib.Path
33
+ ) -> None:
34
+ for config in write_config.values:
35
+ self._write_with_config(base_opath, config)
36
+
37
+ def _write_with_config(self, base_opath: pathlib.Path, config: dict) -> None:
38
+ tree, src_root, opath = self._write_prepare_tree(base_opath, config)
39
+
40
+ self.logger.trace("Write tree@%s to %s", src_root, opath) # type: ignore
41
+
42
+ to_write = tree
43
+
44
+ with utils.utf8open(opath, "w") as f:
45
+ json.dump(to_write, f, indent=2)
46
+
47
+ def _write_prepare_tree(
48
+ self, base_opath: pathlib.Path, config: dict
49
+ ) -> tp.Tuple[tp.Optional[types.JSON], str, pathlib.Path]:
50
+ if config["src_parent"] is None:
51
+ src_root = config["src_tag"]
52
+ else:
53
+ src_root = "{0}.{1}".format(config["src_parent"], config["src_tag"])
54
+
55
+ expr = jpparse(src_root)
56
+ matches = expr.find(self.tree)
57
+ assert len(matches) == 1, "src_root was not unique!"
58
+ tree_out = matches[0].value
59
+
60
+ # Customizing the output write path is not required
61
+ if "opath_leaf" in config and config["opath_leaf"] is not None:
62
+ opath = base_opath.with_name(base_opath.name + config["opath_leaf"])
63
+ else:
64
+ opath = base_opath
65
+
66
+ return (tree_out, src_root, opath)
67
+
68
+
69
+ def root_querypath() -> str:
70
+ return "$"
71
+
72
+
73
+ @implements.implements(definition.BaseExpDef)
74
+ class ExpDef:
75
+ """Read, write, and modify parsed XML files into experiment definitions."""
76
+
77
+ def __init__(
78
+ self,
79
+ input_fpath: pathlib.Path,
80
+ write_config: tp.Optional[definition.WriterConfig] = None,
81
+ ) -> None:
82
+
83
+ self.write_config = write_config
84
+ self.input_fpath = input_fpath
85
+ with utils.utf8open(self.input_fpath, "r") as f:
86
+ self.tree = json.load(f)
87
+ self.element_adds = definition.ElementAddList()
88
+ self.attr_chgs = definition.AttrChangeSet()
89
+
90
+ self.logger = logging.getLogger(__name__)
91
+
92
+ def n_mods(self) -> tp.Tuple[int, int]:
93
+ return len(self.element_adds), len(self.attr_chgs)
94
+
95
+ def write_config_set(self, config: definition.WriterConfig) -> None:
96
+ """Set the write config for the object.
97
+
98
+ Provided for cases in which the configuration is dependent on whether or
99
+ not certain tags are present in the input file.
100
+
101
+ """
102
+ self.write_config = config
103
+
104
+ def write(self, base_opath: pathlib.Path) -> None:
105
+ assert self.write_config is not None, "Can't write without write config"
106
+
107
+ writer = Writer(self.tree)
108
+ writer(self.write_config, base_opath)
109
+
110
+ def flatten(self, keys: tp.List[str]) -> None:
111
+ """
112
+ Flatten a nested JSON structure.
113
+
114
+ Recursively searches for each of the supplies keys, and replaces the
115
+ values of all matching keys with the corresponding config files. The
116
+ paths to the nested config files are assumed to be specified relative to
117
+ the root/main config file, and to reside in subdirs/adjacent dirs to it.
118
+ """
119
+ for k in keys:
120
+ self.logger.debug("Flattening with key=%s", k)
121
+ self._flatten_recurse(self.tree, self.input_fpath, k)
122
+
123
+ def _flatten_recurse(
124
+ self, blob: types.JSON, prefix: pathlib.Path, path_key: str
125
+ ) -> None:
126
+ """
127
+ Recursive flattening implementation.
128
+
129
+ The use of recursion enables searching for simple key matches instead of
130
+ having to deal with complicated jsonpath expressions, which is a huge
131
+ win. Plus, it's generally faster than an iterating implementation when
132
+ it comes to large files.
133
+
134
+ Arguments:
135
+ blob: The tree of JSON containing filepath references to flatten.
136
+
137
+ prefix: The prefix which should be prepended to all values which
138
+ match ``prefix``. This allows nested JSON structures where
139
+ filepaths are specified relative to the root-level
140
+ configuration (really it's parent directory), which is very
141
+ convenient. Note that all paths must be relative to
142
+ root-level configuration--relativity to a sub-path will NOT
143
+ work.
144
+
145
+ path_key: The key to recursively search for. Not a substring--will
146
+ be checked for exact match.
147
+ """
148
+
149
+ def _flatten_update_path(parent, key: str, value) -> None:
150
+ # Base case
151
+ if path_key != key:
152
+ return
153
+
154
+ # Make relative to input prefix. This SHOULD work recursively for
155
+ # nested dirs, though I'm not 100% sure.
156
+ path = pathlib.Path(value)
157
+
158
+ if not path.is_absolute():
159
+ path = prefix.parent / path
160
+ value = str(path.resolve())
161
+
162
+ # If the file doesn't exist, that's an error, so don't catch the
163
+ # exception if that happens.
164
+ with open(path, "r") as f:
165
+ subblob = json.load(f)
166
+
167
+ self._flatten_recurse(subblob, path, path_key)
168
+
169
+ if isinstance(parent, dict):
170
+ parent.update(subblob)
171
+
172
+ # This ensures that the original <key,value> pair is removed
173
+ # from the parent.
174
+ parent.pop(path_key)
175
+
176
+ def _flatten_erase_key(_, __, value):
177
+ if isinstance(value, dict):
178
+ keys_to_erase = [key for key in value if path_key == key]
179
+ for key in reversed(keys_to_erase):
180
+ value.pop(key, None)
181
+
182
+ self._flatten_apply1(blob, _flatten_update_path)
183
+ self._flatten_apply2(blob, _flatten_erase_key)
184
+
185
+ def _flatten_apply1(self, blob: types.JSON, f: tp.Callable) -> None:
186
+ """Apply the given callable to every unstructured key-value pair.
187
+
188
+ "Unstructured" here means pairs where the value is a literal instead of
189
+ a list or dict.
190
+ """
191
+ if isinstance(blob, dict):
192
+ c = blob.copy()
193
+ for key, val in c.items():
194
+ if isinstance(val, (dict, list)):
195
+ # recurse on each value in dict. Key is ignored.
196
+ self._flatten_apply1(val, f)
197
+ else:
198
+ # Base case: literal
199
+ f(blob, key, val)
200
+
201
+ elif isinstance(blob, list):
202
+ for item in blob:
203
+ # Recurse on each item in list
204
+ self._flatten_apply1(item, f)
205
+
206
+ def _flatten_apply2(self, blob: types.JSON, f: tp.Callable) -> None:
207
+ """Apply the given callable to every structured key-value pair.
208
+
209
+ "Structured" here means pairs where the value is a list or dict instead
210
+ of a literal.
211
+
212
+ This function does not have a base case per-se, because we iterate
213
+ through each item in the dict/list passed in and call this function on
214
+ each one; recursion will terminate after we have exhaustively applied
215
+ the callback to all sub-blobs.
216
+ """
217
+ if isinstance(blob, dict):
218
+ for key, val in blob.items():
219
+ if isinstance(val, (dict, list)):
220
+ self._flatten_apply2(val, f)
221
+
222
+ f(blob, key, val)
223
+ elif isinstance(blob, list):
224
+ for item in blob:
225
+ self._flatten_apply2(item, f)
226
+
227
+ def attr_get(self, path: str, attr: str) -> tp.Optional[tp.Union[str, int, float]]:
228
+ expr = jpparse(path)
229
+ matches = expr.find(self.tree)
230
+
231
+ assert len(matches) <= 1, f"Path '{path}' to element was not unique!"
232
+
233
+ if len(matches) == 0:
234
+ return None
235
+
236
+ match = matches[0].value
237
+
238
+ if not isinstance(match, list):
239
+ match = [match]
240
+
241
+ for m in match:
242
+ if attr in m.keys() and not isinstance(m[attr], (list, dict)):
243
+ return m[attr]
244
+
245
+ return None
246
+
247
+ def attr_change(
248
+ self,
249
+ path: str,
250
+ attr: str,
251
+ value: tp.Union[str, int, float],
252
+ noprint: bool = False,
253
+ ) -> bool:
254
+
255
+ expr = jpparse(path)
256
+ matches = expr.find(self.tree)
257
+
258
+ if len(matches) == 0:
259
+ if not noprint:
260
+ self.logger.warning("Parent element '%s' not found", path)
261
+ return False
262
+
263
+ for m in matches:
264
+ match = m.value
265
+ if attr not in match.keys() or isinstance(match[attr], (list, dict)):
266
+ if not noprint:
267
+ self.logger.warning(
268
+ "Attribute '%s' not found in path '%s'", attr, m.full_path
269
+ )
270
+ return False
271
+
272
+ match[attr] = value
273
+ self.logger.trace(
274
+ "Modify attr: '%s/%s' = '%s'", m.full_path, attr, value # type: ignore
275
+ )
276
+
277
+ self.attr_chgs.add(definition.AttrChange(path, attr, value))
278
+ return True
279
+
280
+ def attr_add(
281
+ self,
282
+ path: str,
283
+ attr: str,
284
+ value: tp.Union[str, int, float],
285
+ noprint: bool = False,
286
+ ) -> bool:
287
+ expr = jpparse(path)
288
+ matches = expr.find(self.tree)
289
+
290
+ assert len(matches) <= 1, f"Path '{path}' to element was not unique!"
291
+
292
+ if len(matches) == 0:
293
+ if not noprint:
294
+ self.logger.warning("Node '%s' not found", path)
295
+ return False
296
+
297
+ for m in matches:
298
+ match = m.value
299
+ if attr in match:
300
+ if not noprint:
301
+ self.logger.warning(
302
+ "Attribute '%s' already in path '%s'", attr, m.full_path
303
+ )
304
+ return False
305
+
306
+ match[attr] = value
307
+ self.logger.trace(
308
+ "Add new attribute: '%s/%s' = '%s'", m.full_path, attr, value # type: ignore
309
+ )
310
+ self.attr_chgs.add(definition.AttrChange(path, attr, value))
311
+ return True
312
+
313
+ def has_element(self, path: str) -> bool:
314
+ expr = jpparse(path)
315
+ el = expr.find(self.tree)
316
+
317
+ assert len(el) <= 1, (
318
+ f"Path '{path}' to element was not unique! Perhaps "
319
+ "you have malform JSON?"
320
+ )
321
+
322
+ if el:
323
+ # If path maps to a literal, then we are pointing to an attribute,
324
+ # which is obviously not an element.
325
+ return isinstance(el[0].value, (list, dict))
326
+
327
+ return False
328
+
329
+ def has_attr(self, path: str, attr: str) -> bool:
330
+ expr = jpparse(path)
331
+ matches = expr.find(self.tree)
332
+
333
+ assert len(matches) <= 1, f"Path '{path}' to parent element was not unique!"
334
+
335
+ if len(matches) == 0:
336
+ return False
337
+
338
+ found = False
339
+
340
+ match = matches[0].value
341
+ if not isinstance(match, list):
342
+ match = [match]
343
+
344
+ for m in match:
345
+ for k in m:
346
+ # While python/JSON doesn't distinguish between a key which maps
347
+ # to a literal {bool, int, ...}, and one which maps to a
348
+ # sub-element, SIERRA does, because it treats one key as
349
+ # referring to an attribute mapping, and one referring to a
350
+ # sub-element.
351
+ if k == attr and not isinstance(m[k], (list, dict)):
352
+ assert (
353
+ not found
354
+ ), f"Specified attr '{attr}' is not unique in '{path}'"
355
+ found = True
356
+
357
+ return found
358
+
359
+ def element_change(self, path: str, tag: str, value: str) -> bool:
360
+ expr = jpparse(path)
361
+ el = expr.find(self.tree).value
362
+
363
+ if el is None:
364
+ self.logger.warning("Parent element '%s' not found", path)
365
+ return False
366
+
367
+ for child in el:
368
+ if child.tag == tag:
369
+ child.tag = value
370
+ self.logger.trace(
371
+ "Modify tag: '%s.%s' = '%s'", path, tag, value # type: ignore
372
+ )
373
+ return True
374
+
375
+ self.logger.warning("No such element '%s' found in '%s'", tag, path)
376
+ return False
377
+
378
+ def element_remove(self, path: str, tag: str, noprint: bool = False) -> bool:
379
+ expr = jpparse(path)
380
+ parents = expr.find(self.tree)
381
+
382
+ assert len(parents) <= 1, (
383
+ f"Path '{path}' to parent was not unique! If you want to remove "
384
+ "multiple matching elements, use elements_remove_all()"
385
+ )
386
+
387
+ if len(parents) == 0:
388
+ if not noprint:
389
+ self.logger.warning("Parent element '%s' not found", path)
390
+ return False
391
+
392
+ parent = parents[0].value
393
+ victims = jpparse(tag).find(parent)
394
+
395
+ if len(victims) == 0 or not isinstance(victims[0].value, (list, dict)):
396
+ if not noprint:
397
+ self.logger.warning("No victim '%s' found in parent '%s'", tag, path)
398
+ return False
399
+
400
+ del parent[tag]
401
+ return True
402
+
403
+ def element_remove_all(self, path: str, tag: str, noprint: bool = False) -> bool:
404
+
405
+ expr = jpparse(path)
406
+ parents = expr.find(self.tree)
407
+
408
+ if len(parents) == 0:
409
+ if not noprint:
410
+ self.logger.warning("Parent element '%s' not found", path)
411
+ return False
412
+
413
+ parent = parents[0].value
414
+
415
+ victims = jpparse(tag).find(parent)
416
+
417
+ if len(victims) == 0:
418
+ if not noprint:
419
+ self.logger.warning(
420
+ "No victims matching '%s' found in parent '%s'", tag, path
421
+ )
422
+ return False
423
+
424
+ del parent[tag]
425
+ return True
426
+
427
+ def element_add(
428
+ self,
429
+ path: str,
430
+ tag: str,
431
+ attr: tp.Optional[types.StrDict] = None,
432
+ allow_dup: bool = True,
433
+ noprint: bool = False,
434
+ ) -> bool:
435
+ """
436
+ Add tag name as a child element of enclosing parent.
437
+ """
438
+ expr = jpparse(path)
439
+ parents = expr.find(self.tree)
440
+
441
+ assert len(parents) <= 1, f"Path '{path}' to parent was not unique!"
442
+
443
+ if len(parents) == 0:
444
+ if not noprint:
445
+ self.logger.warning("Parent element '%s' not found", path)
446
+ return False
447
+
448
+ parent = parents[0].value
449
+
450
+ if not allow_dup:
451
+ child = jpparse(tag).find(parent)
452
+ if len(child):
453
+ if not noprint:
454
+ self.logger.warning(
455
+ "Child element '%s' already in parent '%s'", tag, path
456
+ )
457
+ return False
458
+
459
+ # Child doesn't exist--just assign to single sub-element.
460
+ parent[tag] = attr
461
+ self.logger.trace(
462
+ "Add new unique element: '%s.%s' = '%s'", # type: ignore
463
+ path,
464
+ tag,
465
+ str(attr),
466
+ )
467
+ else:
468
+ child = jpparse(tag).find(parent)
469
+
470
+ # Child element exists, so update it to be a list of sub-elements
471
+ # rather than a single sub-elements.
472
+ if len(child):
473
+ d = [parent[tag], attr]
474
+ jpparse(tag).update(parent, d)
475
+ else:
476
+ # Child doesn't exist--just assign to single sub-element.
477
+ parent[tag] = attr
478
+
479
+ self.element_adds.append(definition.ElementAdd(path, tag, attr, allow_dup))
480
+ return True
481
+
482
+
483
+ def unpickle(
484
+ fpath: pathlib.Path,
485
+ ) -> tp.Optional[tp.Union[definition.AttrChangeSet, definition.ElementAddList]]:
486
+ """Unickle all XML modifications from the pickle file at the path.
487
+
488
+ You don't know how many there are, so go until you get an exception.
489
+
490
+ """
491
+ try:
492
+ return definition.AttrChangeSet.unpickle(fpath)
493
+ except EOFError:
494
+ pass
495
+
496
+ try:
497
+ return definition.ElementAddList.unpickle(fpath)
498
+ except EOFError:
499
+ pass
500
+
501
+ raise NotImplementedError
502
+
503
+
504
+ __all__ = ["ExpDef", "unpickle"]
@@ -0,0 +1,14 @@
1
+ # Copyright 2024 John Harwell, All rights reserved.
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Container module for the XML expdef plugin."""
5
+
6
+ # Core packages
7
+
8
+ # 3rd party packages
9
+
10
+ # Project packages
11
+
12
+
13
+ def sierra_plugin_type() -> str:
14
+ return "pipeline"