python-ipware 2.0.4__py3-none-any.whl → 2.0.5__py3-none-any.whl

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 +1 @@
1
- __version__ = "2.0.4"
1
+ __version__ = "2.0.5"
@@ -1,7 +1,7 @@
1
1
  import ipaddress
2
2
  import logging
3
3
 
4
- from typing import Any, Dict, List, Optional, Tuple, Union
4
+ from typing import Dict, List, Optional, Tuple, Union
5
5
 
6
6
  IpAddressType = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
7
7
  OptionalIpAddressType = Optional[IpAddressType]
@@ -11,125 +11,130 @@ logger = logging.getLogger(__name__)
11
11
 
12
12
  class IpWareMeta:
13
13
  """
14
- A class that handles meta data frm an HTTP request.
14
+ A class to manage metadata extracted from HTTP request headers, primarily for identifying
15
+ the client's IP address behind load balancers and proxies.
15
16
  """
16
17
 
17
18
  def __init__(
18
- self,
19
- precedence: Optional[Tuple[str, ...]] = None,
20
- leftmost: bool = True,
19
+ self, precedence: Optional[Tuple[str, ...]] = None, leftmost: bool = True
21
20
  ) -> None:
22
- self.precedence = precedence or (
23
- "X_FORWARDED_FOR", # Load balancers or proxies such as AWS ELB (default client is `left-most` [`<client>, <proxy1>, <proxy2>`])
24
- "HTTP_X_FORWARDED_FOR", # Similar to X_FORWARDED_TO
25
- "HTTP_CLIENT_IP", # Standard headers used by providers such as Amazon EC2, Heroku etc.
26
- "HTTP_X_REAL_IP", # Standard headers used by providers such as Amazon EC2, Heroku etc.
27
- "HTTP_X_FORWARDED", # Squid and others
28
- "HTTP_X_CLUSTER_CLIENT_IP", # Rackspace LB and Riverbed Stingray
29
- "HTTP_FORWARDED_FOR", # RFC 7239
30
- "HTTP_FORWARDED", # RFC 7239
31
- "HTTP_CF_CONNECTING_IP", # CloudFlare
32
- "X-CLIENT-IP", # Microsoft Azure
33
- "X-REAL-IP", # NGINX
34
- "X-CLUSTER-CLIENT-IP", # Rackspace Cloud Load Balancers
35
- "X_FORWARDED", # Squid
36
- "FORWARDED_FOR", # RFC 7239
37
- "CF-CONNECTING-IP", # CloudFlare
38
- "TRUE-CLIENT-IP", # CloudFlare Enterprise,
39
- "FASTLY-CLIENT-IP", # Firebase, Fastly
40
- "FORWARDED", # RFC 7239
41
- "CLIENT-IP", # Akamai and Cloudflare: True-Client-IP and Fastly: Fastly-Client-IP
42
- "REMOTE_ADDR", # Default
43
- )
21
+ """
22
+ Initializes the metadata handler with a precedence list of HTTP headers and the position of the client IP.
23
+
24
+ :param precedence: A tuple of header names that define the order to check for a valid IP.
25
+ If not provided, a default list of commonly used headers is applied.
26
+ :param leftmost: A boolean that indicates whether to use the left-most IP in the header
27
+ when multiple IPs are listed (typically the case with proxies).
28
+ """
29
+ # Default precedence list of headers if none is provided
30
+ if precedence is None:
31
+ precedence = (
32
+ "X_FORWARDED_FOR", # Common header for proxies, load balancers, like AWS ELB. Default to the left-most IP.
33
+ "HTTP_X_FORWARDED_FOR", # Alternative header similar to `X_FORWARDED_FOR`.
34
+ "HTTP_CLIENT_IP", # Header used by some providers like Amazon EC2, Heroku.
35
+ "HTTP_X_REAL_IP", # Header used by some providers like Amazon EC2, Heroku.
36
+ "HTTP_X_FORWARDED", # Used by Squid and similar software.
37
+ "HTTP_X_CLUSTER_CLIENT_IP", # Used by Rackspace LB, Riverbed Stingray.
38
+ "HTTP_FORWARDED_FOR", # Standard header defined by RFC 7239.
39
+ "HTTP_FORWARDED", # Standard header defined by RFC 7239.
40
+ "HTTP_CF_CONNECTING_IP", # Used by CloudFlare.
41
+ "X-CLIENT-IP", # Used by Microsoft Azure.
42
+ "X-REAL-IP", # Commonly used by NGINX.
43
+ "X-CLUSTER-CLIENT-IP", # Used by Rackspace Cloud Load Balancers.
44
+ "X_FORWARDED", # Used by Squid.
45
+ "FORWARDED_FOR", # Standard header defined by RFC 7239.
46
+ "CF-CONNECTING-IP", # Used by CloudFlare.
47
+ "TRUE-CLIENT-IP", # Header for CloudFlare Enterprise.
48
+ "FASTLY-CLIENT-IP", # Used by Fastly, Firebase.
49
+ "FORWARDED", # Standard header defined by RFC 7239.
50
+ "CLIENT-IP", # Used by Akamai, Cloudflare's True-Client-IP, and Fastly's Fastly-Client-IP.
51
+ "REMOTE_ADDR", # The default IP address header (direct connection).
52
+ )
53
+
54
+ self.precedence = precedence
44
55
  self.leftmost = leftmost
