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
« 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."""
5from __future__ import annotations
7import re
9from django.utils.text import slugify
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())
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]
25class DeviceTypeIdentityResolver:
26 """Resolve all profile Device Type identities from two batch-loaded indexes."""
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)
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 )
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
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
77__all__ = ("DeviceTypeIdentityResolver", "default_identity_slugs", "normalize_mapping_text")