Coverage for netbox_data_import/values.py: 100%
53 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"""Share source-value normalization between interpretation and target planning."""
5from __future__ import annotations
7from decimal import Decimal, InvalidOperation
8import math
10NUMERIC_TARGET_FIELDS: frozenset[str] = frozenset({"u_position", "u_height"})
12# A spreadsheet exports an empty cell as one of these words, and no caller wants the literal text.
13_NONE_LIKE = frozenset({"none", "nan", "null", "n/a", "#n/a"})
16def source_text(value) -> str:
17 """Return a source value as stripped text, with a null-like word read as empty."""
18 if value is None:
19 return ""
20 text = str(value).strip()
21 return "" if text.lower() in _NONE_LIKE else text
24def identity_text(value) -> str:
25 """Return the case-insensitive comparison form of a source identity."""
26 return " ".join(source_text(value).split()).casefold()
29def source_position(value, default=None):
30 """Return a finite rack position without losing half-unit precision."""
31 text = source_text(value)
32 if not text:
33 return default
34 try:
35 position = Decimal(text)
36 except (InvalidOperation, TypeError, ValueError):
37 return default
38 if not position.is_finite() or not math.isfinite(float(position)):
39 return default
40 if position == position.to_integral_value():
41 return int(position)
42 return float(position)
45def effective_device_name(row) -> str:
46 """Return the imported device name, including the asset-tag fallback."""
47 return source_text(row.get("device_name")) or source_text(row.get("asset_tag"))[:50]
50def has_below_rack_position(row) -> bool:
51 """Return whether a source row explicitly names a position below rack unit one."""
52 position = source_position(row.get("u_position"))
53 return position is not None and position < 1
56def normalize_for_compare(value) -> str:
57 """Normalize a value for field-diff comparison.
59 Whole-number floats (e.g. 35.0, "35.0") are normalized to their integer string form ("35") to
60 avoid false diffs caused by type differences between the source file and what NetBox returns.
61 """
62 if value is None:
63 return ""
64 try:
65 number = float(value)
66 if number == int(number):
67 return str(int(number))
68 return str(number)
69 except (TypeError, ValueError, OverflowError):
70 return str(value).strip()
73def comparison_key(target_field: str, value) -> str:
74 """Return the comparison key for *value* appropriate to the field's kind."""
75 if target_field in NUMERIC_TARGET_FIELDS:
76 return normalize_for_compare(value)
77 return "" if value is None else str(value).strip()
80def status_map() -> dict[str, str]:
81 """Return the source words that name a NetBox device status, taken from NetBox's own choices."""
82 from dcim.choices import DeviceStatusChoices
84 return {
85 "live": DeviceStatusChoices.STATUS_ACTIVE,
86 "production": DeviceStatusChoices.STATUS_ACTIVE,
87 "planned": DeviceStatusChoices.STATUS_PLANNED,
88 "staged": DeviceStatusChoices.STATUS_STAGED,
89 "inventory": DeviceStatusChoices.STATUS_INVENTORY,
90 "failed": DeviceStatusChoices.STATUS_FAILED,
91 "offline": DeviceStatusChoices.STATUS_OFFLINE,
92 "decommissioning": DeviceStatusChoices.STATUS_DECOMMISSIONING,
93 }
96def translation_maps():
97 """Return (side, airflow, status) source-word tables, with the choice values imported lazily."""
98 from dcim.choices import DeviceAirflowChoices, DeviceFaceChoices
100 side = {
101 "front": DeviceFaceChoices.FACE_FRONT,
102 "back": DeviceFaceChoices.FACE_REAR,
103 "rear": DeviceFaceChoices.FACE_REAR,
104 }
105 airflow = {
106 "front to back": DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR,
107 "back to front": DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT,
108 "passive": DeviceAirflowChoices.AIRFLOW_PASSIVE,
109 }
110 return side, airflow, status_map()
113__all__ = (
114 "NUMERIC_TARGET_FIELDS",
115 "comparison_key",
116 "effective_device_name",
117 "has_below_rack_position",
118 "identity_text",
119 "normalize_for_compare",
120 "source_position",
121 "source_text",
122 "status_map",
123 "translation_maps",
124)