66import typing
77from urllib .request import getproxies
88
9+ from abc import abstractmethod
10+
911from ._types import PrimitiveData
1012
1113if typing .TYPE_CHECKING : # pragma: no cover
@@ -123,24 +125,35 @@ def peek_filelike_length(stream: typing.Any) -> int | None:
123125 return length
124126
125127
126- class URLPattern :
128+ class Pattern (typing .Protocol ):
129+ @abstractmethod
130+ def matches (self , other : URL ) -> bool :
131+ pass
132+
133+ @property
134+ @abstractmethod
135+ def priority (self ) -> tuple [int , int , int ]:
136+ pass
137+
138+
139+ class WildcardURLPattern (Pattern ):
127140 """
128141 A utility class currently used for making lookups against proxy keys...
129142
130143 # Wildcard matching...
131- >>> pattern = URLPattern ("all://")
144+ >>> pattern = WildcardURLPattern ("all://")
132145 >>> pattern.matches(httpx.URL("http://example.com"))
133146 True
134147
135148 # Witch scheme matching...
136- >>> pattern = URLPattern ("https://")
149+ >>> pattern = WildcardURLPattern ("https://")
137150 >>> pattern.matches(httpx.URL("https://example.com"))
138151 True
139152 >>> pattern.matches(httpx.URL("http://example.com"))
140153 False
141154
142155 # With domain matching...
143- >>> pattern = URLPattern ("https://example.com")
156+ >>> pattern = WildcardURLPattern ("https://example.com")
144157 >>> pattern.matches(httpx.URL("https://example.com"))
145158 True
146159 >>> pattern.matches(httpx.URL("http://example.com"))
@@ -149,7 +162,7 @@ class URLPattern:
149162 False
150163
151164 # Wildcard scheme, with domain matching...
152- >>> pattern = URLPattern ("all://example.com")
165+ >>> pattern = WildcardURLPattern ("all://example.com")
153166 >>> pattern.matches(httpx.URL("https://example.com"))
154167 True
155168 >>> pattern.matches(httpx.URL("http://example.com"))
@@ -158,7 +171,7 @@ class URLPattern:
158171 False
159172
160173 # With port matching...
161- >>> pattern = URLPattern ("https://example.com:1234")
174+ >>> pattern = WildcardURLPattern ("https://example.com:1234")
162175 >>> pattern.matches(httpx.URL("https://example.com:1234"))
163176 True
164177 >>> pattern.matches(httpx.URL("https://example.com"))
@@ -229,7 +242,51 @@ def __lt__(self, other: URLPattern) -> bool:
229242 return self .priority < other .priority
230243
231244 def __eq__ (self , other : typing .Any ) -> bool :
232- return isinstance (other , URLPattern ) and self .pattern == other .pattern
245+ return isinstance (other , WildcardURLPattern ) and self .pattern == other .pattern
246+
247+
248+ class IPNetPattern (Pattern ):
249+ def __init__ (self , ip_net : str ) -> None :
250+ try :
251+ addr , range = ip_net .split ('/' , 1 )
252+ if addr [0 ] == '[' and addr [- 1 ] == ']' :
253+ addr = addr [1 :- 1 ]
254+ ip_net = f'{ addr } /{ range } '
255+ except ValueError :
256+ pass # not a range
257+ self .net = ipaddress .ip_network (ip_net )
258+
259+ def matches (self , other : URL ):
260+ try :
261+ return ipaddress .ip_address (other .host ) in self .net
262+ except ValueError :
263+ return False
264+
265+ @property
266+ def priority (self ) -> tuple [int , int , int ]:
267+ return - 1 , 0 , 0 # higher priority than URLPatterns
268+
269+ def __hash__ (self ) -> int :
270+ return hash (self .net )
271+
272+ def __lt__ (self , other : URLPattern ) -> bool :
273+ return self .priority < other .priority
274+
275+ def __eq__ (self , other : typing .Any ) -> bool :
276+ return isinstance (other , IPNetPattern ) and self .net == other .net
277+
278+
279+ URLPattern = IPNetPattern | WildcardURLPattern
280+
281+
282+ def build_url_pattern (pattern : str ) -> URLPattern :
283+ try :
284+ proto , rest = pattern .split ('://' , 1 )
285+ if proto == 'all' and '/' in rest :
286+ return IPNetPattern (rest )
287+ except ValueError : # covers .split() and IPNetPattern
288+ pass
289+ return WildcardURLPattern (pattern )
233290
234291
235292def is_ipv4_hostname (hostname : str ) -> bool :
@@ -245,4 +302,4 @@ def is_ipv6_hostname(hostname: str) -> bool:
245302 ipaddress .IPv6Address (hostname .split ("/" )[0 ])
246303 except Exception :
247304 return False
248- return True
305+ return True
0 commit comments