biosim_schema 1.0.1__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.
- biosim_schema/__init__.py +8 -0
- biosim_schema/fieldextraction/__init__.py +0 -0
- biosim_schema/fieldextraction/extract_engine_mappings.py +287 -0
- biosim_schema/fieldextraction/extract_summary.py +219 -0
- biosim_schema/fieldextraction/extract_webform_fields.py +466 -0
- biosim_schema/fieldextraction/generate_marshmallow.py +258 -0
- biosim_schema/schema/biosim_schema.yaml +129 -0
- biosim_schema/schema/components/biosim_composition.yaml +435 -0
- biosim_schema/schema/components/biosim_compute.yaml +383 -0
- biosim_schema/schema/components/biosim_files.yaml +89 -0
- biosim_schema/schema/components/biosim_observables.yaml +135 -0
- biosim_schema/schema/components/biosim_potentials.yaml +406 -0
- biosim_schema/schema/components/biosim_quantities.yaml +697 -0
- biosim_schema/schema/components/biosim_settings.yaml +841 -0
- biosim_schema/schema/components/biosim_stages.yaml +725 -0
- biosim_schema/schema/components/biosim_topology.yaml +134 -0
- biosim_schema/schema/components/biosim_trajectory.yaml +261 -0
- biosim_schema/schema/generated/schema_enginemappings.json +1700 -0
- biosim_schema/utils/generate_artifacts.py +405 -0
- biosim_schema/utils/paths.py +11 -0
- biosim_schema-1.0.1.dist-info/METADATA +71 -0
- biosim_schema-1.0.1.dist-info/RECORD +25 -0
- biosim_schema-1.0.1.dist-info/WHEEL +4 -0
- biosim_schema-1.0.1.dist-info/entry_points.txt +3 -0
- biosim_schema-1.0.1.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Extract engine mappings from a JSON-LD schema.
|
|
2
|
+
|
|
3
|
+
This script walks the schema starting from a root class, collects slot-level
|
|
4
|
+
and enum-level `engine_mapping` annotations, and writes forward and reverse
|
|
5
|
+
lookup tables to `schema_enginemappings.json`.
|
|
6
|
+
|
|
7
|
+
The schema_enginemappings.json file is used to extract metadata from MD topology, trajectory, and log files via the biosim-extractor.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MappingsExtractor:
|
|
15
|
+
"""Extract forward and reverse engine mappings from a schema.
|
|
16
|
+
|
|
17
|
+
The extractor indexes schema classes, slots, and enums, then traverses the
|
|
18
|
+
class tree from a root class to build:
|
|
19
|
+
- a forward map: schema path -> engine mapping entries
|
|
20
|
+
- a reverse map: engine key -> schema path -> permissible values
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, filepath):
|
|
24
|
+
"""Initialize the extractor.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
filepath: Path to the JSON-LD schema file.
|
|
28
|
+
"""
|
|
29
|
+
self.filepath = filepath
|
|
30
|
+
self.schema = {}
|
|
31
|
+
|
|
32
|
+
self.slot_index = {}
|
|
33
|
+
self.class_index = {}
|
|
34
|
+
self.enum_index = {}
|
|
35
|
+
|
|
36
|
+
self.forward = {}
|
|
37
|
+
self.reverse = {}
|
|
38
|
+
|
|
39
|
+
# ----------------------------
|
|
40
|
+
# Entry
|
|
41
|
+
# ----------------------------
|
|
42
|
+
def extract(self, root_class="SimulationMetadata"):
|
|
43
|
+
"""Extract mappings from the schema.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
root_class: Name of the schema class to start traversal from.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A dictionary with `forward` and `reverse` mapping tables.
|
|
50
|
+
"""
|
|
51
|
+
with open(self.filepath) as f:
|
|
52
|
+
self.schema = json.load(f)
|
|
53
|
+
|
|
54
|
+
# Indexes
|
|
55
|
+
self.slot_index = {s["name"]: s for s in self.schema.get("slots", [])}
|
|
56
|
+
self.class_index = {c["name"]: c for c in self.schema.get("classes", [])}
|
|
57
|
+
self.enum_index = {e["name"]: e for e in self.schema.get("enums", [])}
|
|
58
|
+
|
|
59
|
+
self.forward = {}
|
|
60
|
+
self.reverse = {}
|
|
61
|
+
|
|
62
|
+
self._walk_class(root_class, path=root_class)
|
|
63
|
+
|
|
64
|
+
return {"forward": self.forward, "reverse": self.reverse}
|
|
65
|
+
|
|
66
|
+
# ----------------------------
|
|
67
|
+
# Recursive traversal
|
|
68
|
+
# ----------------------------
|
|
69
|
+
def _walk_class(self, class_name, path):
|
|
70
|
+
"""Recursively walk a class and its nested slot ranges.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
class_name: Class name to traverse.
|
|
74
|
+
path: Dot-separated schema path for the current class.
|
|
75
|
+
"""
|
|
76
|
+
cls = self.class_index.get(class_name, {})
|
|
77
|
+
|
|
78
|
+
for slot_name in cls.get("slots", []):
|
|
79
|
+
slot = self.slot_index[slot_name]
|
|
80
|
+
slot_path = f"{path}.{slot_name}"
|
|
81
|
+
|
|
82
|
+
self._handle_slot(slot_name, slot, slot_path)
|
|
83
|
+
|
|
84
|
+
# Recurse into nested classes
|
|
85
|
+
range_ = slot.get("range")
|
|
86
|
+
if range_ in self.class_index:
|
|
87
|
+
self._walk_class(range_, slot_path)
|
|
88
|
+
|
|
89
|
+
# ----------------------------
|
|
90
|
+
# Slot handler
|
|
91
|
+
# ----------------------------
|
|
92
|
+
def _handle_slot(self, slot_name, slot, path):
|
|
93
|
+
"""Collect mappings for a slot and its enum range, if any.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
slot_name: Slot name.
|
|
97
|
+
slot: Slot definition.
|
|
98
|
+
path: Dot-separated schema path for the slot.
|
|
99
|
+
"""
|
|
100
|
+
mappings = self._get_engine_mappings(slot)
|
|
101
|
+
|
|
102
|
+
for m in mappings:
|
|
103
|
+
for norm in self._normalise_mapping(m):
|
|
104
|
+
self._add_mapping(path, norm)
|
|
105
|
+
|
|
106
|
+
# Enum handling
|
|
107
|
+
range_ = slot.get("range")
|
|
108
|
+
is_multivalued = slot.get("multivalued", False)
|
|
109
|
+
if range_ in self.enum_index:
|
|
110
|
+
self._handle_enum(
|
|
111
|
+
slot_name, self.enum_index[range_], path, multivalued=is_multivalued
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# ----------------------------
|
|
115
|
+
# Enum handler
|
|
116
|
+
# ----------------------------
|
|
117
|
+
def _handle_enum(self, slot_name, enum, path, multivalued):
|
|
118
|
+
"""Collect mappings for all permissible values of an enum.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
slot_name: Slot name using the enum.
|
|
122
|
+
enum: Enum definition.
|
|
123
|
+
path: Dot-separated schema path for the slot.
|
|
124
|
+
"""
|
|
125
|
+
for pv in enum.get("permissible_values", []):
|
|
126
|
+
# don't use alias, can't validate against schema
|
|
127
|
+
# pv_name = pv.get("aliases", [pv["text"]])[0]
|
|
128
|
+
pv_name = pv["text"]
|
|
129
|
+
|
|
130
|
+
mappings = self._get_engine_mappings(pv)
|
|
131
|
+
|
|
132
|
+
for m in mappings:
|
|
133
|
+
for norm in self._normalise_mapping(m):
|
|
134
|
+
self._add_mapping(
|
|
135
|
+
path, norm, enum_value=pv_name, multivalued=multivalued
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# ----------------------------
|
|
139
|
+
# Extract engine mappings
|
|
140
|
+
# ----------------------------
|
|
141
|
+
def _get_engine_mappings(self, obj):
|
|
142
|
+
"""Return engine mappings declared in annotations.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
obj: Schema object that may contain an `engine_mapping` annotation.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A list of mapping dictionaries, or an empty list if none exist.
|
|
149
|
+
"""
|
|
150
|
+
for ann in obj.get("annotations", []):
|
|
151
|
+
if ann.get("tag") == "engine_mapping":
|
|
152
|
+
return ann.get("value", [])
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
# ----------------------------
|
|
156
|
+
# NORMALISATION
|
|
157
|
+
# ----------------------------
|
|
158
|
+
def _normalise_mapping(self, mapping):
|
|
159
|
+
"""Normalize a raw mapping into one or more key/value/unit entries.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
mapping: Raw engine mapping dictionary.
|
|
163
|
+
|
|
164
|
+
Yields:
|
|
165
|
+
Normalized mapping dictionaries with keys:
|
|
166
|
+
`engine`, `key`, `value`, and optional `unit`.
|
|
167
|
+
"""
|
|
168
|
+
keys = mapping.get("key")
|
|
169
|
+
|
|
170
|
+
if keys is None:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
if not isinstance(keys, list):
|
|
174
|
+
keys = [keys]
|
|
175
|
+
|
|
176
|
+
for k in keys:
|
|
177
|
+
yield {
|
|
178
|
+
"engine": mapping["engine"],
|
|
179
|
+
"key": k,
|
|
180
|
+
"value": mapping.get("value"),
|
|
181
|
+
"unit": mapping.get("unit"),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# ----------------------------
|
|
185
|
+
# Add to forward + reverse maps
|
|
186
|
+
# ----------------------------
|
|
187
|
+
def _add_mapping(self, path, mapping, enum_value=None, multivalued=False):
|
|
188
|
+
"""Add a normalized mapping to the forward and reverse indexes.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
path: Dot-separated schema path.
|
|
192
|
+
mapping: Normalized mapping dictionary.
|
|
193
|
+
enum_value: Optional enum permissible value associated with the mapping.
|
|
194
|
+
"""
|
|
195
|
+
engine = mapping["engine"]
|
|
196
|
+
key = mapping["key"]
|
|
197
|
+
unit = mapping.get("unit") # NEW
|
|
198
|
+
|
|
199
|
+
# ============================
|
|
200
|
+
# FORWARD MAP
|
|
201
|
+
# ============================
|
|
202
|
+
self.forward.setdefault(engine, {})
|
|
203
|
+
|
|
204
|
+
entry = {"key": key, "value": mapping.get("value")}
|
|
205
|
+
|
|
206
|
+
# only include unit if present
|
|
207
|
+
if unit is not None:
|
|
208
|
+
entry["unit"] = unit
|
|
209
|
+
|
|
210
|
+
if enum_value:
|
|
211
|
+
self.forward[engine].setdefault(path, {})
|
|
212
|
+
self.forward[engine][path].setdefault(enum_value, [])
|
|
213
|
+
self.forward[engine][path][enum_value].append(entry)
|
|
214
|
+
else:
|
|
215
|
+
self.forward[engine].setdefault(path, [])
|
|
216
|
+
self.forward[engine][path].append(entry)
|
|
217
|
+
|
|
218
|
+
# ============================
|
|
219
|
+
# REVERSE MAP
|
|
220
|
+
# ============================
|
|
221
|
+
if engine not in self.reverse:
|
|
222
|
+
self.reverse[engine] = {}
|
|
223
|
+
|
|
224
|
+
if key not in self.reverse[engine]:
|
|
225
|
+
self.reverse[engine][key] = {
|
|
226
|
+
"by_path": {},
|
|
227
|
+
"path_metadata": {},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
rev = self.reverse[engine][key]
|
|
231
|
+
rev["by_path"].setdefault(path, {})
|
|
232
|
+
|
|
233
|
+
if multivalued:
|
|
234
|
+
rev["path_metadata"].setdefault(path, {})
|
|
235
|
+
rev["path_metadata"][path]["multivalued"] = True
|
|
236
|
+
|
|
237
|
+
if enum_value:
|
|
238
|
+
raw_value = mapping.get("value")
|
|
239
|
+
|
|
240
|
+
if isinstance(raw_value, list):
|
|
241
|
+
value_keys = raw_value
|
|
242
|
+
else:
|
|
243
|
+
value_keys = [raw_value]
|
|
244
|
+
|
|
245
|
+
for vk in value_keys:
|
|
246
|
+
vk = str(vk)
|
|
247
|
+
rev["by_path"][path].setdefault(vk, [])
|
|
248
|
+
|
|
249
|
+
if enum_value not in rev["by_path"][path][vk]:
|
|
250
|
+
rev["by_path"][path][vk].append(enum_value)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# =========================
|
|
254
|
+
# ENTRY POINT
|
|
255
|
+
# =========================
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def parse_args():
|
|
259
|
+
"""Parse command-line arguments.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Parsed ``argparse.Namespace`` object.
|
|
263
|
+
"""
|
|
264
|
+
parser = argparse.ArgumentParser(
|
|
265
|
+
description="Extract engine mappings from a JSON-LD schema."
|
|
266
|
+
)
|
|
267
|
+
parser.add_argument("jsonldschema", help="Path to JSON-LD schema file")
|
|
268
|
+
parser.add_argument("--output", "-o", help="Output file path (default: stdout)")
|
|
269
|
+
return parser.parse_args()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def main():
|
|
273
|
+
"""Entry point: parse args, run extraction, and write output."""
|
|
274
|
+
args = parse_args()
|
|
275
|
+
|
|
276
|
+
extractor = MappingsExtractor(args.jsonldschema)
|
|
277
|
+
result = extractor.extract()
|
|
278
|
+
|
|
279
|
+
if args.output:
|
|
280
|
+
with open(args.output, "w") as f:
|
|
281
|
+
json.dump(result, f, indent=2, ensure_ascii=False)
|
|
282
|
+
else:
|
|
283
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build simplified schema summaries from schema_webformfields.json.
|
|
3
|
+
|
|
4
|
+
Outputs:
|
|
5
|
+
- YAML nested summary (default behavior)
|
|
6
|
+
- CSV flattened summary (one row per leaf field)
|
|
7
|
+
|
|
8
|
+
CSV hierarchy rules:
|
|
9
|
+
- section: top-level schema section
|
|
10
|
+
- subsection_1/subsection_2: intermediate hierarchy levels only
|
|
11
|
+
- field: leaf field key (never duplicated as a subsection column)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import csv
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
METADATA_KEYS = ("label", "hint", "typehint", "placeholder", "units", "example")
|
|
22
|
+
"""tuple[str, ...]: Field metadata keys copied from each webform node into the summary."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _summarise(node):
|
|
26
|
+
"""Recursively extract field metadata, skipping enum fields.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
node: A field definition dict from schema_webformfields.json.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
dict or None: Summarised field metadata, or None if the field is skipped.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if node.get("type") == "select":
|
|
36
|
+
result = {k: node[k] for k in METADATA_KEYS if node.get(k) is not None}
|
|
37
|
+
# result["options"] = node.get("options", [])
|
|
38
|
+
result["typehint"] = "string" # set enums to strings by default
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
result = {k: node[k] for k in METADATA_KEYS if node.get(k) is not None}
|
|
42
|
+
|
|
43
|
+
if "fields" in node:
|
|
44
|
+
value_unit = node["fields"].get("value_unit")
|
|
45
|
+
if value_unit and value_unit.get("type") == "select":
|
|
46
|
+
# result["units"] = value_unit["options"]
|
|
47
|
+
if value_unit.get("default"):
|
|
48
|
+
result["default_unit"] = value_unit["default"]
|
|
49
|
+
|
|
50
|
+
# hoist value/vector_value typehint directly onto parent, drop fields wrapper
|
|
51
|
+
data_field = node["fields"].get("value") or node["fields"].get("vector_value")
|
|
52
|
+
other_fields = {
|
|
53
|
+
k: v
|
|
54
|
+
for k, v in node["fields"].items()
|
|
55
|
+
if k not in ("value", "vector_value", "value_unit")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if data_field and not other_fields:
|
|
59
|
+
if "typehint" in data_field:
|
|
60
|
+
result["typehint"] = data_field["typehint"]
|
|
61
|
+
if (
|
|
62
|
+
node["fields"].get("vector_value")
|
|
63
|
+
and data_field.get("typehint") == "float"
|
|
64
|
+
):
|
|
65
|
+
result["typehint"] = "list"
|
|
66
|
+
else:
|
|
67
|
+
sub = {name: _summarise(child) for name, child in node["fields"].items()}
|
|
68
|
+
sub = {k: v for k, v in sub.items() if v is not None}
|
|
69
|
+
if sub:
|
|
70
|
+
result["fields"] = sub
|
|
71
|
+
|
|
72
|
+
return result or None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_summary(webform_json_path):
|
|
76
|
+
"""Extract a simplified field summary from a schema_webformfields.json file.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
webform_json_path: Path to the schema_webformfields.json file.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict: Nested summary keyed by section then field name.
|
|
83
|
+
"""
|
|
84
|
+
with open(webform_json_path) as f:
|
|
85
|
+
schema = json.load(f)
|
|
86
|
+
|
|
87
|
+
return {name: s for name, node in schema.items() if (s := _summarise(node))}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _flatten_rows(node, path):
|
|
91
|
+
"""Flatten nested summary nodes into CSV rows.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
node (dict): A node from the simplified summary produced by _summarise.
|
|
95
|
+
path (list[str]): Hierarchy path from section to current node.
|
|
96
|
+
Example leaf path: ["composition", "molecule_ID", "InChI"].
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
list[dict]: CSV-ready rows. Internal nodes recurse; leaf nodes emit one row.
|
|
100
|
+
|
|
101
|
+
Notes:
|
|
102
|
+
- subsection columns are intermediate levels only (path[1:-1]).
|
|
103
|
+
- field is always the final path element (path[-1]).
|
|
104
|
+
- This avoids duplicating leaf keys in subsection columns.
|
|
105
|
+
"""
|
|
106
|
+
rows = []
|
|
107
|
+
children = node.get("fields", {})
|
|
108
|
+
if children:
|
|
109
|
+
for child_name, child_node in children.items():
|
|
110
|
+
rows.extend(_flatten_rows(child_node, path + [child_name]))
|
|
111
|
+
return rows
|
|
112
|
+
|
|
113
|
+
# leaf field row
|
|
114
|
+
subsections = path[
|
|
115
|
+
1:-1
|
|
116
|
+
] # only intermediate levels, excludes section and field name
|
|
117
|
+
|
|
118
|
+
row = {
|
|
119
|
+
"section": path[0] if path else "",
|
|
120
|
+
"subsection_1": subsections[0] if len(subsections) > 0 else "",
|
|
121
|
+
# "subsection_2": subsections[1] if len(subsections) > 1 else "",
|
|
122
|
+
"field": path[-1] if path else "",
|
|
123
|
+
"field_name": node.get("label", ""),
|
|
124
|
+
"help_text": node.get("hint", ""),
|
|
125
|
+
"placeholder_text": node.get("placeholder", ""),
|
|
126
|
+
"default_unit": node.get("default_unit", ""),
|
|
127
|
+
"data_type": node.get("typehint", ""),
|
|
128
|
+
"example": node.get("example", ""),
|
|
129
|
+
}
|
|
130
|
+
return [row]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def write_summary_csv(summary, output_path):
|
|
134
|
+
"""Write the flattened schema summary to CSV.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
summary (dict): Nested summary from extract_summary().
|
|
138
|
+
output_path (str): Destination CSV path.
|
|
139
|
+
|
|
140
|
+
CSV columns:
|
|
141
|
+
section, subsection_1, field,
|
|
142
|
+
field_name, help_text, placeholder_text, default_unit, data_type
|
|
143
|
+
"""
|
|
144
|
+
rows = []
|
|
145
|
+
for section_name, section_node in summary.items():
|
|
146
|
+
rows.extend(_flatten_rows(section_node, [section_name]))
|
|
147
|
+
|
|
148
|
+
fieldnames = [
|
|
149
|
+
"section",
|
|
150
|
+
"subsection_1",
|
|
151
|
+
"field",
|
|
152
|
+
"field_name",
|
|
153
|
+
"help_text",
|
|
154
|
+
"placeholder_text",
|
|
155
|
+
"default_unit",
|
|
156
|
+
"data_type",
|
|
157
|
+
"example",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
with open(output_path, "w", newline="", encoding="utf-8") as f:
|
|
161
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
162
|
+
writer.writeheader()
|
|
163
|
+
writer.writerows(rows)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# =========================
|
|
167
|
+
# ENTRY POINT
|
|
168
|
+
# =========================
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_args():
|
|
172
|
+
"""Parses CLI arguments for summary extraction.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
argparse.Namespace: Parsed arguments containing:
|
|
176
|
+
webschema: Path to schema_webformfields.json.
|
|
177
|
+
yamloutput: Optional YAML output path. Default is schema_summary.yaml
|
|
178
|
+
"""
|
|
179
|
+
parser = argparse.ArgumentParser(
|
|
180
|
+
description="Extract engine mappings from a webform fields schema."
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument("webschema", help="Path to webform fields json schema file")
|
|
183
|
+
parser.add_argument("--yamloutput", "-o", help="Yaml output file path")
|
|
184
|
+
return parser.parse_args()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main():
|
|
188
|
+
"""Runs summary extraction and writes YAML and CSV outputs.
|
|
189
|
+
|
|
190
|
+
Side Effects:
|
|
191
|
+
Writes a YAML summary to --yamloutput (or schema_summary.yaml).
|
|
192
|
+
Writes a CSV summary next to the YAML output using a .csv suffix.
|
|
193
|
+
"""
|
|
194
|
+
args = parse_args()
|
|
195
|
+
|
|
196
|
+
# summary from the webform fields
|
|
197
|
+
summary = extract_summary(args.webschema)
|
|
198
|
+
|
|
199
|
+
# output as yaml
|
|
200
|
+
yaml_path = (
|
|
201
|
+
Path(args.yamloutput) if args.yamloutput else Path("schema_summary.yaml")
|
|
202
|
+
)
|
|
203
|
+
with open(yaml_path, "w") as f:
|
|
204
|
+
yaml.dump(
|
|
205
|
+
summary, f, default_flow_style=False, allow_unicode=True, sort_keys=True
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# output as json
|
|
209
|
+
json_path = yaml_path.with_suffix(".json")
|
|
210
|
+
with open(json_path, "w") as fp:
|
|
211
|
+
json.dump(summary, fp)
|
|
212
|
+
|
|
213
|
+
# flatten and output as csv
|
|
214
|
+
csv_path = yaml_path.with_suffix(".csv")
|
|
215
|
+
write_summary_csv(summary, str(csv_path))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|