45
56
 
46
57
 
47
58
  class IpWareIpAddress:
48
59
  """
49
- A class that handles IP address data from an HTTP request.
60
+ A class for handling and parsing IP address data from HTTP requests.
50
61
  """
51
62
 
52
- def extract_ipv4_only(self, ip_address: str) -> str:
53
- """
54
- Given an IPv4 address or address:port, it extracts the IP address
55
- @param ip_str: IP address or address:port
56
- @return: IP address
63
+ def extract_ipv4(self, ip_address: str) -> str:
57
64
  """
65
+ Extracts the IPv4 address from a given string that may include a port number.
58
66
 
59
- if ip_address:
60
- # handle ipv4 addresses with port
61
- if ":" in ip_address:
62
- ip_address = ip_address.split(":")[0]
63
- return ip_address.strip()
64
-
65
- return ip_address.strip()
67
+ Args:
68
+ ip_address (str): An IPv4 address possibly including a port (e.g., "192.168.1.1:8080").
66
69
 
67
- return ""
68
-
69
- def extract_ipv6_only(self, ip_address: str) -> str:
70
+ Returns:
71
+ str: The IPv4 address without the port. Returns an empty string if input is None.
70
72
  """
71
- Given an IPv6 address or address:port, it extracts the IP address
72
- @param ip_str: IP address or address:port
73
- @return: IP address
74
- """
75
-
76
73
  if ip_address:
77
- # handle ipv6 addresses with port
78
- if "]:" in ip_address:
79
- ip_address = ip_address.split("]:")[0]
80
- ip_address = ip_address.replace("[", "")
81
- return ip_address.strip()
74
+ return ip_address.split(":")[0].strip()
75
+ return ""
82
76
 
83
- return ip_address.strip()
77
+ def extract_ipv6(self, ip_address: str) -> str:
78
+ """
79
+ Extracts the IPv6 address from a given string that may include a port number.
84
80
 
85
- return ""
81
+ Args:
82
+ ip_address (str): An IPv6 address possibly including a port (e.g., "[2001:db8::1]:8080").
86
83
 
87
- def get_ip_object(
88
- self,
89
- ip_str: str,
90
- ) -> OptionalIpAddressType:
84
+ Returns:
85
+ str: The IPv6 address without brackets or port. Returns an empty string if input is None.
91
86
  """
92
- Given an IP address or address:port, it parses the IP address
93
- @param ip_str: IP address or address:port
94
- @return: IP address of type IPv4Address or IPv6Address
87
+ if ip_address and "]:" in ip_address:
88
+ return ip_address.split("]:")[0].replace("[", "").strip()
89
+ return ip_address.strip() if ip_address else ""
90
+
91
+ def parse_ip_address(self, ip_str: str):
95
92
  """
93
+ Parses the given IP address string to an IPv4Address or IPv6Address object.
96
94
 
97
- ip: OptionalIpAddressType = None
98
- if ip_str:
95
+ Args:
96
+ ip_str (str): An IP address possibly including a port.
97
+
98
+ Returns:
99
+ ipaddress.IPv4Address|ipaddress.IPv6Address|None: The parsed IP object or None if the address is invalid.
100
+ """
101
+ try:
102
+ # First, try to parse as IPv6.
103
+ ipv6 = self.extract_ipv6(ip_str)
104
+ ip = ipaddress.IPv6Address(ipv6)
105
+ return ip.ipv4_mapped or ip
106
+ except ipaddress.AddressValueError:
99
107
  try:
