Files
basegame-vcko/python3-vckonline/lib/python3.8/site-packages/dict_tools/aggregation.py
2020-11-03 18:30:14 -08:00

241 lines
5.3 KiB
Python

# -*- 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