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
« 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."""
5from django.core.exceptions import ValidationError
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)
19def cable_type_choices():
20 """Return the Cable Type values offered by the running NetBox instance."""
21 from dcim.choices import CableTypeChoices
23 return _flatten_choice_groups(CableTypeChoices.CHOICES)
26def cable_profile_choices():
27 """Return the Cable Profile values offered by the running NetBox instance."""
28 from dcim.choices import CableProfileChoices
30 return _flatten_choice_groups(CableProfileChoices.CHOICES)
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
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
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 )
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
73# An active optical assembly states no terminated medium, so its whole group decides nothing.
74INDETERMINATE_MEDIA_TYPE = "aoc"
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
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 ]
88def cable_media_families():
89 """Return the family each Cable Type belongs to, keyed by a value rather than by a group label.
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
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
108def decisive_media_family(cable_type) -> str:
109 """Return the family that decides one Cable Type's medium, or "" when nothing decides it.
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
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.
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)
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
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))
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))
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)