Coverage for netbox_data_import/cable_policy.py: 100.00%

58 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"""Own the Cable policy question: the runtime choices, and which stored decision is in force.""" 

4 

5from django.core.exceptions import ValidationError 

6 

7 

8def _flatten_choice_groups(choices): 

9 """Return value and label pairs from flat or grouped NetBox choices.""" 

10 flattened = [] 

11 for value, label in choices: 

12 if isinstance(label, (tuple, list)): 

13 flattened.extend(label) 

14 else: 

15 flattened.append((value, label)) 

16 return tuple(flattened) 

17 

18 

19def cable_type_choices(): 

20 """Return the Cable Type values offered by the running NetBox instance.""" 

21 from dcim.choices import CableTypeChoices 

22 

23 return _flatten_choice_groups(CableTypeChoices.CHOICES) 

24 

25 

26def cable_profile_choices(): 

27 """Return the Cable Profile values offered by the running NetBox instance.""" 

28 from dcim.choices import CableProfileChoices 

29 

30 return _flatten_choice_groups(CableProfileChoices.CHOICES) 

31 

32 

33def cable_profile_accepts_one_termination_per_side(value) -> bool: 

34 """Return whether NetBox reports one connector on each side of the Cable Profile.""" 

35 from dcim.models import Cable 

36 

37 profile_class = Cable(profile=value).profile_class 

38 return profile_class is not None and len(profile_class.a_connectors) == 1 and len(profile_class.b_connectors) == 1 

39 

40 

41def compatible_cable_profile_choices(): 

42 """Return running Cable Profiles that permit one termination on each side.""" 

43 return tuple( 

44 (value, label) 

45 for value, label in cable_profile_choices() 

46 if cable_profile_accepts_one_termination_per_side(value) 

47 ) 

48 

49 

50def policy_choice_errors(cable_type, cable_profile): 

51 """Return runtime-choice and profile-cardinality errors by model field.""" 

52 errors = {} 

53 type_values = {value for value, _label in cable_type_choices()} 

54 profile_values = {value for value, _label in cable_profile_choices()} 

55 if cable_type is not None and cable_type not in type_values: 

56 errors["cable_type"] = ValidationError( 

57 "The selected Cable Type is no longer offered by this NetBox instance.", 

58 code="cable.policy_stale", 

59 ) 

60 if cable_profile is not None and cable_profile not in profile_values: 

61 errors["cable_profile"] = ValidationError( 

62 "The selected Cable Profile is no longer offered by this NetBox instance.", 

63 code="cable.policy_stale", 

64 ) 

65 elif cable_profile is not None and not cable_profile_accepts_one_termination_per_side(cable_profile): 

66 errors["cable_profile"] = ValidationError( 

67 "The selected Cable Profile does not permit one termination on each side.", 

68 code="cable.profile_incompatible", 

69 ) 

70 return errors 

71 

72 

73# An active optical assembly states no terminated medium, so its whole group decides nothing. 

74INDETERMINATE_MEDIA_TYPE = "aoc" 

75 

76 

77def _media_groups(): 

78 """Return the Cable Type values of each group the running instance offers, with its label.""" 

79 from dcim.choices import CableTypeChoices 

80 

81 return [ 

82 (label, [item_value for item_value, _item_label in group]) 

83 for label, group in CableTypeChoices.CHOICES 

84 if isinstance(group, (tuple, list)) 

85 ] 

86 

87 

88def cable_media_families(): 

89 """Return the family each Cable Type belongs to, keyed by a value rather than by a group label. 

90 

91 A group label is a lazy translation, so keying on it would make classification depend on the 

92 reader's language and would put translated text in a fingerprint (spec section 4.3). 

93 """ 

94 families = {} 

95 for _label, values in _media_groups(): 

96 families.update(dict.fromkeys(values, min(values))) 

97 return families 

98 

99 

100def cable_media_family_label(family) -> str: 

101 """Return the operator-facing name of one media family, in the reader's own language.""" 

102 for label, values in _media_groups(): 

103 if min(values) == family: 

104 return str(label) 

105 return family 

106 

107 

108def decisive_media_family(cable_type) -> str: 

109 """Return the family that decides one Cable Type's medium, or "" when nothing decides it. 

110 

111 A value the instance does not group, and one it groups as indeterminate, are incomplete coverage. 

112 They never read as agreement with another segment and never contradict one. 

113 """ 

114 families = cable_media_families() 

115 family = families.get(cable_type or "", "") 

116 return "" if family and family == families.get(INDETERMINATE_MEDIA_TYPE, "") else family 

117 

118 

119def cable_profile_splits_a_span(cable_profile) -> bool: 

120 """Return whether a Cable with this profile cannot be read as one medium along one path. 

121 

122 A profile with several connectors on a side, a breakout or a trunk, fans one Cable out into runs 

123 the path does not state, so its type says nothing about the medium of the segment beside it. 

124 """ 

125 return bool(cable_profile) and not cable_profile_accepts_one_termination_per_side(cable_profile) 

126 

127 

128def policy_in_force(override, mapping): 

129 """Return the stored decision that governs one segment: its override, else the CableClass row.""" 

130 return mapping if override is None else override 

131 

132 

133def cable_type_label(value) -> str: 

134 """Return the operator-facing name of one stored Cable Type value.""" 

135 return "None" if value is None else str(dict(cable_type_choices()).get(value, value)) 

136 

137 

138def cable_profile_label(value) -> str: 

139 """Return the operator-facing name of one stored Cable Profile value.""" 

140 return "None" if value is None else str(dict(cable_profile_choices()).get(value, value)) 

141 

142 

143__all__ = ( 

144 "INDETERMINATE_MEDIA_TYPE", 

145 "cable_media_families", 

146 "cable_media_family_label", 

147 "cable_profile_accepts_one_termination_per_side", 

148 "cable_profile_choices", 

149 "cable_profile_label", 

150 "cable_profile_splits_a_span", 

151 "cable_type_choices", 

152 "cable_type_label", 

153 "compatible_cable_profile_choices", 

154 "decisive_media_family", 

155 "policy_choice_errors", 

156 "policy_in_force", 

157)