100
- # try to parse as IPv6 address with optional port
101
- ipv6 = self.extract_ipv6_only(ip_str)
102
- ip = ipaddress.IPv6Address(ipv6)
103
- ip = ip.ipv4_mapped or ip
108
+ # If IPv6 parsing fails, try to parse as IPv4.
109
+ ipv4 = self.extract_ipv4(ip_str)
110
+ return ipaddress.IPv4Address(ipv4)
104
111
  except ipaddress.AddressValueError:
105
- try:
106
- # try to parse as IPv4 address with optional port
107
- ipv4 = self.extract_ipv4_only(ip_str)
108
- ip = ipaddress.IPv4Address(ipv4)
109
- except ipaddress.AddressValueError:
110
- # not a valid IP address, return None
111
- logger.info("Invalid ip address. {0}".format(ip_str))
112
- ip = None
113
- return ip
112
+ # Log error if IP is invalid
113
+ # print(f"Invalid IP address: {ip_str}")
114
+ return None
114
115
 
115
116
  def get_ips_from_string(
116
117
  self,
117
118
  ip_str: str,
118
- ) -> Optional[List[IpAddressType]]:
119
- """
120
- Given a comma separated list of IP addresses or address:port, it parses the IP addresses
121
- @param ip_str: comma separated list of IP addresses or address:port
122
- @return: list of IP addresses of type IPv4Address or IPv6Address
119
+ strict: bool = False,
120
+ ) -> Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]]:
123
121
  """
124
- ip_list: List[IpAddressType] = []
122
+ Parses a comma-separated list of IP addresses, each possibly including a port.
125
123
 
124
+ Args:
125
+ ip_str (str): A comma-separated list of IP addresses or address:port entries.
126
+ strict (bool): True to bail out on first invalid ip, False to skip invalid ip
127
+ Returns:
128
+ List[ipaddress.IPv4Address|ipaddress.IPv6Address]|None: A list of IP address objects or None if any IP is invalid.
129
+ """
130
+ ip_list = []
126
131
  for ip_address in ip_str.split(","):
127
- ip = self.get_ip_object(ip_address.strip())
132
+ ip = self.parse_ip_address(ip_address.strip())
128
133
  if ip:
129
134
  ip_list.append(ip)
130
135
  else:
131
- # we have at least one invalid IP address, return empty list, instead
132
- return None
136
+ if strict:
137
+ return None
133
138
 
134
139
  if not self.leftmost:
135
140
  ip_list.reverse()
@@ -139,90 +144,101 @@ class IpWareIpAddress:
139
144
 
140
145
  class IpWareProxy:
141
146
  """
142
- A class that handles proxy data from an HTTP request.
147
+ A class to handle proxy data from an HTTP request, including validating
148
+ proxy counts and trusted proxy lists.
143
149
  """
144
150
 
145
151
  def __init__(
146
- self,
147
- proxy_count: Optional[int] = None,
148
- proxy_list: Optional[List[str]] = None,
152
+ self, proxy_count: Optional[int] = None, proxy_list: Optional[List[str]] = None
149
153
  ) -> None:
150
- self.proxy_count = proxy_count
151
- self.proxy_list = self._is_valid_proxy_trusted_list(proxy_list or [])
154
+ """
155
+ Initialize the IpWareProxy class with optional proxy count and proxy list.
152
156
 
