Coverage for netbox_data_import/values.py: 100.00%

51 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"""Share source-value normalization between interpretation and target planning.""" 

4 

5from __future__ import annotations 

6 

7from decimal import Decimal, InvalidOperation 

8import math 

9 

10NUMERIC_TARGET_FIELDS: frozenset[str] = frozenset({"u_position", "u_height"}) 

11 

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"}) 

14 

15 

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 

22 

23 

24def source_position(value, default=None): 

25 """Return a finite rack position without losing half-unit precision.""" 

26 text = source_text(value) 

27 if not text: 

28 return default 

29 try: 

30 position = Decimal(text) 

31 except (InvalidOperation, TypeError, ValueError): 

32 return default 

33 if not position.is_finite() or not math.isfinite(float(position)): 

34 return default 

35 if position == position.to_integral_value(): 

36 return int(position) 

37 return float(position) 

38 

39 

40def effective_device_name(row) -> str: 

41 """Return the imported device name, including the asset-tag fallback.""" 

42 return source_text(row.get("device_name")) or source_text(row.get("asset_tag"))[:50] 

43 

44 

45def has_below_rack_position(row) -> bool: 

46 """Return whether a source row explicitly names a position below rack unit one.""" 

47 position = source_position(row.get("u_position")) 

48 return position is not None and position < 1 

49 

50 

51def normalize_for_compare(value) -> str: 

52 """Normalize a value for field-diff comparison. 

53 

54 Whole-number floats (e.g. 35.0, "35.0") are normalized to their integer string form ("35") to 

55 avoid false diffs caused by type differences between the source file and what NetBox returns. 

56 """ 

57 if value is None: 

58 return "" 

59 try: 

60 number = float(value) 

61 if number == int(number): 

62 return str(int(number)) 

63 return str(number) 

64 except (TypeError, ValueError, OverflowError): 

65 return str(value).strip() 

66 

67 

68def comparison_key(target_field: str, value) -> str: 

69 """Return the comparison key for *value* appropriate to the field's kind.""" 

70 if target_field in NUMERIC_TARGET_FIELDS: 

71 return normalize_for_compare(value) 

72 return "" if value is None else str(value).strip() 

73 

74 

75def status_map() -> dict[str, str]: 

76 """Return the source words that name a NetBox device status, taken from NetBox's own choices.""" 

77 from dcim.choices import DeviceStatusChoices 

78 

79 return { 

80 "live": DeviceStatusChoices.STATUS_ACTIVE, 

81 "production": DeviceStatusChoices.STATUS_ACTIVE, 

82 "planned": DeviceStatusChoices.STATUS_PLANNED, 

83 "staged": DeviceStatusChoices.STATUS_STAGED, 

84 "inventory": DeviceStatusChoices.STATUS_INVENTORY, 

85 "failed": DeviceStatusChoices.STATUS_FAILED, 

86 "offline": DeviceStatusChoices.STATUS_OFFLINE, 

87 "decommissioning": DeviceStatusChoices.STATUS_DECOMMISSIONING, 

88 } 

89 

90 

91def translation_maps(): 

92 """Return (side, airflow, status) source-word tables, with the choice values imported lazily.""" 

93 from dcim.choices import DeviceAirflowChoices, DeviceFaceChoices 

94 

95 side = { 

96 "front": DeviceFaceChoices.FACE_FRONT, 

97 "back": DeviceFaceChoices.FACE_REAR, 

98 "rear": DeviceFaceChoices.FACE_REAR, 

99 } 

100 airflow = { 

101 "front to back": DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, 

102 "back to front": DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, 

103 "passive": DeviceAirflowChoices.AIRFLOW_PASSIVE, 

104 } 

105 return side, airflow, status_map() 

106 

107 

108__all__ = ( 

109 "NUMERIC_TARGET_FIELDS", 

110 "comparison_key", 

111 "effective_device_name", 

112 "has_below_rack_position", 

113 "normalize_for_compare", 

114 "source_position", 

115 "source_text", 

116 "status_map", 

117 "translation_maps", 

118)