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
« 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."""
5from __future__ import annotations
7import re
8from typing import NamedTuple
10from django.utils.text import slugify
12from .identity import identity_text
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)
20def normalize_mapping_text(value: str) -> str:
21 r"""Normalize whitespace and decode JavaScript-style \uXXXX escapes."""
22 return " ".join(_decoded_escapes(value).split())
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))
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]
37DEVICE_TYPE_AMBIGUOUS = "device_type"
38MANUFACTURER_AMBIGUOUS = "manufacturer"
41class DeviceTypeIdentity(NamedTuple):
42 """The Device Type one source make and model name, or which mapping table names it two ways."""
44 manufacturer_slug: str
45 device_type_slug: str
46 explicit: bool
47 ambiguous: str = ""
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()}
58class DeviceTypeIdentityResolver:
59 """Resolve all profile Device Type identities from two batch-loaded indexes."""
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 )
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 )
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)
99__all__ = (
100 "DEVICE_TYPE_AMBIGUOUS",
101 "MANUFACTURER_AMBIGUOUS",
102 "DeviceTypeIdentity",
103 "DeviceTypeIdentityResolver",
104 "default_identity_slugs",
105 "normalize_mapping_text",
106)