153
- def _is_valid_proxy_trusted_list(self, proxy_list: Any) -> List[str]:
157
+ Args:
158
+ proxy_count: The expected number of proxies, can be None if no specific count is enforced.
159
+ proxy_list: A list of partial IP addresses as trusted proxies, can be None.
154
160
  """
155
- Checks if the proxy list is a valid list of strings
156
- @return: proxy list or raises an exception
161
+ if proxy_count is not None and proxy_count < 0:
162
+ raise ValueError("proxy_count must be non-negative")
163
+ self.proxy_count = proxy_count
164
+ self.proxy_list = self._validate_proxy_list(proxy_list or [])
165
+
166
+ def _validate_proxy_list(self, proxy_list: List[str]) -> List[str]:
157
167
  """
168
+ Validates that the proxy list contains only strings.
169
+
170
+ Args:
171
+ proxy_list: A list of proxies.
158
172
 
159
- if not isinstance(proxy_list, list):
160
- raise ValueError("Parameter must be a list")
161
- if not all(isinstance(x, str) for x in proxy_list):
162
- raise ValueError("All elements in list must be strings")
173
+ Returns:
174
+ The proxy list if all items are valid strings.
163
175
 
176
+ Raises:
177
+ ValueError: If the proxy list is not a list of strings.
178
+ """
179
+ if not all(isinstance(ip, str) for ip in proxy_list):
180
+ raise ValueError("All elements in the proxy list must be strings.")
164
181
  return proxy_list
165
182
 
166
- def is_proxy_count_valid(
167
- self, ip_list: List[IpAddressType], strict: bool = False
168
- ) -> bool:
183
+ def is_proxy_count_valid(self, ip_list: List[str], strict: bool = False) -> bool:
169
184
  """
170
- Checks if the proxy count is valid
171
- @param ip_list: list of ip addresses
172
- @param strict: if True, we must have exactly proxy_count proxies, including `0` proxies
173
- @return: True if the proxy count is valid, False otherwise
185
+ Validates the proxy count against a list of IP addresses.
186
+
187
+ Args:
188
+ ip_list: A list of IP addresses from the request headers.
189
+ strict: If True, the number of proxies must exactly match the proxy_count.
190
+
191
+ Returns:
192
+ True if the proxy count is valid, False otherwise.
174
193
  """
175
- # No proxy count check is required
176
194
  if self.proxy_count is None:
177
- return True
178
-
179
- ip_count: int = len(ip_list)
195
+ return True # No proxy count specified, so always valid.
180
196
 
197
+ ip_count = len(ip_list)
181
198
  if strict:
182
- # our first proxy takes the last ip address and treats it as client ip
183
- return self.proxy_count == ip_count - 1
199
+ return ip_count - 1 == self.proxy_count
200
+ # Exact match required, excluding client's own IP.
184
201
 
185
- # the client could have gone through their own proxy and included extra ips
186
- # client could be sending in the header: X-Forwarded-For: <fake_ip>, <client_ip>
187
- return ip_count - 1 > self.proxy_count
202
+ return ip_count - 1 >= self.proxy_count
203
+ # Allow more proxies than the count, excluding client's IP.
188
204
 
189
205
  def is_proxy_trusted_list_valid(
190
- self,
191
- ip_list: List[IpAddressType],
192
- strict: bool = False,
206
+ self, ip_list: List[str], strict: bool = False
193
207
  ) -> bool:
194
208
  """
195
- Checks if the proxy list is valid (all proxies are in the proxy_list)
196
- @param ip_list: list of ip addresses
197
- @param strict: if True, we must have exactly proxy_count proxies
198
- @return: client's best match ip address or False
209
+ Checks if all proxies in the incoming list are trusted based on the proxy_list.
210
+
211
+ Args:
212
+ ip_list: A list of IP addresses from the request headers.
213
+ strict: If True, the number of proxies must exactly match the length of proxy_list.
214
+
215
+ Returns:
216
+ True if all proxies are trusted, False otherwise.
199
217
  """
200
218
  if not self.proxy_list:
201
- return True
219
+ return True # No specific proxies to trust, so always valid.
202
220
 
203
221
  ip_count = len(ip_list)
204
222
  proxy_list_count = len(self.proxy_list)
205
223
 
206
- # in strict mode, total ip count must be 1 more than proxy count
207
224
  if strict and ip_count - 1 != proxy_list_count:
208
- return False
225
+ return False # Strict mode: Exact count match required.
209
226
 
210
- # total ip count (client + proxies) must be more than proxy count
211
227
  if ip_count - 1 < proxy_list_count:
212
- return False
228
+ return False # Not enough IPs to match the trusted proxies.
213
229
 
214
- # start from the end, slice the incoming ip list to the same length as the trusted proxy list
215
- ip_list_slice = ip_list[-proxy_list_count:]
216
- for index, value in enumerate(ip_list_slice):
217
- if not str(value).startswith(self.proxy_list[index]):
218
- return False
219
-
220
- return True
230
+ # Verify each proxy against the trusted list by comparing each corresponding element.
231
+ return all(
232
+ str(ip).startswith(trusted_proxy_pattern)
233
+ for ip, trusted_proxy_pattern in zip(
234
+ ip_list[-proxy_list_count:], self.proxy_list
235
+ )
236
+ )
221
237
 
222
238
 
223
239
  class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
224
240
  """
