summaryrefslogtreecommitdiff
path: root/python/vyos/utils/dict.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos/utils/dict.py')
-rw-r--r--python/vyos/utils/dict.py307
1 files changed, 307 insertions, 0 deletions
diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py
new file mode 100644
index 000000000..9484eacdd
--- /dev/null
+++ b/python/vyos/utils/dict.py
@@ -0,0 +1,307 @@
+# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+def colon_separated_to_dict(data_string, uniquekeys=False):
+ """ Converts a string containing newline-separated entries
+ of colon-separated key-value pairs into a dict.
+
+ Such files are common in Linux /proc filesystem
+
+ Args:
+ data_string (str): data string
+ uniquekeys (bool): whether to insist that keys are unique or not
+
+ Returns: dict
+
+ Raises:
+ ValueError: if uniquekeys=True and the data string has
+ duplicate keys.
+
+ Note:
+ If uniquekeys=True, then dict entries are always strings,
+ otherwise they are always lists of strings.
+ """
+ import re
+ key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
+
+ data_raw = re.split('\n', data_string)
+
+ data = {}
+
+ for l in data_raw:
+ l = l.strip()
+ if l:
+ match = re.match(key_value_re, l)
+ if match and (len(match.groups()) == 2):
+ key = match.groups()[0].strip()
+ value = match.groups()[1].strip()
+ else:
+ raise ValueError(f"""Line "{l}" could not be parsed a colon-separated pair """, l)
+ if key in data.keys():
+ if uniquekeys:
+ raise ValueError("Data string has duplicate keys: {0}".format(key))
+ else:
+ data[key].append(value)
+ else:
+ if uniquekeys:
+ data[key] = value
+ else:
+ data[key] = [value]
+ else:
+ pass
+
+ return data
+
+def mangle_dict_keys(data, regex, replacement, abs_path=None, no_tag_node_value_mangle=False):
+ """ Mangles dict keys according to a regex and replacement character.
+ Some libraries like Jinja2 do not like certain characters in dict keys.
+ This function can be used for replacing all offending characters
+ with something acceptable.
+
+ Args:
+ data (dict): Original dict to mangle
+ regex, replacement (str): arguments to re.sub(regex, replacement, ...)
+ abs_path (list): if data is a config dict and no_tag_node_value_mangle is True
+ then abs_path should be the absolute config path to the first
+ keys of data, non-inclusive
+ no_tag_node_value_mangle (bool): do not mangle keys of tag node values
+
+ Returns: dict
+ """
+ import re
+ from vyos.xml_ref import is_tag_value
+
+ if abs_path is None:
+ abs_path = []
+
+ new_dict = type(data)()
+
+ for k in data.keys():
+ if no_tag_node_value_mangle and is_tag_value(abs_path + [k]):
+ new_key = k
+ else:
+ new_key = re.sub(regex, replacement, k)
+
+ value = data[k]
+
+ if isinstance(value, dict):
+ new_dict[new_key] = mangle_dict_keys(value, regex, replacement,
+ abs_path=abs_path + [k],
+ no_tag_node_value_mangle=no_tag_node_value_mangle)
+ else:
+ new_dict[new_key] = value
+
+ return new_dict
+
+def _get_sub_dict(d, lpath):
+ k = lpath[0]
+ if k not in d.keys():
+ return {}
+ c = {k: d[k]}
+ lpath = lpath[1:]
+ if not lpath:
+ return c
+ elif not isinstance(c[k], dict):
+ return {}
+ return _get_sub_dict(c[k], lpath)
+
+def get_sub_dict(source, lpath, get_first_key=False):
+ """ Returns the sub-dict of a nested dict, defined by path of keys.
+
+ Args:
+ source (dict): Source dict to extract from
+ lpath (list[str]): sequence of keys
+
+ Returns: source, if lpath is empty, else
+ {key : source[..]..[key]} for key the last element of lpath, if exists
+ {} otherwise
+ """
+ if not isinstance(source, dict):
+ raise TypeError("source must be of type dict")
+ if not isinstance(lpath, list):
+ raise TypeError("path must be of type list")
+ if not lpath:
+ return source
+
+ ret = _get_sub_dict(source, lpath)
+
+ if get_first_key and lpath and ret:
+ tmp = next(iter(ret.values()))
+ if not isinstance(tmp, dict):
+ raise TypeError("Data under node is not of type dict")
+ ret = tmp
+
+ return ret
+
+def dict_search(path, dict_object):
+ """ Traverse Python dictionary (dict_object) delimited by dot (.).
+ Return value of key if found, None otherwise.
+
+ This is faster implementation then jmespath.search('foo.bar', dict_object)"""
+ if not isinstance(dict_object, dict) or not path:
+ return None
+
+ parts = path.split('.')
+ inside = parts[:-1]
+ if not inside:
+ if path not in dict_object:
+ return None
+ return dict_object[path]
+ c = dict_object
+ for p in parts[:-1]:
+ c = c.get(p, {})
+ return c.get(parts[-1], None)
+
+def dict_search_args(dict_object, *path):
+ # Traverse dictionary using variable arguments
+ # Added due to above function not allowing for '.' in the key names
+ # Example: dict_search_args(some_dict, 'key', 'subkey', 'subsubkey', ...)
+ if not isinstance(dict_object, dict) or not path:
+ return None
+
+ for item in path:
+ if item not in dict_object:
+ return None
+ dict_object = dict_object[item]
+ return dict_object
+
+def dict_search_recursive(dict_object, key, path=[]):
+ """ Traverse a dictionary recurisvely and return the value of the key
+ we are looking for.
+
+ Thankfully copied from https://stackoverflow.com/a/19871956
+
+ Modified to yield optional path to found keys
+ """
+ if isinstance(dict_object, list):
+ for i in dict_object:
+ new_path = path + [i]
+ for x in dict_search_recursive(i, key, new_path):
+ yield x
+ elif isinstance(dict_object, dict):
+ if key in dict_object:
+ new_path = path + [key]
+ yield dict_object[key], new_path
+ for k, j in dict_object.items():
+ new_path = path + [k]
+ for x in dict_search_recursive(j, key, new_path):
+ yield x
+
+def dict_to_list(d, save_key_to=None):
+ """ Convert a dict to a list of dicts.
+
+ Optionally, save the original key of the dict inside
+ dicts stores in that list.
+ """
+ def save_key(i, k):
+ if isinstance(i, dict):
+ i[save_key_to] = k
+ return
+ elif isinstance(i, list):
+ for _i in i:
+ save_key(_i, k)
+ else:
+ raise ValueError(f"Cannot save the key: the item is {type(i)}, not a dict")
+
+ collect = []
+
+ for k,_ in d.items():
+ item = d[k]
+ if save_key_to is not None:
+ save_key(item, k)
+ if isinstance(item, list):
+ collect += item
+ else:
+ collect.append(item)
+
+ return collect
+
+def dict_to_paths(d: dict) -> list:
+ """ Generator to return list of paths from dict of list[str]|str
+ """
+ def func(d, path):
+ if isinstance(d, dict):
+ if not d:
+ yield path
+ for k, v in d.items():
+ for r in func(v, path + [k]):
+ yield r
+ elif isinstance(d, list):
+ for i in d:
+ for r in func(i, path):
+ yield r
+ elif isinstance(d, str):
+ yield path + [d]
+ else:
+ raise ValueError('object is not a dict of strings/list of strings')
+ for r in func(d, []):
+ yield r
+
+def check_mutually_exclusive_options(d, keys, required=False):
+ """ Checks if a dict has at most one or only one of
+ mutually exclusive keys.
+ """
+ present_keys = []
+
+ for k in d:
+ if k in keys:
+ present_keys.append(k)
+
+ # Un-mangle the keys to make them match CLI option syntax
+ from re import sub
+ orig_keys = list(map(lambda s: sub(r'_', '-', s), keys))
+ orig_present_keys = list(map(lambda s: sub(r'_', '-', s), present_keys))
+
+ if len(present_keys) > 1:
+ raise ValueError(f"Options {orig_keys} are mutually-exclusive but more than one of them is present: {orig_present_keys}")
+
+ if required and (len(present_keys) < 1):
+ raise ValueError(f"At least one of the following options is required: {orig_keys}")
+
+class FixedDict(dict):
+ """
+ FixedDict: A dictionnary not allowing new keys to be created after initialisation.
+
+ >>> f = FixedDict(**{'count':1})
+ >>> f['count'] = 2
+ >>> f['king'] = 3
+ File "...", line ..., in __setitem__
+ raise ConfigError(f'Option "{k}" has no defined default')
+ """
+
+ from vyos import ConfigError
+
+ def __init__(self, **options):
+ self._allowed = options.keys()
+ super().__init__(**options)
+
+ def __setitem__(self, k, v):
+ """
+ __setitem__ is a builtin which is called by python when setting dict values:
+ >>> d = dict()
+ >>> d['key'] = 'value'
+ >>> d
+ {'key': 'value'}
+
+ is syntaxic sugar for
+
+ >>> d = dict()
+ >>> d.__setitem__('key','value')
+ >>> d
+ {'key': 'value'}
+ """
+ if k not in self._allowed:
+ raise ConfigError(f'Option "{k}" has no defined default')
+ super().__setitem__(k, v)