UTI: Export results
[csit.git] / resources / libraries / python / model / mem2raw.py
1 # Copyright (c) 2021 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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Module for converting in-memory data into raw JSON output.
15
16 CSIT and VPP PAPI are using custom data types
17 that are not directly serializable into JSON.
18
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.
22
23 Validation is outside the scope of this module,
24 as it should use the JSON data read from disk.
25 """
26
27 import json
28 import os
29
30 from collections.abc import Iterable, Mapping, Set
31 from enum import IntFlag
32
33
34 def _pre_serialize_recursive(data):
35     """Recursively sort and convert to a more serializable form.
36
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.
42
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.
48
49     We also convert iterables to list (sorted if the iterable was a set),
50     and prevent numbers from getting converted to strings.
51
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.
56
57     :param data: Object to make serializable, dictized when applicable.
58     :type data: object
59     :returns: Serializable equivalent of the argument.
60     :rtype: object
61     :raises ValueError: If the argument does not support string conversion.
62     """
63     # Recursion ends at scalar values, first handle irregular ones.
64     if isinstance(data, IntFlag):
65         return repr(data)
66     if isinstance(data, bytes):
67         return data.hex()
68     # The regular ones are good to go.
69     if isinstance(data, (str, int, float, bool)):
70         return data
71     # Recurse over, convert and sort mappings.
72     if isinstance(data, Mapping):
73         # Convert and sort alphabetically.
74         ret = {
75             str(key): _pre_serialize_recursive(data[key])
76             for key in sorted(data.keys())
77         }
78         # If exists, move "data" field to the end.
79         if u"data" in ret:
80             data_value = ret.pop(u"data")
81             ret[u"data"] = data_value
82         # If exists, move "type" field at the start.
83         if u"type" in ret:
84             type_value = ret.pop(u"type")
85             ret_old = ret
86             ret = dict(type=type_value)
87             ret.update(ret_old)
88         return ret
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)
95         return list_data
96     # Unknown structure, attempt str().
97     return str(data)
98
99
100 def _pre_serialize_root(data):
101     """Recursively convert to a more serializable form, tweak order.
102
103     See _pre_serialize_recursive for most of changes this does.
104
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.
111
112     Some edits are done in-place, do not trust the argument value after calling.
113
114     :param data: Root data to make serializable, dictized when applicable.
115     :type data: dict
116     :returns: Order-tweaked version of the argument.
117     :rtype: dict
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.
121     """
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
131     return new_data
132
133
134 def write_raw_output(raw_file_path, raw_data):
135     """Prepare data for serialization and dump into a file.
136
137     Ancestor directories are created if needed.
138
139     :param to_raw_path: Local filesystem path, including the file name.
140     :type to_raw_path: str
141     """
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"xt", encoding="utf-8") as file_out:
145         json.dump(raw_data, file_out, indent=1)