225
- A class that makes best effort to determine the client's IP address.
241
+ A class that makes a best effort to determine the client's IP address.
226
242
  """
227
243
 
228
244
  def __init__(
@@ -237,24 +253,23 @@ class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
237
253
 
238
254
  def get_meta_value(self, meta: Dict[str, str], key: str) -> str:
239
255
  """
240
- Given a key, it returns a cleaned up version of the value
241
- @param key: the key to lookup
242
- @return: the value of the key or empty string
256
+ Returns a cleaned up version of the value for a given key.
257
+ @param key: The key to look up.
258
+ @return: The value of the key or an empty string if the key is not found.
243
259
  """
244
260
  meta = meta or {}
245
261
  return meta.get(key, meta.get(key.replace("_", "-"), "")).strip()
246
262
 
247
263
  def get_meta_values(self, meta: Dict[str, str]) -> List[str]:
248
264
  """
249
- Given a list of keys, it returns a list of cleaned up values
250
- @return: a list of values
265
+ Returns a list of cleaned up values for the keys defined in 'precedence'.
266
+ @return: A list of values.
251
267
  """
252
268
  meta_list: List[str] = []
253
269
  for key in self.precedence:
254
270
  value = self.get_meta_value(meta, key).strip()
255
271
  if value:
256
272
  meta_list.append(value)
257
-
258
273
  return meta_list
259
274
 
260
275
  def get_client_ip(
@@ -265,13 +280,11 @@ class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
265
280
  """
266
281
  Returns the client's IP address.
267
282
  """
268
-
269
283
  loopback_list: List[IpAddressType] = []
270
284
  private_list: List[OptionalIpAddressType] = []
271
285
 
272
286
  for ip_str in self.get_meta_values(meta):
273
-
274
- ip_list = self.get_ips_from_string(ip_str)
287
+ ip_list = self.get_ips_from_string(ip_str, strict)
275
288
  if not ip_list:
276
289
  continue
277
290
 
@@ -283,64 +296,40 @@ class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
283
296
  if not proxy_list_validated:
284
297
  continue
285
298
 
286
- client_ip, trusted_route = self.get_best_ip(
287
- ip_list, proxy_count_validated, proxy_list_validated
288
- )
289
-
290
- # we found a global ip, return it
291
- if client_ip is not None and client_ip.is_global:
292
- return client_ip, trusted_route
293
-
294
- # we found a private ip, save it
295
- if client_ip is not None and client_ip.is_loopback:
296
- loopback_list.append(client_ip)
297
- else:
298
- # if not global (public) or loopback (local), we treat it asd private
299
- private_list.append(client_ip)
299
+ client_ip, trusted_route = self.get_best_ip(ip_list)
300
+ if client_ip is not None:
301
+ if client_ip.is_global:
302
+ return client_ip, trusted_route
303
+ if client_ip.is_loopback:
304
+ loopback_list.append(client_ip)
305
+ else:
306
+ private_list.append(client_ip)
300
307
 
301
- # we have not been able to locate a global ip
302
- # it could be the server is running on the intranet
303
- # we will return the first private ip we found
304
308
  if private_list:
305
309
  return private_list[0], False
306
-
307
- # we have not been able to locate a global ip, nor a private ip
308
- # it could be the server is running on a loopback address serving local requests
309
310
  if loopback_list:
310
311
  return loopback_list[0], False
311
-
312
- # we were unable to find any ip address
313
312
  return None, False
314
313
 
315
314
  def get_best_ip(
316
315
  self,
317
316
  ip_list: List[IpAddressType],
318
- proxy_count_validated: bool = True,
319
- proxy_list_validated: bool = True,
320
317
  ) -> Tuple[OptionalIpAddressType, bool]:
321
318
  """
