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:
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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',
|
||||||
|
@ -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 *
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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__ = (
|
||||||
|
@ -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
115
netbox/utilities/data.py
Normal 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
|
@ -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):
|
||||||
|
@ -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").
|
||||||
|
Reference in New Issue
Block a user