1""" 2 This library makes it possible to introspect dataset and aggregate nodes 3 when it is instructed. 4 5 .. note:: 6 7 The following examples with be expressed in YAML for convenience's sake: 8 9 - !aggr-scalar will refer to Scalar python function 10 - !aggr-map will refer to Map python object 11 - !aggr-seq will refer for Sequence python object 12 13 14 How to instructs merging 15 ------------------------ 16 17 This yaml document has duplicate keys: 18 19 .. code-block:: yaml 20 21 foo: !aggr-scalar first 22 foo: !aggr-scalar second 23 bar: !aggr-map {first: foo} 24 bar: !aggr-map {second: bar} 25 baz: !aggr-scalar 42 26 27 but tagged values instruct Salt that overlapping values they can be merged 28 together: 29 30 .. code-block:: yaml 31 32 foo: !aggr-seq [first, second] 33 bar: !aggr-map {first: foo, second: bar} 34 baz: !aggr-seq [42] 35 36 37 Default merge strategy is keep untouched 38 ---------------------------------------- 39 40 For example, this yaml document still has duplicate keys, but does not 41 instruct aggregation: 42 43 .. code-block:: yaml 44 45 foo: first 46 foo: second 47 bar: {first: foo} 48 bar: {second: bar} 49 baz: 42 50 51 So the late found values prevail: 52 53 .. code-block:: yaml 54 55 foo: second 56 bar: {second: bar} 57 baz: 42 58 59 60 Limitations 61 ----------- 62 63 Aggregation is permitted between tagged objects that share the same type. 64 If not, the default merge strategy prevails. 65 66 For example, these examples: 67 68 .. code-block:: yaml 69 70 foo: {first: value} 71 foo: !aggr-map {second: value} 72 73 bar: !aggr-map {first: value} 74 bar: 42 75 76 baz: !aggr-seq [42] 77 baz: [fail] 78 79 qux: 42 80 qux: !aggr-scalar fail 81 82 are interpreted like this: 83 84 .. code-block:: yaml 85 86 foo: !aggr-map{second: value} 87 88 bar: 42 89 90 baz: [fail] 91 92 qux: !aggr-seq [fail] 93 94 95 Introspection 96 ------------- 97 98 TODO: write this part 99 100""" 101import collections 102import copy 103import logging 104from typing import Iterable 105from typing import Tuple 106 107__all__ = ["aggregate", "Aggregate", "Map", "Scalar", "Sequence"] 108 109log = logging.getLogger(__name__) 110 111 112class Aggregate: 113 """ 114 Aggregation base. 115 """ 116 117 118class Map(collections.OrderedDict, Aggregate): 119 """ 120 Map aggregation. 121 """ 122 123 124class Sequence(list, Aggregate): 125 """ 126 Sequence aggregation. 127 """ 128 129 130def Scalar(obj): 131 """ 132 Shortcut for Sequence creation 133 134 >>> Scalar('foo') == Sequence(['foo']) 135 True 136 """ 137 return Sequence([obj]) 138 139 140def levelise(level: bool or int or Iterable) -> Tuple[bool, bool or int]: 141 """ 142 Describe which levels are allowed to do deep merging. 143 144 level can be: 145 146 True 147 all levels are True 148 149 False 150 all levels are False 151 152 an int 153 only the first levels are True, the others are False 154 155 a sequence 156 it describes which levels are True, it can be: 157 158 * a list of bool and int values 159 * a string of 0 and 1 characters 160 161 """ 162 163 if not level: # False, 0, [] ... 164 return False, False 165 if level is True: 166 return True, True 167 if isinstance(level, int): 168 return True, level - 1 169 try: # a sequence 170 deep, subs = int(level[0]), level[1:] 171 return bool(deep), subs 172 except Exception as error: # pylint: disable=broad-except 173 log.warning(error) 174 raise 175 176 177def mark( 178 obj: object, map_class: object = Map, sequence_class: object = Sequence 179) -> object: 180 """ 181 Convert obj into an Aggregate instance 182 """ 183 if isinstance(obj, Aggregate): 184 return obj 185 if isinstance(obj, dict): 186 return map_class(obj) 187 if isinstance(obj, (list, tuple, set)): 188 return sequence_class(obj) 189 else: 190 return sequence_class([obj]) 191 192 193def aggregate( 194 obj_a, 195 obj_b, 196 level: bool or int = False, 197 map_class: object = Map, 198 sequence_class: object = Sequence, 199): 200 """ 201 Merge obj_b into obj_a. 202 203 >>> aggregate('first', 'second', True) == ['first', 'second'] 204 True 205 """ 206 deep, subdeep = levelise(level) 207 208 if deep: 209 obj_a = mark(obj_a, map_class=map_class, sequence_class=sequence_class) 210 obj_b = mark(obj_b, map_class=map_class, sequence_class=sequence_class) 211 212 if isinstance(obj_a, dict) and isinstance(obj_b, dict): 213 if isinstance(obj_a, Aggregate) and isinstance(obj_b, Aggregate): 214 # deep merging is more or less a.update(obj_b) 215 response = copy.copy(obj_a) 216 else: 217 # introspection on obj_b keys only 218 response = copy.copy(obj_b) 219 220 for key, value in obj_b.items(): 221 if key in obj_a: 222 value = aggregate(obj_a[key], value, subdeep, map_class, sequence_class) 223 response[key] = value 224 return response 225 226 if isinstance(obj_a, Sequence) and isinstance(obj_b, Sequence): 227 response = obj_a.__class__(obj_a[:]) 228 for value in obj_b: 229 if value not in obj_a: 230 response.append(value) 231 return response 232 233 response = copy.copy(obj_b) 234 235 if isinstance(obj_a, Aggregate) or isinstance(obj_b, Aggregate): 236 log.info("only one value marked as aggregate. keep `obj_b` value") 237 return response 238 239 log.debug("no value marked as aggregate. keep `obj_b` value") 240 return response 241