Coverage for netbox_data_import/device_identity.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-09 20:50 +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 

8 

9from django.utils.text import slugify 

10 

11 

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

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

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

15 return " ".join(value.split()) 

16 

17 

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

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

20 normalized_make = normalize_mapping_text(make) 

21 normalized_model = normalize_mapping_text(model) 

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

23 

24 

25class DeviceTypeIdentityResolver: 

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

27 

28 def __init__(self, device_type_mappings, manufacturer_mappings): 

29 self.device_type_mappings = tuple(device_type_mappings) 

30 self.manufacturer_mappings = tuple(manufacturer_mappings) 

31 self._device_types_exact = {} 

32 self._device_types_by_make = {} 

33 for mapping in self.device_type_mappings: 

34 self._device_types_exact.setdefault((mapping.source_make, mapping.source_model), mapping) 

35 normalized_make = normalize_mapping_text(mapping.source_make).casefold() 

36 self._device_types_by_make.setdefault(normalized_make, []).append(mapping) 

37 self._manufacturers_exact = {} 

38 for mapping in self.manufacturer_mappings: 

39 normalized_make = normalize_mapping_text(mapping.source_make).casefold() 

40 self._manufacturers_exact.setdefault(normalized_make, mapping) 

41 

42 @classmethod 

43 def for_profile(cls, profile): 

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

45 return cls( 

46 profile.device_type_mappings.all(), 

47 profile.manufacturer_mappings.all(), 

48 ) 

49 

50 def resolve(self, make: str, model: str) -> tuple[str, str, bool]: 

51 """Return manufacturer slug, Device Type slug, and explicit status.""" 

52 normalized_make = normalize_mapping_text(make) 

53 normalized_model = normalize_mapping_text(model) 

54 mapping = self._device_types_exact.get((make, model)) 

55 if mapping is None: 

56 mapping = next( 

57 ( 

58 candidate 

59 for candidate in self._device_types_by_make.get(normalized_make.casefold(), ()) 

60 if normalize_mapping_text(candidate.source_model).casefold() == normalized_model.casefold() 

61 ), 

62 None, 

63 ) 

64 if mapping is not None: 

65 return mapping.netbox_manufacturer_slug, mapping.netbox_device_type_slug, True 

66 

67 manufacturer_mapping = self._manufacturers_exact.get(normalized_make.casefold()) 

68 default_manufacturer_slug, default_device_type_slug = default_identity_slugs(make, model) 

69 manufacturer_slug = ( 

70 manufacturer_mapping.netbox_manufacturer_slug 

71 if manufacturer_mapping is not None 

72 else default_manufacturer_slug 

73 ) 

74 return manufacturer_slug, default_device_type_slug, False 

75 

76 

77__all__ = ("DeviceTypeIdentityResolver", "default_identity_slugs", "normalize_mapping_text")