1 # Copyright (c) 2022 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Module for converting in-memory data into raw JSON output.
16 CSIT and VPP PAPI are using custom data types
17 that are not directly serializable into JSON.
19 Thus, before writing the raw outpt onto disk,
20 the data is recursively converted to equivalent serializable types,
21 in extreme cases replaced by string representation.
23 Validation is outside the scope of this module,
24 as it should use the JSON data read from disk.
30 from collections.abc import Iterable, Mapping, Set
31 from enum import IntFlag
34 def _pre_serialize_recursive(data):
35 """Recursively sort and convert to a more serializable form.
37 VPP PAPI code can give data with its own MACAddres type,
38 or various other enum and flag types.
39 The default json.JSONEncoder method raises TypeError on that.
40 First point of this function is to apply str() or repr()
41 to leaf values that need it.
43 Also, PAPI responses are namedtuples, which confuses
44 the json.JSONEncoder method (so it does not recurse).
45 Dictization (see PapiExecutor) helps somewhat, but it turns namedtuple
46 into a UserDict, which also confuses json.JSONEncoder.
47 Therefore, we recursively convert any Mapping into an ordinary dict.
49 We also convert iterables to list (sorted if the iterable was a set),
50 and prevent numbers from getting converted to strings.
52 As we are doing such low level operations,
53 we also convert mapping keys to strings
54 and sort the mapping items by keys alphabetically,
55 except "data" field moved to the end.
57 :param data: Object to make serializable, dictized when applicable.
59 :returns: Serializable equivalent of the argument.
61 :raises ValueError: If the argument does not support string conversion.
63 # Recursion ends at scalar values, first handle irregular ones.
64 if isinstance(data, IntFlag):
66 if isinstance(data, bytes):
68 # The regular ones are good to go.
69 if isinstance(data, (str, int, float, bool)):
71 # Recurse over, convert and sort mappings.
72 if isinstance(data, Mapping):
73 # Convert and sort alphabetically.
75 str(key): _pre_serialize_recursive(data[key])
76 for key in sorted(data.keys())
78 # If exists, move "data" field to the end.
80 data_value = ret.pop(u"data")
81 ret[u"data"] = data_value
82 # If exists, move "type" field at the start.
84 type_value = ret.pop(u"type")
86 ret = dict(type=type_value)
89 # Recurse over and convert iterables.
90 if isinstance(data, Iterable):
91 list_data = [_pre_serialize_recursive(item) for item in data]
92 # Additionally, sets are exported as sorted.
93 if isinstance(data, Set):
94 list_data = sorted(list_data)
96 # Unknown structure, attempt str().
100 def _pre_serialize_root(data):
101 """Recursively convert to a more serializable form, tweak order.
103 See _pre_serialize_recursive for most of changes this does.
105 The logic here (outside the recursive function) only affects
106 field ordering in the root mapping,
107 to make it more human friendly.
108 We are moving "version" to the top,
109 followed by start time and end time.
110 and various long fields (such as "log") to the bottom.
112 Some edits are done in-place, do not trust the argument value after calling.
114 :param data: Root data to make serializable, dictized when applicable.
116 :returns: Order-tweaked version of the argument.
118 :raises KeyError: If the data does not contain required fields.
119 :raises TypeError: If the argument is not a dict.
120 :raises ValueError: If the argument does not support string conversion.
122 if not isinstance(data, dict):
123 raise RuntimeError(f"Root data object needs to be a dict: {data!r}")
124 data = _pre_serialize_recursive(data)
125 log = data.pop(u"log")
126 new_data = dict(version=data.pop(u"version"))
127 new_data[u"start_time"] = data.pop(u"start_time")
128 new_data[u"end_time"] = data.pop(u"end_time")
129 new_data.update(data)
130 new_data[u"log"] = log
134 def write_raw_output(raw_file_path, raw_data):
135 """Prepare data for serialization and dump into a file.
137 Ancestor directories are created if needed.
139 :param to_raw_path: Local filesystem path, including the file name.
140 :type to_raw_path: str
142 raw_data = _pre_serialize_root(raw_data)
143 os.makedirs(os.path.dirname(raw_file_path), exist_ok=True)
144 with open(raw_file_path, u"wt", encoding="utf-8") as file_out:
145 json.dump(raw_data, file_out, indent=1)