ipaddons 0.2.3__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipaddons
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Additional tools for the ipaddress standard library module
5
5
  Project-URL: docs, https://ipaddress-tools.readthedocs.io/stable/
6
6
  Project-URL: homepage, https://ipaddress-tools.readthedocs.io/stable/
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.3'
32
- __version_tuple__ = version_tuple = (0, 2, 3)
31
+ __version__ = version = '0.3.0'
32
+ __version_tuple__ = version_tuple = (0, 3, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -17,6 +17,10 @@ if TYPE_CHECKING:
17
17
  NetworkRange: TypeAlias = tuple[int, int]
18
18
 
19
19
 
20
+ class IPAllocationError(Exception):
21
+ pass
22
+
23
+
20
24
  def ip_allocation(supernet: UnSpecNet, used_networks: UnspecNetSeq | None = None) -> IPv4Allocation | IPv6Allocation:
21
25
  """Create an IP allocation.
22
26
 
@@ -42,7 +46,11 @@ def ip_allocation(supernet: UnSpecNet, used_networks: UnspecNetSeq | None = None
42
46
  raise ValueError(msg)
43
47
 
44
48
 
45
- def overlap(a: NetworkRange, b: NetworkRange) -> bool:
49
+ def _ceildiv(a: int, b: int) -> int:
50
+ return -(a // -b)
51
+
52
+
53
+ def range_overlap(a: NetworkRange, b: NetworkRange) -> bool:
46
54
  """Network Range Overlap Check.
47
55
 
48
56
  :param a: a network range
@@ -52,7 +60,7 @@ def overlap(a: NetworkRange, b: NetworkRange) -> bool:
52
60
  return max(a[0], b[0]) < min(a[1], b[1])
53
61
 
54
62
 
55
- def overlaps(a: NetworkRange, b: list[NetworkRange]) -> NetworkRange | None:
63
+ def range_overlaps(a: NetworkRange, b: list[NetworkRange]) -> NetworkRange | None:
56
64
  """Overlap between a network range and a list of other network ranges.
57
65
 
58
66
  :param a: Network range to check against.
@@ -60,7 +68,7 @@ def overlaps(a: NetworkRange, b: list[NetworkRange]) -> NetworkRange | None:
60
68
  :return: The first range from b that overlaps with a.
61
69
  """
62
70
  for x in b:
63
- if overlap(a, x):
71
+ if range_overlap(a, x):
64
72
  return x
65
73
  return None
66
74
 
@@ -75,6 +83,29 @@ def netrange(network: IPNetwork) -> NetworkRange:
75
83
  return int(network[0]), int(network[-1])
76
84
 
77
85
 
86
+ def free_ranges(superrange: NetworkRange, used_ranges: list[NetworkRange]) -> Generator[NetworkRange, None, None]:
87
+ """Get free ranges from a supernet with provided used ranges.
88
+
89
+ :param superrange: Covering range from where free subranges should be returned.
90
+ :param used_ranges: List of used network ranges.
91
+ :return: An interator for free ranges.
92
+ """
93
+ used_ranges = sorted(used_ranges)
94
+ for range in used_ranges:
95
+ if superrange[0] >= superrange[1]:
96
+ # Last range either ends at or shoots over the supernet range
97
+ return
98
+ if superrange[0] == range[0]:
99
+ # used range is at the beginning, shrink superrange
100
+ superrange = range[1] + 1, superrange[1]
101
+ continue
102
+ free_range = superrange[0], range[0] - 1
103
+ superrange = range[1] + 1, superrange[1]
104
+ yield free_range
105
+ # The rest of the superrange is free when there are no more used ranges left
106
+ yield superrange
107
+
108
+
78
109
  def merge_ranges(ranges: list[NetworkRange]) -> list[NetworkRange]:
79
110
  """Merge network ranges that overlap.
80
111
 
@@ -89,7 +120,7 @@ def merge_ranges(ranges: list[NetworkRange]) -> list[NetworkRange]:
89
120
  base = sorted_ranges.pop()
90
121
  while len(sorted_ranges):
91
122
  range = sorted_ranges.pop()
92
- if overlap(base, range):
123
+ if range_overlap(base, range):
93
124
  base = min(base[0], range[0]), max(base[1], range[1])
94
125
  else:
95
126
  merged_ranges.append(base)
@@ -98,16 +129,28 @@ def merge_ranges(ranges: list[NetworkRange]) -> list[NetworkRange]:
98
129
  return merged_ranges
99
130
 
100
131
 
101
- def net_size_iterator(start: IPNetwork, cidr: int) -> Generator[NetworkRange, None, None]:
132
+ def net_size_iterator(
133
+ supernet: IPNetwork, cidr: int, used_ranges: list[NetworkRange] | None = None
134
+ ) -> Generator[NetworkRange, None, None]:
102
135
  """Iterate over an ipaddr network object and return subnet network ranges of the specified cidr size.
103
136
 
104
- :param start: An ipaddr network object.
137
+ :param supernet: An ipaddr network object.
105
138
  :param cidr: CIDR size of the subnets.
139
+ :param used_ranges: A list of used network ranges.
106
140
  :return: An interator over all subnets in the network in the specified CIDR size.
107
141
  """
108
- cidr_size = 2 ** (start.max_prefixlen - cidr)
109
- for s in range(int(start[0]), int(start[-1]), cidr_size):
110
- yield s, s + cidr_size - 1
142
+ cidr_size = 2 ** (supernet.max_prefixlen - cidr)
143
+ supernet_range = int(supernet[0]), int(supernet[-1])
144
+ if used_ranges is None:
145
+ used_ranges = []
146
+ for free_range in free_ranges(supernet_range, used_ranges):
147
+ free_range_boundary = (
148
+ # Large IPv6 integers are tricky...
149
+ _ceildiv(free_range[0], cidr_size) * cidr_size,
150
+ free_range[1] // cidr_size * cidr_size - 1,
151
+ )
152
+ for s in range(free_range_boundary[0], free_range_boundary[1] + cidr_size, cidr_size):
153
+ yield s, s + cidr_size - 1
111
154
 
112
155
 
113
156
  class _BaseIPAllocation(ABC, Generic[_N]):
@@ -195,9 +238,10 @@ class _BaseIPAllocation(ABC, Generic[_N]):
195
238
  :param prefixlen: Size of the free subnets returned in CIDR notation.
196
239
  :return: An iterator for free subnets of :py:attr:`prefixlen` size.
197
240
  """
198
- for n in net_size_iterator(self.supernet, prefixlen):
199
- if overlaps(n, self._used_network_ranges):
200
- continue
241
+ for n in net_size_iterator(self.supernet, prefixlen, self._used_network_ranges):
242
+ if range_overlaps(n, self._used_network_ranges):
243
+ msg = "This should never happen!"
244
+ raise IPAllocationError(msg)
201
245
  yield self._network_class((n[0], prefixlen))
202
246
 
203
247
 
@@ -98,12 +98,45 @@ def test_merge(subnets):
98
98
  covering_prefix = subnets[0]
99
99
  assert tools.merge_ranges(subnets) == [covering_prefix]
100
100
 
101
+
101
102
  @pytest.mark.parametrize(
102
103
  "subnets",
103
104
  mergeable_subnets,
104
105
  )
105
106
  def test_merge_one(subnets):
106
- assert tools.merge_ranges(subnets[:1]) == subnets[:1]
107
+ assert subnets[:1] == tools.merge_ranges(subnets[:1])
108
+
107
109
 
108
110
  def test_merge_empty():
109
111
  assert tools.merge_ranges([]) == []
112
+
113
+
114
+ @pytest.mark.parametrize(
115
+ ("supernet", "used_nets", "cidr", "first_free"),
116
+ [
117
+ ("2001:db8::/32", ["2001:db8::/48", "2001:db8:1::/120"], 64, "2001:db8:1:1::/64"),
118
+ ("10.0.0.0/8", ["10.0.0.0/16", "10.1.0.0/24"], 16, "10.2.0.0/16"),
119
+ ],
120
+ )
121
+ def test_netsize_iterator(supernet, used_nets, cidr, first_free):
122
+ supernet = ipaddress.ip_network(supernet)
123
+ used_nets = [(int(ipaddress.ip_network(n)[0]), int(ipaddress.ip_network(n)[-1])) for n in used_nets]
124
+ i = tools.net_size_iterator(supernet, cidr, used_nets)
125
+ first_ip, last_ip = next(i)
126
+ net = next(ipaddress.summarize_address_range(ipaddress.ip_address(first_ip), ipaddress.ip_address(last_ip)))
127
+ assert first_free == str(net)
128
+
129
+
130
+ @pytest.mark.parametrize(
131
+ ("supernet", "used_nets", "cidr", "last_free"),
132
+ [
133
+ ("10.0.0.0/24", ["10.0.0.0/29", "10.0.0.96/30"], 30, "10.0.0.252/30"),
134
+ ],
135
+ )
136
+ def test_netsize_iterator_last(supernet, used_nets, cidr, last_free):
137
+ supernet = ipaddress.ip_network(supernet)
138
+ used_nets = [(int(ipaddress.ip_network(n)[0]), int(ipaddress.ip_network(n)[-1])) for n in used_nets]
139
+ i = tools.net_size_iterator(supernet, cidr, used_nets)
140
+ first_ip, last_ip = list(i)[-1]
141
+ net = next(ipaddress.summarize_address_range(ipaddress.ip_address(first_ip), ipaddress.ip_address(last_ip)))
142
+ assert last_free == str(net)
File without changes
File without changes
File without changes
File without changes
File without changes