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