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

View File

@ -18,8 +18,8 @@ from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, PrimaryModel from netbox.models import OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.conversion import to_grams from utilities.conversion import to_grams
from utilities.data import array_to_string, drange
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange
from .device_components import PowerPort from .device_components import PowerPort
from .devices import Device, Module from .devices import Device, Module
from .mixins import WeightMixin from .mixins import WeightMixin

View File

@ -14,8 +14,8 @@ from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from netbox.config import get_config from netbox.config import get_config
from utilities.data import array_to_ranges
from utilities.html import foreground_color from utilities.html import foreground_color
from utilities.utils import array_to_ranges
from dcim.constants import RACK_ELEVATION_BORDER_WIDTH from dcim.constants import RACK_ELEVATION_BORDER_WIDTH

View File

@ -7,7 +7,7 @@ from dcim.choices import *
from dcim.models import * from dcim.models import *
from extras.models import CustomField from extras.models import CustomField
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import drange from utilities.data import drange
class LocationTestCase(TestCase): class LocationTestCase(TestCase):

View File

@ -9,11 +9,11 @@ from jinja2.sandbox import SandboxedEnvironment
from extras.querysets import ConfigContextQuerySet from extras.querysets import ConfigContextQuerySet
from netbox.config import get_config from netbox.config import get_config
from netbox.registry import registry
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from netbox.registry import registry
from utilities.data import deepmerge
from utilities.jinja2 import DataFileLoader from utilities.jinja2 import DataFileLoader
from utilities.utils import deepmerge
__all__ = ( __all__ = (
'ConfigContext', 'ConfigContext',

View File

@ -18,12 +18,13 @@ from extras.dashboard.utils import get_widget_class
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.data import shallow_compare_dict
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.request import copy_safe_request from utilities.request import copy_safe_request
from utilities.rqworker import get_workers_for_queue from utilities.rqworker import get_workers_for_queue
from utilities.templatetags.builtins.filters import render_markdown from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import count_related, normalize_querydict, shallow_compare_dict from utilities.utils import count_related, normalize_querydict
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *

View File

@ -8,8 +8,7 @@ from django.utils.translation import gettext_lazy as _
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from netbox.models import PrimaryModel from netbox.models import PrimaryModel
from utilities.utils import array_to_string from utilities.data import array_to_string
__all__ = ( __all__ = (
'Service', 'Service',

View File

@ -14,8 +14,8 @@ from rest_framework.viewsets import ViewSet
from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from users import filtersets from users import filtersets
from users.models import Group, ObjectPermission, Token, UserConfig from users.models import Group, ObjectPermission, Token, UserConfig
from utilities.data import deepmerge
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import deepmerge
from . import serializers from . import serializers

View File

@ -12,11 +12,11 @@ from ipam.validators import prefix_validator
from netbox.preferences import PREFERENCES from netbox.preferences import PREFERENCES
from users.constants import * from users.constants import *
from users.models import * from users.models import *
from utilities.data import flatten_dict
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DateTimePicker from utilities.forms.widgets import DateTimePicker
from utilities.permissions import qs_filter_from_constraints from utilities.permissions import qs_filter_from_constraints
from utilities.utils import flatten_dict
__all__ = ( __all__ = (
'UserTokenForm', 'UserTokenForm',

View File

@ -25,8 +25,8 @@ from netaddr import IPNetwork
from core.models import ObjectType from core.models import ObjectType
from ipam.fields import IPNetworkField from ipam.fields import IPNetworkField
from netbox.config import get_config from netbox.config import get_config
from utilities.data import flatten_dict
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import flatten_dict
from .constants import * from .constants import *
__all__ = ( __all__ = (

View File

@ -3,8 +3,8 @@ from django.urls import reverse
from core.models import ObjectType from core.models import ObjectType
from users.models import Group, ObjectPermission, Token from users.models import Group, ObjectPermission, Token
from utilities.data import deepmerge
from utilities.testing import APIViewTestCases, APITestCase, create_test_user from utilities.testing import APIViewTestCases, APITestCase, create_test_user
from utilities.utils import deepmerge
User = get_user_model() User = get_user_model()

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.http import QueryDict
from django.test import TestCase 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): class DictToFilterParamsTest(TestCase):

View File

@ -1,5 +1,3 @@
import decimal
from itertools import count, groupby
from urllib.parse import urlencode from urllib.parse import urlencode
from django.db.models import Count, ManyToOneRel, OuterRef, Subquery 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): def prepare_cloned_fields(instance):
""" """
Generate a QueryDict comprising attributes from an object's clone() method. 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) 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): def content_type_name(ct, include_app=True):
""" """
Return a human-friendly ContentType name (e.g. "DCIM > Site"). Return a human-friendly ContentType name (e.g. "DCIM > Site").