322
- Returns the best possible ip for the client.
319
+ Determines the best possible IP address for the client from a list of IP addresses.
323
320
  """
324
-
325
321
  if not ip_list:
326
- logger.warning("Invalid ip list provided.")
322
+ logger.warning("Invalid IP list provided.")
327
323
  return None, False
328
324
 
329
- # the incoming ips match our trusted proxy list
330
- if len(self.proxy_list) > 0 and proxy_list_validated:
325
+ if self.proxy_list and len(self.proxy_list) > 0:
331
326
  best_client_ip_index = len(self.proxy_list) + 1
332
327
  best_client_ip = ip_list[-best_client_ip_index]
333
328
  return best_client_ip, True
334
329
 
335
- # the incoming ips match our proxy count
336
- if (
337
- self.proxy_count is not None
338
- and self.proxy_count > 0
339
- and proxy_count_validated
340
- ):
330
+ if self.proxy_count is not None:
341
331
  best_client_ip_index = self.proxy_count + 1
342
332
  best_client_ip = ip_list[-best_client_ip_index]
343
333
  return best_client_ip, True
344
334
 
345
- # we don't track proxy related info, so we just return the first ip
346
335
  return ip_list[0], False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-ipware
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary: A Python package to retrieve user's IP address
5
5
  Author-email: Val Neekman <info@neekware.com>
6
6
  License: MIT
@@ -0,0 +1,9 @@
1
+ python_ipware/__init__.py,sha256=TCZx4mt5Gxr2D_zNU6j0ynopdsAN_v4xeiTWz1OXSpY,87
2
+ python_ipware/__version__.py,sha256=xEb7Z4b8xalXXExBg42XPAhbJKniHzcsEPjp-6S3ppg,22
3
+ python_ipware/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ python_ipware/python_ipware.py,sha256=IotMZQIGSOu2XCbmdYH4WuhBvXm-lJVJHe3FXylSMvc,12725
5
+ python_ipware-2.0.5.dist-info/LICENSE,sha256=MLpNxpqfTc4TLdcDk3x6k7Vz4lJGBNLV-SxQZlFMDU8,1103
6
+ python_ipware-2.0.5.dist-info/METADATA,sha256=3Lsxn8HubcN0KnuwggZLcDu5h67rZc5YOn5ad-YNNdY,15420
7
+ python_ipware-2.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
+ python_ipware-2.0.5.dist-info/top_level.txt,sha256=URE4dvjpReJtRHyQpJqoBow5EWt_RqVXFn85B9Tq5Xw,14
9
+ python_ipware-2.0.5.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- python_ipware/__init__.py,sha256=TCZx4mt5Gxr2D_zNU6j0ynopdsAN_v4xeiTWz1OXSpY,87
2
- python_ipware/__version__.py,sha256=ZOg41aCgKEuObK5WhegikXektFWPJaeZ-TMDh0Ke95M,22
3
- python_ipware/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- python_ipware/python_ipware.py,sha256=sN3NeWWs96beGzI9x3YZ9ucYnMIZh1Q4fWOOIm2_P-s,11987
5
- python_ipware-2.0.4.dist-info/LICENSE,sha256=MLpNxpqfTc4TLdcDk3x6k7Vz4lJGBNLV-SxQZlFMDU8,1103
6
- python_ipware-2.0.4.dist-info/METADATA,sha256=-KqYlSwY52kwnnAMA7ym2A31L1yX8BffjMdZ3I32Wl0,15420
7
- python_ipware-2.0.4.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
- python_ipware-2.0.4.dist-info/top_level.txt,sha256=URE4dvjpReJtRHyQpJqoBow5EWt_RqVXFn85B9Tq5Xw,14
9
- python_ipware-2.0.4.dist-info/RECORD,,