Coverage for netbox_data_import/device_identity.py: 100.00%

50 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-30 09:47 +0000

1# SPDX-License-Identifier: Apache-2.0 

2# SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com> 

3"""Resolve the NetBox Device Type identity from source make and model names.""" 

4 

5from __future__ import annotations 

6 

7import re 

8from typing import NamedTuple 

9 

10from django.utils.text import slugify 

11 

12from .identity import identity_text 

13 

14 

15def _decoded_escapes(value: str) -> str: 

16 r"""Decode JavaScript-style \uXXXX escapes.""" 

17 return re.sub(r"\\u([0-9a-fA-F]{4})", lambda match: chr(int(match.group(1), 16)), value) 

18 

19 

20def normalize_mapping_text(value: str) -> str: 

21 r"""Normalize whitespace and decode JavaScript-style \uXXXX escapes.""" 

22 return " ".join(_decoded_escapes(value).split()) 

23 

24 

25def mapping_identity(value: str) -> str: 

26 """Return the name identity a make or model compares under, after its escapes are decoded.""" 

27 return identity_text(_decoded_escapes(value)) 

28 

29 

30def default_identity_slugs(make: str, model: str) -> tuple[str, str]: 

31 """Return the Manufacturer and Device Type slugs one source make and model derive.""" 

32 normalized_make = normalize_mapping_text(make) 

33 normalized_model = normalize_mapping_text(model) 

34 return slugify(normalized_make)[:50], slugify(f"{normalized_make}-{normalized_model}")[:50] 

35 

36 

37DEVICE_TYPE_AMBIGUOUS = "device_type" 

38MANUFACTURER_AMBIGUOUS = "manufacturer" 

39 

40 

41class DeviceTypeIdentity(NamedTuple): 

42 """The Device Type one source make and model name, or which mapping table names it two ways.""" 

43 

44 manufacturer_slug: str 

45 device_type_slug: str 

46 explicit: bool 

47 ambiguous: str = "" 

48 

49 

50def _targets_by_identity(mappings, identity, target) -> dict: 

51 """Return each identity's one target, or None when its mappings name more than one target.""" 

52 found: dict = {} 

53 for mapping in mappings: 

54 found.setdefault(identity(mapping), set()).add(target(mapping)) 

55 return {key: next(iter(targets)) if len(targets) == 1 else None for key, targets in found.items()} 

56 

57 

58class DeviceTypeIdentityResolver: 

59 """Resolve all profile Device Type identities from two batch-loaded indexes.""" 

60 

61 def __init__(self, device_type_mappings, manufacturer_mappings): 

62 self.device_type_mappings = tuple(device_type_mappings) 

63 self.manufacturer_mappings = tuple(manufacturer_mappings) 

64 self._device_types = _targets_by_identity( 

65 self.device_type_mappings, 

66 lambda mapping: (mapping_identity(mapping.source_make), mapping_identity(mapping.source_model)), 

67 lambda mapping: (mapping.netbox_manufacturer_slug, mapping.netbox_device_type_slug), 

68 ) 

69 self._manufacturers = _targets_by_identity( 

70 self.manufacturer_mappings, 

71 lambda mapping: mapping_identity(mapping.source_make), 

72 lambda mapping: mapping.netbox_manufacturer_slug, 

73 ) 

74 

75 @classmethod 

76 def for_profile(cls, profile): 

77 """Load both mapping tables once for one import run.""" 

78 return cls( 

79 profile.device_type_mappings.all(), 

80 profile.manufacturer_mappings.all(), 

81 ) 

82 

83 def resolve(self, make: str, model: str) -> DeviceTypeIdentity: 

84 """Return the Device Type slugs a make and model resolve to, or the mapping table that names two.""" 

85 key = (mapping_identity(make), mapping_identity(model)) 

86 if key in self._device_types: 

87 target = self._device_types[key] 

88 if target is None: 

89 return DeviceTypeIdentity("", "", explicit=False, ambiguous=DEVICE_TYPE_AMBIGUOUS) 

90 manufacturer_slug, device_type_slug = target 

91 return DeviceTypeIdentity(manufacturer_slug, device_type_slug, explicit=True) 

92 default_manufacturer_slug, default_device_type_slug = default_identity_slugs(make, model) 

93 manufacturer_slug = self._manufacturers.get(key[0], default_manufacturer_slug) 

94 if manufacturer_slug is None: 

95 return DeviceTypeIdentity("", "", explicit=False, ambiguous=MANUFACTURER_AMBIGUOUS) 

96 return DeviceTypeIdentity(manufacturer_slug, default_device_type_slug, explicit=False) 

97 

98 

99__all__ = ( 

100 "DEVICE_TYPE_AMBIGUOUS", 

101 "MANUFACTURER_AMBIGUOUS", 

102 "DeviceTypeIdentity", 

103 "DeviceTypeIdentityResolver", 

104 "default_identity_slugs", 

105 "normalize_mapping_text", 

106)