1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Move array_to_range(), array_to_string(), deepmerge(), drange(), flatten_dict(), and shallow_compare_dict() to utilities.data

This commit is contained in:
Jeremy Stretch
2024-03-21 14:08:37 -04:00
parent 1d3efc90c0
commit 81ca455fef
13 changed files with 129 additions and 107 deletions

115
netbox/utilities/data.py Normal file
View File

@@ -0,0 +1,115 @@
import decimal
from itertools import count, groupby
__all__ = (
'array_to_ranges',
'array_to_string',
'deepmerge',
'drange',
'flatten_dict',
'shallow_compare_dict',
)
#
# Dictionary utilities
#
def deepmerge(original, new):
"""
Deep merge two dictionaries (new into original) and return a new dict
"""
merged = dict(original)
for key, val in new.items():
if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
merged[key] = deepmerge(original[key], val)
else:
merged[key] = val
return merged
def flatten_dict(d, prefix='', separator='.'):
"""
Flatten nested dictionaries into a single level by joining key names with a separator.
:param d: The dictionary to be flattened
:param prefix: Initial prefix (if any)
:param separator: The character to use when concatenating key names
"""
ret = {}
for k, v in d.items():
key = separator.join([prefix, k]) if prefix else k
if type(v) is dict:
ret.update(flatten_dict(v, prefix=key, separator=separator))
else:
ret[key] = v
return ret
def shallow_compare_dict(source_dict, destination_dict, exclude=tuple()):
"""
Return a new dictionary of the different keys. The values of `destination_dict` are returned. Only the equality of
the first layer of keys/values is checked. `exclude` is a list or tuple of keys to be ignored.
"""
difference = {}
for key, value in destination_dict.items():
if key in exclude:
continue
if source_dict.get(key) != value:
difference[key] = value
return difference
#
# Array utilities
#
def array_to_ranges(array):
"""
Convert an arbitrary array of integers to a list of consecutive values. Nonconsecutive values are returned as
single-item tuples. For example:
[0, 1, 2, 10, 14, 15, 16] => [(0, 2), (10,), (14, 16)]"
"""
group = (
list(x) for _, x in groupby(sorted(array), lambda x, c=count(): next(c) - x)
)
return [
(g[0], g[-1])[:len(g)] for g in group
]
def array_to_string(array):
"""
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
For example:
[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
"""
ret = []
ranges = array_to_ranges(array)
for value in ranges:
if len(value) == 1:
ret.append(str(value[0]))
else:
ret.append(f'{value[0]}-{value[1]}')
return ', '.join(ret)
#
# Range utilities
#
def drange(start, end, step=decimal.Decimal(1)):
"""
Decimal-compatible implementation of Python's range()
"""
start, end, step = decimal.Decimal(start), decimal.Decimal(end), decimal.Decimal(step)
if start < end:
while start < end:
yield start
start += step
else:
while start > end:
yield start
start += step

View File

@@ -1,7 +1,8 @@
from django.http import QueryDict
from django.test import TestCase
from utilities.utils import deepmerge, dict_to_filter_params, normalize_querydict
from utilities.data import deepmerge
from utilities.utils import dict_to_filter_params, normalize_querydict
class DictToFilterParamsTest(TestCase):

View File

@@ -1,5 +1,3 @@
import decimal
from itertools import count, groupby
from urllib.parse import urlencode
from django.db.models import Count, ManyToOneRel, OuterRef, Subquery
@@ -103,34 +101,6 @@ def normalize_querydict(querydict):
}
def deepmerge(original, new):
"""
Deep merge two dictionaries (new into original) and return a new dict
"""
merged = dict(original)
for key, val in new.items():
if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
merged[key] = deepmerge(original[key], val)
else:
merged[key] = val
return merged
def drange(start, end, step=decimal.Decimal(1)):
"""
Decimal-compatible implementation of Python's range()
"""
start, end, step = decimal.Decimal(start), decimal.Decimal(end), decimal.Decimal(step)
if start < end:
while start < end:
yield start
start += step
else:
while start > end:
yield start
start += step
def prepare_cloned_fields(instance):
"""
Generate a QueryDict comprising attributes from an object's clone() method.
@@ -154,70 +124,6 @@ def prepare_cloned_fields(instance):
return QueryDict(urlencode(params), mutable=True)
def shallow_compare_dict(source_dict, destination_dict, exclude=tuple()):
"""
Return a new dictionary of the different keys. The values of `destination_dict` are returned. Only the equality of
the first layer of keys/values is checked. `exclude` is a list or tuple of keys to be ignored.
"""
difference = {}
for key, value in destination_dict.items():
if key in exclude:
continue
if source_dict.get(key) != value:
difference[key] = value
return difference
def flatten_dict(d, prefix='', separator='.'):
"""
Flatten netsted dictionaries into a single level by joining key names with a separator.
:param d: The dictionary to be flattened
:param prefix: Initial prefix (if any)
:param separator: The character to use when concatenating key names
"""
ret = {}
for k, v in d.items():
key = separator.join([prefix, k]) if prefix else k
if type(v) is dict:
ret.update(flatten_dict(v, prefix=key, separator=separator))
else:
ret[key] = v
return ret
def array_to_ranges(array):
"""
Convert an arbitrary array of integers to a list of consecutive values. Nonconsecutive values are returned as
single-item tuples. For example:
[0, 1, 2, 10, 14, 15, 16] => [(0, 2), (10,), (14, 16)]"
"""
group = (
list(x) for _, x in groupby(sorted(array), lambda x, c=count(): next(c) - x)
)
return [
(g[0], g[-1])[:len(g)] for g in group
]
def array_to_string(array):
"""
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
For example:
[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
"""
ret = []
ranges = array_to_ranges(array)
for value in ranges:
if len(value) == 1:
ret.append(str(value[0]))
else:
ret.append(f'{value[0]}-{value[1]}')
return ', '.join(ret)
def content_type_name(ct, include_app=True):
"""
Return a human-friendly ContentType name (e.g. "DCIM > Site").