first commit
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This library makes it possible to introspect dataset and aggregate nodes
|
||||
when it is instructed.
|
||||
|
||||
.. note::
|
||||
|
||||
The following examples with be expressed in YAML for convenience's sake:
|
||||
|
||||
- !aggr-scalar will refer to Scalar python function
|
||||
- !aggr-map will refer to Map python object
|
||||
- !aggr-seq will refer for Sequence python object
|
||||
|
||||
|
||||
How to instructs merging
|
||||
------------------------
|
||||
|
||||
This yaml document has duplicate keys:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo: !aggr-scalar first
|
||||
foo: !aggr-scalar second
|
||||
bar: !aggr-map {first: foo}
|
||||
bar: !aggr-map {second: bar}
|
||||
baz: !aggr-scalar 42
|
||||
|
||||
but tagged values instruct Salt that overlapping values they can be merged
|
||||
together:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo: !aggr-seq [first, second]
|
||||
bar: !aggr-map {first: foo, second: bar}
|
||||
baz: !aggr-seq [42]
|
||||
|
||||
|
||||
Default merge strategy is keep untouched
|
||||
----------------------------------------
|
||||
|
||||
For example, this yaml document still has duplicate keys, but does not
|
||||
instruct aggregation:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo: first
|
||||
foo: second
|
||||
bar: {first: foo}
|
||||
bar: {second: bar}
|
||||
baz: 42
|
||||
|
||||
So the late found values prevail:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo: second
|
||||
bar: {second: bar}
|
||||
baz: 42
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
Aggregation is permitted between tagged objects that share the same type.
|
||||
If not, the default merge strategy prevails.
|
||||
|
||||
For example, these examples:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo: {first: value}
|
||||
foo: !aggr-map {second: value}
|
||||
|
||||
bar: !aggr-map {first: value}
|
||||
bar: 42
|
||||
|
||||
baz: !aggr-seq [42]
|
||||
baz: [fail]
|
||||
|
||||
qux: 42
|
||||
qux: !aggr-scalar fail
|
||||
|
||||
are interpreted like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo: !aggr-map{second: value}
|
||||
|
||||
bar: 42
|
||||
|
||||
baz: [fail]
|
||||
|
||||
qux: !aggr-seq [fail]
|
||||
|
||||
|
||||
Introspection
|
||||
-------------
|
||||
|
||||
TODO: write this part
|
||||
|
||||
"""
|
||||
from typing import Iterable, Tuple
|
||||
import copy
|
||||
import logging
|
||||
import collections
|
||||
|
||||
__all__ = ["aggregate", "Aggregate", "Map", "Scalar", "Sequence"]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Aggregate(object):
|
||||
"""
|
||||
Aggregation base.
|
||||
"""
|
||||
|
||||
|
||||
class Map(collections.OrderedDict, Aggregate):
|
||||
"""
|
||||
Map aggregation.
|
||||
"""
|
||||
|
||||
|
||||
class Sequence(list, Aggregate):
|
||||
"""
|
||||
Sequence aggregation.
|
||||
"""
|
||||
|
||||
|
||||
def Scalar(obj):
|
||||
"""
|
||||
Shortcut for Sequence creation
|
||||
|
||||
>>> Scalar('foo') == Sequence(['foo'])
|
||||
True
|
||||
"""
|
||||
return Sequence([obj])
|
||||
|
||||
|
||||
def levelise(level: bool or int or Iterable) -> Tuple[bool, bool or int]:
|
||||
"""
|
||||
Describe which levels are allowed to do deep merging.
|
||||
|
||||
level can be:
|
||||
|
||||
True
|
||||
all levels are True
|
||||
|
||||
False
|
||||
all levels are False
|
||||
|
||||
an int
|
||||
only the first levels are True, the others are False
|
||||
|
||||
a sequence
|
||||
it describes which levels are True, it can be:
|
||||
|
||||
* a list of bool and int values
|
||||
* a string of 0 and 1 characters
|
||||
|
||||
"""
|
||||
|
||||
if not level: # False, 0, [] ...
|
||||
return False, False
|
||||
if level is True:
|
||||
return True, True
|
||||
if isinstance(level, int):
|
||||
return True, level - 1
|
||||
try: # a sequence
|
||||
deep, subs = int(level[0]), level[1:]
|
||||
return bool(deep), subs
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
log.warning(error)
|
||||
raise
|
||||
|
||||
|
||||
def mark(
|
||||
obj: object, map_class: object = Map, sequence_class: object = Sequence
|
||||
) -> object:
|
||||
"""
|
||||
Convert obj into an Aggregate instance
|
||||
"""
|
||||
if isinstance(obj, Aggregate):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return map_class(obj)
|
||||
if isinstance(obj, (list, tuple, set)):
|
||||
return sequence_class(obj)
|
||||
else:
|
||||
return sequence_class([obj])
|
||||
|
||||
|
||||
def aggregate(
|
||||
obj_a,
|
||||
obj_b,
|
||||
level: bool or int = False,
|
||||
map_class: object = Map,
|
||||
sequence_class: object = Sequence,
|
||||
):
|
||||
"""
|
||||
Merge obj_b into obj_a.
|
||||
|
||||
>>> aggregate('first', 'second', True) == ['first', 'second']
|
||||
True
|
||||
"""
|
||||
deep, subdeep = levelise(level)
|
||||
|
||||
if deep:
|
||||
obj_a = mark(obj_a, map_class=map_class, sequence_class=sequence_class)
|
||||
obj_b = mark(obj_b, map_class=map_class, sequence_class=sequence_class)
|
||||
|
||||
if isinstance(obj_a, dict) and isinstance(obj_b, dict):
|
||||
if isinstance(obj_a, Aggregate) and isinstance(obj_b, Aggregate):
|
||||
# deep merging is more or less a.update(obj_b)
|
||||
response = copy.copy(obj_a)
|
||||
else:
|
||||
# introspection on obj_b keys only
|
||||
response = copy.copy(obj_b)
|
||||
|
||||
for key, value in obj_b.items():
|
||||
if key in obj_a:
|
||||
value = aggregate(obj_a[key], value, subdeep, map_class, sequence_class)
|
||||
response[key] = value
|
||||
return response
|
||||
|
||||
if isinstance(obj_a, Sequence) and isinstance(obj_b, Sequence):
|
||||
response = obj_a.__class__(obj_a[:])
|
||||
for value in obj_b:
|
||||
if value not in obj_a:
|
||||
response.append(value)
|
||||
return response
|
||||
|
||||
response = copy.copy(obj_b)
|
||||
|
||||
if isinstance(obj_a, Aggregate) or isinstance(obj_b, Aggregate):
|
||||
log.info("only one value marked as aggregate. keep `obj_b` value")
|
||||
return response
|
||||
|
||||
log.debug("no value marked as aggregate. keep `obj_b` value")
|
||||
return response
|
||||
Reference in New Issue
Block a user