Coverage for netbox_data_import/source_trace.py: 100.00%
71 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"""Adapter-neutral source values for traced physical paths."""
5from __future__ import annotations
7from dataclasses import dataclass
8from typing import TYPE_CHECKING
10from .field_keys import PORT_CLASS_CLAIMED_KINDS, REAR_PORT_CLASSES
11from .identity import identity_text
13if TYPE_CHECKING:
14 from .adapters import SourceDiagnostic
17IdentityKey = tuple[str, str, str, str]
20@dataclass(frozen=True)
21class TerminationReference:
22 """Name one source termination and retain its corroboration values."""
24 device: str
25 cards: str
26 port: str
27 port_class: str
28 u_position: str = ""
29 rack: str = ""
30 location: str = ""
32 @property
33 def identity_key(self) -> IdentityKey:
34 """Return the normalized device, cards, port, and claimed-kind identity."""
35 return (
36 identity_text(self.device),
37 identity_text(self.cards),
38 identity_text(self.port),
39 PORT_CLASS_CLAIMED_KINDS.get(self.port_class, identity_text(self.port_class)),
40 )
43@dataclass(frozen=True)
44class EndpointSummary:
45 """Retain the source From and To statements in their original direction."""
47 from_termination: TerminationReference
48 to_termination: TerminationReference
49 from_text: str
50 to_text: str
53@dataclass(frozen=True)
54class SegmentEvidence:
55 """Describe one source-claimed cable between two Termination References."""
57 left: TerminationReference
58 cable_class: str
59 right: TerminationReference
62@dataclass(frozen=True)
63class PassThroughClaim:
64 """Describe the source-claimed continuation through one device."""
66 device: str
67 cards: str
68 entry_port: str
69 exit_port: str
72@dataclass(frozen=True, order=True)
73class PortClassLabel:
74 """Name one source PortClass label outside the fixed vocabulary, and the PortClass a map gave it."""
76 label: str
77 port_class: str = ""
80@dataclass(frozen=True)
81class TraceProvenance:
82 """Locate one Source Trace occurrence in its source document."""
84 workbook_fingerprint: str
85 sheet: str
86 block_ordinal: int
87 row_start: int
88 row_end: int
89 export_timestamp: str
90 from_text: str
91 to_text: str
92 direction: str
95@dataclass(frozen=True)
96class SourceTrace:
97 """Carry canonical Segment Evidence for one path or an Endpoint Summary fallback."""
99 endpoint_summary: EndpointSummary
100 segments: tuple[SegmentEvidence, ...]
101 pass_through_claims: tuple[PassThroughClaim, ...]
102 corroboration: tuple[TerminationReference, ...]
103 identity: str
104 content_fingerprint: str
105 provenance: tuple[TraceProvenance, ...]
106 errors: tuple[SourceDiagnostic, ...] = ()
107 port_class_labels: tuple[PortClassLabel, ...] = ()
109 def __post_init__(self) -> None:
110 """Reject a trace that states no source position, which every consumer reads."""
111 if not self.provenance:
112 raise ValueError("A Source Trace must carry at least one TraceProvenance record.")
114 @property
115 def valid(self) -> bool:
116 """Return whether source validation found no errors on this trace."""
117 return not self.errors
119 @property
120 def ends_at_rear_port(self) -> bool:
121 """Return whether the original To termination is a rear port."""
122 return self.endpoint_summary.to_termination.port_class in REAR_PORT_CLASSES
125__all__ = (
126 "EndpointSummary",
127 "IdentityKey",
128 "PassThroughClaim",
129 "PortClassLabel",
130 "SegmentEvidence",
131 "SourceTrace",
132 "TerminationReference",
133 "TraceProvenance",
134)