ipspot 0.4__py3-none-any.whl → 0.6__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.
- ipspot/__init__.py +2 -1
- ipspot/cli.py +50 -10
- ipspot/ipv4.py +117 -73
- ipspot/ipv6.py +380 -0
- ipspot/params.py +20 -1
- ipspot/utils.py +64 -1
- {ipspot-0.4.dist-info → ipspot-0.6.dist-info}/METADATA +180 -38
- ipspot-0.6.dist-info/RECORD +14 -0
- ipspot-0.4.dist-info/RECORD +0 -13
- {ipspot-0.4.dist-info → ipspot-0.6.dist-info}/WHEEL +0 -0
- {ipspot-0.4.dist-info → ipspot-0.6.dist-info}/entry_points.txt +0 -0
- {ipspot-0.4.dist-info → ipspot-0.6.dist-info}/licenses/AUTHORS.md +0 -0
- {ipspot-0.4.dist-info → ipspot-0.6.dist-info}/licenses/LICENSE +0 -0
- {ipspot-0.4.dist-info → ipspot-0.6.dist-info}/top_level.txt +0 -0
ipspot/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""ipspot modules."""
|
|
3
|
-
from .params import IPSPOT_VERSION, IPv4API
|
|
3
|
+
from .params import IPSPOT_VERSION, IPv4API, IPv6API
|
|
4
4
|
from .ipv4 import get_private_ipv4, get_public_ipv4, is_ipv4
|
|
5
|
+
from .ipv6 import get_private_ipv6, get_public_ipv6, is_ipv6
|
|
5
6
|
from .utils import is_loopback
|
|
6
7
|
__version__ = IPSPOT_VERSION
|
ipspot/cli.py
CHANGED
|
@@ -4,8 +4,9 @@ import argparse
|
|
|
4
4
|
from typing import Union, Tuple
|
|
5
5
|
from art import tprint
|
|
6
6
|
from .ipv4 import get_public_ipv4, get_private_ipv4
|
|
7
|
+
from .ipv6 import get_public_ipv6, get_private_ipv6
|
|
7
8
|
from .utils import _filter_parameter
|
|
8
|
-
from .params import IPv4API, PARAMETERS_NAME_MAP
|
|
9
|
+
from .params import IPv4API, IPv6API, PARAMETERS_NAME_MAP
|
|
9
10
|
from .params import IPSPOT_OVERVIEW, IPSPOT_REPO, IPSPOT_VERSION
|
|
10
11
|
|
|
11
12
|
|
|
@@ -17,42 +18,72 @@ def ipspot_info() -> None: # pragma: no cover
|
|
|
17
18
|
print("Repo : " + IPSPOT_REPO)
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def display_ip_info(ipv4_api: IPv4API = IPv4API.AUTO_SAFE,
|
|
21
|
+
def display_ip_info(ipv4_api: IPv4API = IPv4API.AUTO_SAFE,
|
|
22
|
+
ipv6_api: IPv6API = IPv6API.AUTO_SAFE,
|
|
23
|
+
geo: bool=False,
|
|
21
24
|
timeout: Union[float, Tuple[float, float]]=5,
|
|
22
25
|
max_retries: int = 0, retry_delay: float = 1.0) -> None: # pragma: no cover
|
|
23
26
|
"""
|
|
24
27
|
Print collected IP and location data.
|
|
25
28
|
|
|
26
29
|
:param ipv4_api: public IPv4 API
|
|
30
|
+
:param ipv6_api: public IPv6 API
|
|
27
31
|
:param geo: geolocation flag
|
|
28
32
|
:param timeout: timeout value for API
|
|
29
33
|
:param max_retries: number of retries
|
|
30
34
|
:param retry_delay: delay between retries (in seconds)
|
|
31
35
|
"""
|
|
32
|
-
private_result = get_private_ipv4()
|
|
33
36
|
print("Private IP:\n")
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
private_ipv4_result = get_private_ipv4()
|
|
38
|
+
if private_ipv4_result["status"]:
|
|
39
|
+
private_ipv4 = private_ipv4_result["data"]["ip"]
|
|
40
|
+
else:
|
|
41
|
+
private_ipv4 = private_ipv4_result["error"]
|
|
42
|
+
print(" IPv4: {private_ipv4}\n".format(private_ipv4=private_ipv4))
|
|
43
|
+
|
|
44
|
+
private_ipv6_result = get_private_ipv6()
|
|
45
|
+
if private_ipv6_result["status"]:
|
|
46
|
+
private_ipv6 = private_ipv6_result["data"]["ip"]
|
|
47
|
+
else:
|
|
48
|
+
private_ipv6 = private_ipv6_result["error"]
|
|
49
|
+
print(" IPv6: {private_ipv6}".format(private_ipv6=private_ipv6))
|
|
36
50
|
|
|
37
51
|
public_title = "\nPublic IP"
|
|
38
52
|
if geo:
|
|
39
53
|
public_title += " and Location Info"
|
|
40
54
|
public_title += ":\n"
|
|
41
55
|
print(public_title)
|
|
42
|
-
|
|
56
|
+
print(" IPv4:\n")
|
|
57
|
+
public_ipv4_result = get_public_ipv4(
|
|
43
58
|
ipv4_api,
|
|
44
59
|
geo=geo,
|
|
45
60
|
timeout=timeout,
|
|
46
61
|
max_retries=max_retries,
|
|
47
62
|
retry_delay=retry_delay)
|
|
48
|
-
if
|
|
49
|
-
for name, parameter in sorted(
|
|
63
|
+
if public_ipv4_result["status"]:
|
|
64
|
+
for name, parameter in sorted(public_ipv4_result["data"].items()):
|
|
50
65
|
print(
|
|
51
|
-
"
|
|
66
|
+
" {name}: {parameter}".format(
|
|
52
67
|
name=PARAMETERS_NAME_MAP[name],
|
|
53
68
|
parameter=_filter_parameter(parameter)))
|
|
54
69
|
else:
|
|
55
|
-
print("
|
|
70
|
+
print(" Error: {public_ipv4_result[error]}".format(public_ipv4_result=public_ipv4_result))
|
|
71
|
+
|
|
72
|
+
print("\n IPv6:\n")
|
|
73
|
+
public_ipv6_result = get_public_ipv6(
|
|
74
|
+
ipv6_api,
|
|
75
|
+
geo=geo,
|
|
76
|
+
timeout=timeout,
|
|
77
|
+
max_retries=max_retries,
|
|
78
|
+
retry_delay=retry_delay)
|
|
79
|
+
if public_ipv6_result["status"]:
|
|
80
|
+
for name, parameter in sorted(public_ipv6_result["data"].items()):
|
|
81
|
+
print(
|
|
82
|
+
" {name}: {parameter}".format(
|
|
83
|
+
name=PARAMETERS_NAME_MAP[name],
|
|
84
|
+
parameter=_filter_parameter(parameter)))
|
|
85
|
+
else:
|
|
86
|
+
print(" Error: {public_ipv6_result[error]}".format(public_ipv6_result=public_ipv6_result))
|
|
56
87
|
|
|
57
88
|
|
|
58
89
|
def main() -> None: # pragma: no cover
|
|
@@ -65,6 +96,13 @@ def main() -> None: # pragma: no cover
|
|
|
65
96
|
choices=[
|
|
66
97
|
x.value for x in IPv4API],
|
|
67
98
|
default=IPv4API.AUTO_SAFE.value)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
'--ipv6-api',
|
|
101
|
+
help='public IPv6 API',
|
|
102
|
+
type=str.lower,
|
|
103
|
+
choices=[
|
|
104
|
+
x.value for x in IPv6API],
|
|
105
|
+
default=IPv6API.AUTO_SAFE.value)
|
|
68
106
|
parser.add_argument('--info', help='info', nargs="?", const=1)
|
|
69
107
|
parser.add_argument('--version', help='version', nargs="?", const=1)
|
|
70
108
|
parser.add_argument('--no-geo', help='no geolocation data', nargs="?", const=1, default=False)
|
|
@@ -79,9 +117,11 @@ def main() -> None: # pragma: no cover
|
|
|
79
117
|
ipspot_info()
|
|
80
118
|
else:
|
|
81
119
|
ipv4_api = IPv4API(args.ipv4_api)
|
|
120
|
+
ipv6_api = IPv6API(args.ipv6_api)
|
|
82
121
|
geo = not args.no_geo
|
|
83
122
|
display_ip_info(
|
|
84
123
|
ipv4_api=ipv4_api,
|
|
124
|
+
ipv6_api=ipv6_api,
|
|
85
125
|
geo=geo,
|
|
86
126
|
timeout=args.timeout,
|
|
87
127
|
max_retries=args.max_retries,
|
ipspot/ipv4.py
CHANGED
|
@@ -3,69 +3,9 @@
|
|
|
3
3
|
import ipaddress
|
|
4
4
|
import socket
|
|
5
5
|
from typing import Union, Dict, List, Tuple
|
|
6
|
-
import
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from .utils import is_loopback, _get_json_standard, _attempt_with_retries
|
|
10
|
-
from .params import REQUEST_HEADERS, IPv4API
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class IPv4HTTPAdapter(HTTPAdapter):
|
|
14
|
-
"""A custom HTTPAdapter that enforces the use of IPv4 for DNS resolution during HTTP(S) requests using the requests library."""
|
|
15
|
-
|
|
16
|
-
def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None:
|
|
17
|
-
"""
|
|
18
|
-
Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv4 addresses are used.
|
|
19
|
-
|
|
20
|
-
:param connections: the number of connection pools to cache
|
|
21
|
-
:param maxsize: the maximum number of connections to save in the pool
|
|
22
|
-
:param block: whether the connections should block when reaching the max size
|
|
23
|
-
:param kwargs: additional keyword arguments for the PoolManager
|
|
24
|
-
"""
|
|
25
|
-
self.poolmanager = PoolManager(
|
|
26
|
-
num_pools=connections,
|
|
27
|
-
maxsize=maxsize,
|
|
28
|
-
block=block,
|
|
29
|
-
socket_options=self._ipv4_socket_options(),
|
|
30
|
-
**kwargs
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
def _ipv4_socket_options(self) -> list:
|
|
34
|
-
"""
|
|
35
|
-
Temporarily patches socket.getaddrinfo to filter only IPv4 addresses (AF_INET).
|
|
36
|
-
|
|
37
|
-
:return: an empty list of socket options; DNS patching occurs here
|
|
38
|
-
"""
|
|
39
|
-
original_getaddrinfo = socket.getaddrinfo
|
|
40
|
-
|
|
41
|
-
def ipv4_only_getaddrinfo(*args: list, **kwargs: dict) -> List[Tuple]:
|
|
42
|
-
results = original_getaddrinfo(*args, **kwargs)
|
|
43
|
-
return [res for res in results if res[0] == socket.AF_INET]
|
|
44
|
-
|
|
45
|
-
self._original_getaddrinfo = socket.getaddrinfo
|
|
46
|
-
socket.getaddrinfo = ipv4_only_getaddrinfo
|
|
47
|
-
|
|
48
|
-
return []
|
|
49
|
-
|
|
50
|
-
def __del__(self) -> None:
|
|
51
|
-
"""Restores the original socket.getaddrinfo function upon adapter deletion."""
|
|
52
|
-
if hasattr(self, "_original_getaddrinfo"):
|
|
53
|
-
socket.getaddrinfo = self._original_getaddrinfo
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _get_json_ipv4_forced(url: str, timeout: Union[float, Tuple[float, float]]) -> dict:
|
|
57
|
-
"""
|
|
58
|
-
Send GET request with forced IPv4 using IPv4HTTPAdapter that returns JSON response.
|
|
59
|
-
|
|
60
|
-
:param url: API url
|
|
61
|
-
:param timeout: timeout value for API
|
|
62
|
-
"""
|
|
63
|
-
with requests.Session() as session:
|
|
64
|
-
session.mount("http://", IPv4HTTPAdapter())
|
|
65
|
-
session.mount("https://", IPv4HTTPAdapter())
|
|
66
|
-
response = session.get(url, headers=REQUEST_HEADERS, timeout=timeout)
|
|
67
|
-
response.raise_for_status()
|
|
68
|
-
return response.json()
|
|
6
|
+
from .utils import is_loopback, _attempt_with_retries
|
|
7
|
+
from .utils import _get_json_standard, _get_json_force_ip
|
|
8
|
+
from .params import IPv4API
|
|
69
9
|
|
|
70
10
|
|
|
71
11
|
def is_ipv4(ip: str) -> bool:
|
|
@@ -180,7 +120,7 @@ def _my_ip_io_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
180
120
|
return {"status": False, "error": str(e)}
|
|
181
121
|
|
|
182
122
|
|
|
183
|
-
def _ifconfig_co_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
123
|
+
def _ifconfig_co_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]] # very low rate limit
|
|
184
124
|
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
185
125
|
"""
|
|
186
126
|
Get public IP and geolocation using ifconfig.co.
|
|
@@ -189,7 +129,7 @@ def _ifconfig_co_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]
|
|
|
189
129
|
:param timeout: timeout value for API
|
|
190
130
|
"""
|
|
191
131
|
try:
|
|
192
|
-
data =
|
|
132
|
+
data = _get_json_force_ip(url="https://ifconfig.co/json", timeout=timeout, version="ipv4")
|
|
193
133
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ifconfig.co"}}
|
|
194
134
|
if geo:
|
|
195
135
|
geo_data = {
|
|
@@ -217,7 +157,7 @@ def _ipapi_co_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
217
157
|
:param timeout: timeout value for API
|
|
218
158
|
"""
|
|
219
159
|
try:
|
|
220
|
-
data =
|
|
160
|
+
data = _get_json_force_ip(url="https://ipapi.co/json/", timeout=timeout, version="ipv4")
|
|
221
161
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ipapi.co"}}
|
|
222
162
|
if geo:
|
|
223
163
|
geo_data = {
|
|
@@ -245,7 +185,7 @@ def _ip_api_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
245
185
|
:param timeout: timeout value for API
|
|
246
186
|
"""
|
|
247
187
|
try:
|
|
248
|
-
data =
|
|
188
|
+
data = _get_json_force_ip(url="http://ip-api.com/json/", timeout=timeout, version="ipv4")
|
|
249
189
|
if data.get("status") != "success":
|
|
250
190
|
return {"status": False, "error": "ip-api lookup failed"}
|
|
251
191
|
result = {"status": True, "data": {"ip": data["query"], "api": "ip-api.com"}}
|
|
@@ -275,7 +215,7 @@ def _ipinfo_io_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
275
215
|
:param timeout: timeout value for API
|
|
276
216
|
"""
|
|
277
217
|
try:
|
|
278
|
-
data =
|
|
218
|
+
data = _get_json_force_ip(url="https://ipinfo.io/json", timeout=timeout, version="ipv4")
|
|
279
219
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ipinfo.io"}}
|
|
280
220
|
if geo:
|
|
281
221
|
loc = data.get("loc", "").split(",")
|
|
@@ -304,7 +244,7 @@ def _reallyfreegeoip_org_ipv4(geo: bool=False, timeout: Union[float, Tuple[float
|
|
|
304
244
|
:param timeout: timeout value for API
|
|
305
245
|
"""
|
|
306
246
|
try:
|
|
307
|
-
data =
|
|
247
|
+
data = _get_json_force_ip(url="https://reallyfreegeoip.org/json/", timeout=timeout, version="ipv4")
|
|
308
248
|
result = {"status": True, "data": {"ip": data["ip"], "api": "reallyfreegeoip.org"}}
|
|
309
249
|
if geo:
|
|
310
250
|
geo_data = {
|
|
@@ -388,7 +328,7 @@ def _myip_la_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
|
|
|
388
328
|
:param timeout: timeout value for API
|
|
389
329
|
"""
|
|
390
330
|
try:
|
|
391
|
-
data =
|
|
331
|
+
data = _get_json_force_ip(url="https://api.myip.la/en?json", timeout=timeout, version="ipv4")
|
|
392
332
|
result = {"status": True, "data": {"ip": data["ip"], "api": "myip.la"}}
|
|
393
333
|
if geo:
|
|
394
334
|
loc = data.get("location", {})
|
|
@@ -417,8 +357,9 @@ def _freeipapi_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, floa
|
|
|
417
357
|
:param timeout: timeout value for API
|
|
418
358
|
"""
|
|
419
359
|
try:
|
|
420
|
-
data =
|
|
360
|
+
data = _get_json_force_ip(url="https://freeipapi.com/api/json", timeout=timeout, version="ipv4")
|
|
421
361
|
result = {"status": True, "data": {"ip": data["ipAddress"], "api": "freeipapi.com"}}
|
|
362
|
+
tzs = data.get("timeZones", [])
|
|
422
363
|
if geo:
|
|
423
364
|
geo_data = {
|
|
424
365
|
"city": data.get("cityName"),
|
|
@@ -427,8 +368,96 @@ def _freeipapi_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, floa
|
|
|
427
368
|
"country_code": data.get("countryCode"),
|
|
428
369
|
"latitude": data.get("latitude"),
|
|
429
370
|
"longitude": data.get("longitude"),
|
|
430
|
-
"organization":
|
|
431
|
-
"timezone":
|
|
371
|
+
"organization": data.get("asnOrganization"),
|
|
372
|
+
"timezone": tzs[0] if len(tzs) > 0 else None
|
|
373
|
+
}
|
|
374
|
+
result["data"].update(geo_data)
|
|
375
|
+
return result
|
|
376
|
+
except Exception as e:
|
|
377
|
+
return {"status": False, "error": str(e)}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _ipquery_io_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
|
|
381
|
+
) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
382
|
+
"""
|
|
383
|
+
Get public IP and geolocation using ipquery.io.
|
|
384
|
+
|
|
385
|
+
:param geo: geolocation flag
|
|
386
|
+
:param timeout: timeout value for API
|
|
387
|
+
"""
|
|
388
|
+
try:
|
|
389
|
+
data = _get_json_force_ip(url="https://api.ipquery.io/?format=json", timeout=timeout, version="ipv4")
|
|
390
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ipquery.io"}}
|
|
391
|
+
if geo:
|
|
392
|
+
loc = data.get("location", {})
|
|
393
|
+
isp = data.get("isp", {})
|
|
394
|
+
geo_data = {
|
|
395
|
+
"city": loc.get("city"),
|
|
396
|
+
"region": loc.get("state"),
|
|
397
|
+
"country": loc.get("country"),
|
|
398
|
+
"country_code": loc.get("country_code"),
|
|
399
|
+
"latitude": loc.get("latitude"),
|
|
400
|
+
"longitude": loc.get("longitude"),
|
|
401
|
+
"timezone": loc.get("timezone"),
|
|
402
|
+
"organization": isp.get("org"),
|
|
403
|
+
}
|
|
404
|
+
result["data"].update(geo_data)
|
|
405
|
+
return result
|
|
406
|
+
except Exception as e:
|
|
407
|
+
return {"status": False, "error": str(e)}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _ipwho_is_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
|
|
411
|
+
) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
412
|
+
"""
|
|
413
|
+
Get public IP and geolocation using ipwho.is.
|
|
414
|
+
|
|
415
|
+
:param geo: geolocation flag
|
|
416
|
+
:param timeout: timeout value for API
|
|
417
|
+
"""
|
|
418
|
+
try:
|
|
419
|
+
data = _get_json_force_ip(url="https://ipwho.is", timeout=timeout, version="ipv4")
|
|
420
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ipwho.is"}}
|
|
421
|
+
if geo:
|
|
422
|
+
connection = data.get("connection", {})
|
|
423
|
+
timezone = data.get("timezone", {})
|
|
424
|
+
geo_data = {
|
|
425
|
+
"city": data.get("city"),
|
|
426
|
+
"region": data.get("region"),
|
|
427
|
+
"country": data.get("country"),
|
|
428
|
+
"country_code": data.get("country_code"),
|
|
429
|
+
"latitude": data.get("latitude"),
|
|
430
|
+
"longitude": data.get("longitude"),
|
|
431
|
+
"organization": connection.get("org"),
|
|
432
|
+
"timezone": timezone.get("id")
|
|
433
|
+
}
|
|
434
|
+
result["data"].update(geo_data)
|
|
435
|
+
return result
|
|
436
|
+
except Exception as e:
|
|
437
|
+
return {"status": False, "error": str(e)}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _wtfismyip_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
|
|
441
|
+
) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
442
|
+
"""
|
|
443
|
+
Get public IP and geolocation using wtfismyip.com.
|
|
444
|
+
|
|
445
|
+
:param geo: geolocation flag
|
|
446
|
+
:param timeout: timeout value for API
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
data = _get_json_standard(url="https://json.ipv4.wtfismyip.com", timeout=timeout)
|
|
450
|
+
result = {"status": True, "data": {"ip": data["YourFuckingIPAddress"], "api": "wtfismyip.com"}}
|
|
451
|
+
if geo:
|
|
452
|
+
geo_data = {
|
|
453
|
+
"city": data.get("YourFuckingCity"),
|
|
454
|
+
"region": None,
|
|
455
|
+
"country": data.get("YourFuckingCountry"),
|
|
456
|
+
"country_code": data.get("YourFuckingCountryCode"),
|
|
457
|
+
"latitude": None,
|
|
458
|
+
"longitude": None,
|
|
459
|
+
"organization": data.get("YourFuckingISP"),
|
|
460
|
+
"timezone": None
|
|
432
461
|
}
|
|
433
462
|
result["data"].update(geo_data)
|
|
434
463
|
return result
|
|
@@ -497,6 +526,21 @@ IPV4_API_MAP = {
|
|
|
497
526
|
"geo": True,
|
|
498
527
|
"function": _myip_la_ipv4,
|
|
499
528
|
},
|
|
529
|
+
IPv4API.IPQUERY_IO: {
|
|
530
|
+
"thread_safe": False,
|
|
531
|
+
"geo": True,
|
|
532
|
+
"function": _ipquery_io_ipv4,
|
|
533
|
+
},
|
|
534
|
+
IPv4API.IPWHO_IS: {
|
|
535
|
+
"thread_safe": False,
|
|
536
|
+
"geo": True,
|
|
537
|
+
"function": _ipwho_is_ipv4,
|
|
538
|
+
},
|
|
539
|
+
IPv4API.WTFISMYIP_COM: {
|
|
540
|
+
"thread_safe": True,
|
|
541
|
+
"geo": True,
|
|
542
|
+
"function": _wtfismyip_com_ipv4
|
|
543
|
+
},
|
|
500
544
|
}
|
|
501
545
|
|
|
502
546
|
|
ipspot/ipv6.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ipspot ipv6 functions."""
|
|
3
|
+
import ipaddress
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Union, Dict, List, Tuple
|
|
6
|
+
from .params import IPv6API
|
|
7
|
+
from .utils import is_loopback, _attempt_with_retries
|
|
8
|
+
from .utils import _get_json_standard, _get_json_force_ip
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_ipv6(ip: str) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if the given input is a valid IPv6 address.
|
|
14
|
+
|
|
15
|
+
:param ip: input IP
|
|
16
|
+
"""
|
|
17
|
+
if not isinstance(ip, str):
|
|
18
|
+
return False
|
|
19
|
+
try:
|
|
20
|
+
_ = ipaddress.IPv6Address(ip)
|
|
21
|
+
return True
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_private_ipv6() -> Dict[str, Union[bool, Dict[str, str], str]]:
|
|
27
|
+
"""Retrieve the private IPv6 address."""
|
|
28
|
+
try:
|
|
29
|
+
with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:
|
|
30
|
+
s.connect(("2001:4860:4860::8888", 80))
|
|
31
|
+
private_ip = s.getsockname()[0]
|
|
32
|
+
private_ip = private_ip.split("%")[0]
|
|
33
|
+
if is_ipv6(private_ip) and not is_loopback(private_ip):
|
|
34
|
+
return {"status": True, "data": {"ip": private_ip}}
|
|
35
|
+
return {"status": False, "error": "Could not identify a non-loopback IPv6 address for this system."}
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return {"status": False, "error": str(e)}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ip_sb_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
41
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
42
|
+
"""
|
|
43
|
+
Get public IP and geolocation using ip.sb.
|
|
44
|
+
|
|
45
|
+
:param geo: geolocation flag
|
|
46
|
+
:param timeout: timeout value for API
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
data = _get_json_standard(url="https://api-ipv6.ip.sb/geoip", timeout=timeout)
|
|
50
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ip.sb"}}
|
|
51
|
+
if geo:
|
|
52
|
+
geo_data = {
|
|
53
|
+
"city": data.get("city"),
|
|
54
|
+
"region": data.get("region"),
|
|
55
|
+
"country": data.get("country"),
|
|
56
|
+
"country_code": data.get("country_code"),
|
|
57
|
+
"latitude": data.get("latitude"),
|
|
58
|
+
"longitude": data.get("longitude"),
|
|
59
|
+
"organization": data.get("organization"),
|
|
60
|
+
"timezone": data.get("timezone")
|
|
61
|
+
}
|
|
62
|
+
result["data"].update(geo_data)
|
|
63
|
+
return result
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return {"status": False, "error": str(e)}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ident_me_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
69
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
70
|
+
"""
|
|
71
|
+
Get public IP and geolocation using ident.me.
|
|
72
|
+
|
|
73
|
+
:param geo: geolocation flag
|
|
74
|
+
:param timeout: timeout value for API
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
data = _get_json_standard(url="https://6.ident.me/json", timeout=timeout)
|
|
78
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ident.me"}}
|
|
79
|
+
if geo:
|
|
80
|
+
geo_data = {
|
|
81
|
+
"city": data.get("city"),
|
|
82
|
+
"region": None,
|
|
83
|
+
"country": data.get("country"),
|
|
84
|
+
"country_code": data.get("cc"),
|
|
85
|
+
"latitude": data.get("latitude"),
|
|
86
|
+
"longitude": data.get("longitude"),
|
|
87
|
+
"organization": data.get("aso"),
|
|
88
|
+
"timezone": data.get("tz")
|
|
89
|
+
}
|
|
90
|
+
result["data"].update(geo_data)
|
|
91
|
+
return result
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return {"status": False, "error": str(e)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _tnedi_me_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
97
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
98
|
+
"""
|
|
99
|
+
Get public IP and geolocation using tnedi.me.
|
|
100
|
+
|
|
101
|
+
:param geo: geolocation flag
|
|
102
|
+
:param timeout: timeout value for API
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
data = _get_json_standard(url="https://6.tnedi.me/json", timeout=timeout)
|
|
106
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "tnedi.me"}}
|
|
107
|
+
if geo:
|
|
108
|
+
geo_data = {
|
|
109
|
+
"city": data.get("city"),
|
|
110
|
+
"region": None,
|
|
111
|
+
"country": data.get("country"),
|
|
112
|
+
"country_code": data.get("cc"),
|
|
113
|
+
"latitude": data.get("latitude"),
|
|
114
|
+
"longitude": data.get("longitude"),
|
|
115
|
+
"organization": data.get("aso"),
|
|
116
|
+
"timezone": data.get("tz")
|
|
117
|
+
}
|
|
118
|
+
result["data"].update(geo_data)
|
|
119
|
+
return result
|
|
120
|
+
except Exception as e:
|
|
121
|
+
return {"status": False, "error": str(e)}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _ipleak_net_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
125
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
126
|
+
"""
|
|
127
|
+
Get public IP and geolocation using ipleak.net.
|
|
128
|
+
|
|
129
|
+
:param geo: geolocation flag
|
|
130
|
+
:param timeout: timeout value for API
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
data = _get_json_standard(url="https://ipv6.ipleak.net/json/", timeout=timeout)
|
|
134
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ipleak.net"}}
|
|
135
|
+
if geo:
|
|
136
|
+
geo_data = {
|
|
137
|
+
"city": data.get("city_name"),
|
|
138
|
+
"region": data.get("region_name"),
|
|
139
|
+
"country": data.get("country_name"),
|
|
140
|
+
"country_code": data.get("country_code"),
|
|
141
|
+
"latitude": data.get("latitude"),
|
|
142
|
+
"longitude": data.get("longitude"),
|
|
143
|
+
"organization": data.get("isp_name"),
|
|
144
|
+
"timezone": data.get("time_zone")
|
|
145
|
+
}
|
|
146
|
+
result["data"].update(geo_data)
|
|
147
|
+
return result
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return {"status": False, "error": str(e)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _my_ip_io_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
153
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
154
|
+
"""
|
|
155
|
+
Get public IP and geolocation using my-ip.io.
|
|
156
|
+
|
|
157
|
+
:param geo: geolocation flag
|
|
158
|
+
:param timeout: timeout value for API
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
data = _get_json_standard(url="https://api6.my-ip.io/v2/ip.json", timeout=timeout)
|
|
162
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "my-ip.io"}}
|
|
163
|
+
if geo:
|
|
164
|
+
geo_data = {
|
|
165
|
+
"city": data.get("city"),
|
|
166
|
+
"region": data.get("region"),
|
|
167
|
+
"country": data.get("country", {}).get("name"),
|
|
168
|
+
"country_code": data.get("country", {}).get("code"),
|
|
169
|
+
"latitude": data.get("location", {}).get("lat"),
|
|
170
|
+
"longitude": data.get("location", {}).get("lon"),
|
|
171
|
+
"organization": data.get("asn", {}).get("name"),
|
|
172
|
+
"timezone": data.get("timeZone")
|
|
173
|
+
}
|
|
174
|
+
result["data"].update(geo_data)
|
|
175
|
+
return result
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return {"status": False, "error": str(e)}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _ifconfig_co_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]] # very low rate limit
|
|
181
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
182
|
+
"""
|
|
183
|
+
Get public IP and geolocation using ifconfig.co.
|
|
184
|
+
|
|
185
|
+
:param geo: geolocation flag
|
|
186
|
+
:param timeout: timeout value for API
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
data = _get_json_force_ip(url="https://ifconfig.co/json", timeout=timeout, version="ipv6")
|
|
190
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ifconfig.co"}}
|
|
191
|
+
if geo:
|
|
192
|
+
geo_data = {
|
|
193
|
+
"city": data.get("city"),
|
|
194
|
+
"region": data.get("region_name"),
|
|
195
|
+
"country": data.get("country"),
|
|
196
|
+
"country_code": data.get("country_iso"),
|
|
197
|
+
"latitude": data.get("latitude"),
|
|
198
|
+
"longitude": data.get("longitude"),
|
|
199
|
+
"organization": data.get("asn_org"),
|
|
200
|
+
"timezone": data.get("time_zone")
|
|
201
|
+
}
|
|
202
|
+
result["data"].update(geo_data)
|
|
203
|
+
return result
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return {"status": False, "error": str(e)}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _reallyfreegeoip_org_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
209
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
210
|
+
"""
|
|
211
|
+
Get public IP and geolocation using reallyfreegeoip.org.
|
|
212
|
+
|
|
213
|
+
:param geo: geolocation flag
|
|
214
|
+
:param timeout: timeout value for API
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
data = _get_json_force_ip(url="https://reallyfreegeoip.org/json/", timeout=timeout, version="ipv6")
|
|
218
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "reallyfreegeoip.org"}}
|
|
219
|
+
if geo:
|
|
220
|
+
geo_data = {
|
|
221
|
+
"city": data.get("city"),
|
|
222
|
+
"region": data.get("region_name"),
|
|
223
|
+
"country": data.get("country_name"),
|
|
224
|
+
"country_code": data.get("country_code"),
|
|
225
|
+
"latitude": data.get("latitude"),
|
|
226
|
+
"longitude": data.get("longitude"),
|
|
227
|
+
"organization": None, # does not provide organization
|
|
228
|
+
"timezone": data.get("time_zone")
|
|
229
|
+
}
|
|
230
|
+
result["data"].update(geo_data)
|
|
231
|
+
return result
|
|
232
|
+
except Exception as e:
|
|
233
|
+
return {"status": False, "error": str(e)}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _myip_la_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
237
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
238
|
+
"""
|
|
239
|
+
Get public IP and geolocation using myip.la.
|
|
240
|
+
|
|
241
|
+
:param geo: geolocation flag
|
|
242
|
+
:param timeout: timeout value for API
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
data = _get_json_force_ip(url="https://api.myip.la/en?json", timeout=timeout, version="ipv6")
|
|
246
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "myip.la"}}
|
|
247
|
+
if geo:
|
|
248
|
+
loc = data.get("location", {})
|
|
249
|
+
geo_data = {
|
|
250
|
+
"city": loc.get("city"),
|
|
251
|
+
"region": loc.get("province"),
|
|
252
|
+
"country": loc.get("country_name"),
|
|
253
|
+
"country_code": loc.get("country_code"),
|
|
254
|
+
"latitude": float(loc.get("latitude")) if loc.get("latitude") else None,
|
|
255
|
+
"longitude": float(loc.get("longitude")) if loc.get("longitude") else None,
|
|
256
|
+
"organization": None,
|
|
257
|
+
"timezone": None
|
|
258
|
+
}
|
|
259
|
+
result["data"].update(geo_data)
|
|
260
|
+
return result
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return {"status": False, "error": str(e)}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _freeipapi_com_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
266
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
267
|
+
"""
|
|
268
|
+
Get public IP and geolocation using freeipapi.com.
|
|
269
|
+
|
|
270
|
+
:param geo: geolocation flag
|
|
271
|
+
:param timeout: timeout value for API
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
data = _get_json_force_ip(url="https://free.freeipapi.com/api/json", timeout=timeout, version="ipv6")
|
|
275
|
+
result = {"status": True, "data": {"ip": data["ipAddress"], "api": "freeipapi.com"}}
|
|
276
|
+
tzs = data.get("timeZones", [])
|
|
277
|
+
if geo:
|
|
278
|
+
geo_data = {
|
|
279
|
+
"city": data.get("cityName"),
|
|
280
|
+
"region": data.get("regionName"),
|
|
281
|
+
"country": data.get("countryName"),
|
|
282
|
+
"country_code": data.get("countryCode"),
|
|
283
|
+
"latitude": data.get("latitude"),
|
|
284
|
+
"longitude": data.get("longitude"),
|
|
285
|
+
"organization": data.get("asnOrganization"),
|
|
286
|
+
"timezone": tzs[0] if len(tzs) > 0 else None
|
|
287
|
+
}
|
|
288
|
+
result["data"].update(geo_data)
|
|
289
|
+
return result
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return {"status": False, "error": str(e)}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
IPV6_API_MAP = {
|
|
295
|
+
IPv6API.IP_SB: {
|
|
296
|
+
"thread_safe": True,
|
|
297
|
+
"geo": True,
|
|
298
|
+
"function": _ip_sb_ipv6
|
|
299
|
+
},
|
|
300
|
+
IPv6API.IDENT_ME: {
|
|
301
|
+
"thread_safe": True,
|
|
302
|
+
"geo": True,
|
|
303
|
+
"function": _ident_me_ipv6
|
|
304
|
+
},
|
|
305
|
+
IPv6API.TNEDI_ME: {
|
|
306
|
+
"thread_safe": True,
|
|
307
|
+
"geo": True,
|
|
308
|
+
"function": _tnedi_me_ipv6
|
|
309
|
+
},
|
|
310
|
+
IPv6API.IPLEAK_NET: {
|
|
311
|
+
"thread_safe": True,
|
|
312
|
+
"geo": True,
|
|
313
|
+
"function": _ipleak_net_ipv6
|
|
314
|
+
},
|
|
315
|
+
IPv6API.MY_IP_IO: {
|
|
316
|
+
"thread_safe": True,
|
|
317
|
+
"geo": True,
|
|
318
|
+
"function": _my_ip_io_ipv6
|
|
319
|
+
},
|
|
320
|
+
IPv6API.IFCONFIG_CO: {
|
|
321
|
+
"thread_safe": False,
|
|
322
|
+
"geo": True,
|
|
323
|
+
"function": _ifconfig_co_ipv6
|
|
324
|
+
},
|
|
325
|
+
IPv6API.REALLYFREEGEOIP_ORG: {
|
|
326
|
+
"thread_safe": False,
|
|
327
|
+
"geo": True,
|
|
328
|
+
"function": _reallyfreegeoip_org_ipv6
|
|
329
|
+
},
|
|
330
|
+
IPv6API.MYIP_LA: {
|
|
331
|
+
"thread_safe": False,
|
|
332
|
+
"geo": True,
|
|
333
|
+
"function": _myip_la_ipv6
|
|
334
|
+
},
|
|
335
|
+
IPv6API.FREEIPAPI_COM: {
|
|
336
|
+
"thread_safe": False,
|
|
337
|
+
"geo": True,
|
|
338
|
+
"function": _freeipapi_com_ipv6
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_public_ipv6(api: IPv6API=IPv6API.AUTO_SAFE, geo: bool=False,
|
|
344
|
+
timeout: Union[float, Tuple[float, float]]=5,
|
|
345
|
+
max_retries: int = 0,
|
|
346
|
+
retry_delay: float = 1.0) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
347
|
+
"""
|
|
348
|
+
Get public IPv6 and geolocation info based on the selected API.
|
|
349
|
+
|
|
350
|
+
:param api: public IPv6 API
|
|
351
|
+
:param geo: geolocation flag
|
|
352
|
+
:param timeout: timeout value for API
|
|
353
|
+
:param max_retries: number of retries
|
|
354
|
+
:param retry_delay: delay between retries (in seconds)
|
|
355
|
+
"""
|
|
356
|
+
if api in [IPv6API.AUTO, IPv6API.AUTO_SAFE]:
|
|
357
|
+
for _, api_data in IPV6_API_MAP.items():
|
|
358
|
+
if api == IPv6API.AUTO_SAFE and not api_data["thread_safe"]:
|
|
359
|
+
continue
|
|
360
|
+
func = api_data["function"]
|
|
361
|
+
result = _attempt_with_retries(
|
|
362
|
+
func=func,
|
|
363
|
+
max_retries=max_retries,
|
|
364
|
+
retry_delay=retry_delay,
|
|
365
|
+
geo=geo,
|
|
366
|
+
timeout=timeout)
|
|
367
|
+
if result["status"]:
|
|
368
|
+
return result
|
|
369
|
+
return {"status": False, "error": "All attempts failed."}
|
|
370
|
+
else:
|
|
371
|
+
api_data = IPV6_API_MAP.get(api)
|
|
372
|
+
if api_data:
|
|
373
|
+
func = api_data["function"]
|
|
374
|
+
return _attempt_with_retries(
|
|
375
|
+
func=func,
|
|
376
|
+
max_retries=max_retries,
|
|
377
|
+
retry_delay=retry_delay,
|
|
378
|
+
geo=geo,
|
|
379
|
+
timeout=timeout)
|
|
380
|
+
return {"status": False, "error": "Unsupported API: {api}".format(api=api)}
|
ipspot/params.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""ipspot params."""
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
-
IPSPOT_VERSION = "0.
|
|
5
|
+
IPSPOT_VERSION = "0.6"
|
|
6
6
|
|
|
7
7
|
IPSPOT_OVERVIEW = '''
|
|
8
8
|
IPSpot is a Python library for retrieving the current system's IP address and location information.
|
|
@@ -35,6 +35,25 @@ class IPv4API(Enum):
|
|
|
35
35
|
REALLYFREEGEOIP_ORG = "reallyfreegeoip.org"
|
|
36
36
|
MYIP_LA = "myip.la"
|
|
37
37
|
FREEIPAPI_COM = "freeipapi.com"
|
|
38
|
+
IPQUERY_IO = "ipquery.io"
|
|
39
|
+
IPWHO_IS = "ipwho.is"
|
|
40
|
+
WTFISMYIP_COM = "wtfismyip.com"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IPv6API(Enum):
|
|
44
|
+
"""Public IPv6 API enum."""
|
|
45
|
+
|
|
46
|
+
AUTO = "auto"
|
|
47
|
+
AUTO_SAFE = "auto-safe"
|
|
48
|
+
IP_SB = "ip.sb"
|
|
49
|
+
IDENT_ME = "ident.me"
|
|
50
|
+
TNEDI_ME = "tnedi.me"
|
|
51
|
+
IPLEAK_NET = "ipleak.net"
|
|
52
|
+
MY_IP_IO = "my-ip.io"
|
|
53
|
+
IFCONFIG_CO = "ifconfig.co"
|
|
54
|
+
REALLYFREEGEOIP_ORG = "reallyfreegeoip.org"
|
|
55
|
+
MYIP_LA = "myip.la"
|
|
56
|
+
FREEIPAPI_COM = "freeipapi.com"
|
|
38
57
|
|
|
39
58
|
|
|
40
59
|
PARAMETERS_NAME_MAP = {
|
ipspot/utils.py
CHANGED
|
@@ -2,12 +2,75 @@
|
|
|
2
2
|
"""ipspot utils."""
|
|
3
3
|
import time
|
|
4
4
|
import ipaddress
|
|
5
|
+
import socket
|
|
5
6
|
import requests
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
6
8
|
from typing import Callable, Dict
|
|
7
|
-
from typing import Union, Tuple, Any
|
|
9
|
+
from typing import Union, Tuple, Any, List
|
|
8
10
|
from .params import REQUEST_HEADERS
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
class ForceIPHTTPAdapter(HTTPAdapter):
|
|
14
|
+
"""A custom HTTPAdapter that enforces IPv4 or IPv6 DNS resolution for HTTP(S) requests."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, version: str = "ipv4", *args: list, **kwargs: dict) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the adapter with the desired IP version.
|
|
19
|
+
|
|
20
|
+
:param version: 'ipv4' or 'ipv6' to select address family
|
|
21
|
+
:param args: additional list arguments for the HTTPAdapter
|
|
22
|
+
:param kwargs: additional keyword arguments for the HTTPAdapter
|
|
23
|
+
"""
|
|
24
|
+
self.version = version.lower()
|
|
25
|
+
if self.version not in ("ipv4", "ipv6"):
|
|
26
|
+
raise ValueError("version must be either 'ipv4' or 'ipv6'")
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
def send(self, *args: list, **kwargs: dict) -> Any:
|
|
30
|
+
"""
|
|
31
|
+
Override send method to apply the monkey patch only during the request.
|
|
32
|
+
|
|
33
|
+
:param args: additional list arguments for the send method
|
|
34
|
+
:param kwargs: additional keyword arguments for the send method
|
|
35
|
+
"""
|
|
36
|
+
family = socket.AF_INET if self.version == "ipv4" else socket.AF_INET6
|
|
37
|
+
original_getaddrinfo = socket.getaddrinfo
|
|
38
|
+
|
|
39
|
+
def filtered_getaddrinfo(*gargs: list, **gkwargs: dict) -> List[Tuple]:
|
|
40
|
+
"""
|
|
41
|
+
Filter getaddrinfo.
|
|
42
|
+
|
|
43
|
+
:param gargs: additional list arguments for the original_getaddrinfo function
|
|
44
|
+
:param gkwargs: additional keyword arguments for the original_getaddrinfo function
|
|
45
|
+
"""
|
|
46
|
+
results = original_getaddrinfo(*gargs, **gkwargs)
|
|
47
|
+
return [res for res in results if res[0] == family]
|
|
48
|
+
|
|
49
|
+
socket.getaddrinfo = filtered_getaddrinfo
|
|
50
|
+
try:
|
|
51
|
+
response = super().send(*args, **kwargs)
|
|
52
|
+
finally:
|
|
53
|
+
socket.getaddrinfo = original_getaddrinfo
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_json_force_ip(url: str, timeout: Union[float, Tuple[float, float]],
|
|
58
|
+
version: str = "ipv4") -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Send GET request with forced IPv4/IPv6 using ForceIPHTTPAdapter that returns JSON response.
|
|
61
|
+
|
|
62
|
+
:param url: API url
|
|
63
|
+
:param timeout: timeout value for API
|
|
64
|
+
:param version: 'ipv4' or 'ipv6' to select address family
|
|
65
|
+
"""
|
|
66
|
+
with requests.Session() as session:
|
|
67
|
+
session.mount("http://", ForceIPHTTPAdapter(version=version))
|
|
68
|
+
session.mount("https://", ForceIPHTTPAdapter(version=version))
|
|
69
|
+
response = session.get(url, headers=REQUEST_HEADERS, timeout=timeout)
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
return response.json()
|
|
72
|
+
|
|
73
|
+
|
|
11
74
|
def _attempt_with_retries(
|
|
12
75
|
func: Callable,
|
|
13
76
|
max_retries: int,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipspot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6
|
|
4
4
|
Summary: IPSpot: A Python Tool to Fetch the System's IP Address
|
|
5
5
|
Home-page: https://github.com/openscilab/ipspot
|
|
6
|
-
Download-URL: https://github.com/openscilab/ipspot/tarball/v0.
|
|
6
|
+
Download-URL: https://github.com/openscilab/ipspot/tarball/v0.6
|
|
7
7
|
Author: IPSpot Development Team
|
|
8
8
|
Author-email: ipspot@openscilab.com
|
|
9
9
|
License: MIT
|
|
10
10
|
Project-URL: Source, https://github.com/openscilab/ipspot
|
|
11
11
|
Keywords: ip ipv4 geo geolocation network location ipspot cli
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
@@ -52,6 +52,7 @@ Dynamic: summary
|
|
|
52
52
|
<img src="https://github.com/openscilab/ipspot/raw/main/otherfiles/logo.png" width="350">
|
|
53
53
|
<h1>IPSpot: A Python Tool to Fetch the System's IP Address</h1>
|
|
54
54
|
<br/>
|
|
55
|
+
<a href="https://codecov.io/gh/openscilab/ipspot"><img src="https://codecov.io/gh/openscilab/ipspot/graph/badge.svg?token=XCFKASULS8"></a>
|
|
55
56
|
<a href="https://badge.fury.io/py/ipspot"><img src="https://badge.fury.io/py/ipspot.svg" alt="PyPI version"></a>
|
|
56
57
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/built%20with-Python3-green.svg" alt="built with Python3"></a>
|
|
57
58
|
<a href="https://github.com/openscilab/ipspot"><img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/openscilab/ipspot"></a>
|
|
@@ -102,13 +103,13 @@ Dynamic: summary
|
|
|
102
103
|
## Installation
|
|
103
104
|
|
|
104
105
|
### Source Code
|
|
105
|
-
- Download [Version 0.
|
|
106
|
+
- Download [Version 0.6](https://github.com/openscilab/ipspot/archive/v0.6.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
106
107
|
- `pip install .`
|
|
107
108
|
|
|
108
109
|
### PyPI
|
|
109
110
|
|
|
110
111
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
111
|
-
- `pip install ipspot==0.
|
|
112
|
+
- `pip install ipspot==0.6`
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
## Usage
|
|
@@ -135,6 +136,26 @@ Dynamic: summary
|
|
|
135
136
|
{'status': True, 'data': {'ip': '10.36.18.154'}}
|
|
136
137
|
```
|
|
137
138
|
|
|
139
|
+
#### Public IPv6
|
|
140
|
+
|
|
141
|
+
```pycon
|
|
142
|
+
>>> from ipspot import get_public_ipv6, IPv6API
|
|
143
|
+
>>> get_public_ipv6(api=IPv6API.IP_SB)
|
|
144
|
+
{'data': {'api': 'ip.sb', 'ip': 'xx:xx:xx:xx::xx'}, 'status': True}
|
|
145
|
+
>>> get_public_ipv6(api=IPv6API.IP_SB, geo=True, timeout=10)
|
|
146
|
+
{'data': {'latitude': 51.2993, 'region': None, 'city': None, 'country_code': 'DE', 'api': 'ip.sb', 'longitude': 9.491, 'country': 'Germany', 'organization': 'Hetzner Online', 'timezone': 'Europe/Berlin', 'ip': 'xx:xx:xx:xx::xx'}, 'status': True}
|
|
147
|
+
>>> get_public_ipv6(api=IPv6API.IP_SB, geo=True, timeout=10, max_retries=5, retry_delay=4)
|
|
148
|
+
{'data': {'latitude': 51.2993, 'region': None, 'city': None, 'country_code': 'DE', 'api': 'ip.sb', 'longitude': 9.491, 'country': 'Germany', 'organization': 'Hetzner Online', 'timezone': 'Europe/Berlin', 'ip': 'xx:xx:xx:xx::xx'}, 'status': True}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Private IPv6
|
|
152
|
+
|
|
153
|
+
```pycon
|
|
154
|
+
>>> from ipspot import get_private_ipv6
|
|
155
|
+
>>> get_private_ipv6()
|
|
156
|
+
{'status': True, 'data': {'ip': 'fe80::e1bd:f78:b233:21c9'}}
|
|
157
|
+
```
|
|
158
|
+
|
|
138
159
|
### CLI
|
|
139
160
|
|
|
140
161
|
ℹ️ You can use `ipspot` or `python -m ipspot` to run this program
|
|
@@ -144,7 +165,7 @@ Dynamic: summary
|
|
|
144
165
|
```console
|
|
145
166
|
> ipspot --version
|
|
146
167
|
|
|
147
|
-
0.
|
|
168
|
+
0.6
|
|
148
169
|
```
|
|
149
170
|
|
|
150
171
|
#### Info
|
|
@@ -159,11 +180,11 @@ Dynamic: summary
|
|
|
159
180
|
|___||_| |____/ | .__/ \___/ \__|
|
|
160
181
|
|_|
|
|
161
182
|
|
|
162
|
-
__ __ ___
|
|
163
|
-
\ \ / / _ / _ \
|
|
164
|
-
\ \ / / (_)| | | | |
|
|
165
|
-
\ V / _ | |_| | _ |
|
|
166
|
-
\_/ (_) \___/ (_)
|
|
183
|
+
__ __ ___ __
|
|
184
|
+
\ \ / / _ / _ \ / /_
|
|
185
|
+
\ \ / / (_)| | | | | '_ \
|
|
186
|
+
\ V / _ | |_| | _ | (_) |
|
|
187
|
+
\_/ (_) \___/ (_) \___/
|
|
167
188
|
|
|
168
189
|
|
|
169
190
|
|
|
@@ -181,25 +202,42 @@ Repo : https://github.com/openscilab/ipspot
|
|
|
181
202
|
> ipspot
|
|
182
203
|
Private IP:
|
|
183
204
|
|
|
184
|
-
|
|
205
|
+
IPv4: 192.168.1.35
|
|
206
|
+
|
|
207
|
+
IPv6: fe80::e1bd:f78:b233:21c9
|
|
185
208
|
|
|
186
209
|
Public IP and Location Info:
|
|
187
210
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
IPv4:
|
|
212
|
+
|
|
213
|
+
API: ipinfo.io
|
|
214
|
+
City: Nuremberg
|
|
215
|
+
Country: Germany
|
|
216
|
+
Country Code: DE
|
|
217
|
+
IP: xx.xx.xx.xx
|
|
218
|
+
Latitude: 49.4527
|
|
219
|
+
Longitude: 11.0783
|
|
220
|
+
Organization: Hetzner Online GmbH
|
|
221
|
+
Region: Bavaria
|
|
222
|
+
Timezone: Europe/Berlin
|
|
223
|
+
|
|
224
|
+
IPv6:
|
|
225
|
+
|
|
226
|
+
API: ip.sb
|
|
227
|
+
City: N/A
|
|
228
|
+
Country: Germany
|
|
229
|
+
Country Code: DE
|
|
230
|
+
IP: xx:xx:xx:xx::xx
|
|
231
|
+
Latitude: 51.2993
|
|
232
|
+
Longitude: 9.491
|
|
233
|
+
Organization: Hetzner Online
|
|
234
|
+
Region: N/A
|
|
235
|
+
Timezone: Europe/Berlin
|
|
198
236
|
```
|
|
199
237
|
|
|
200
238
|
#### IPv4 API
|
|
201
239
|
|
|
202
|
-
ℹ️ `ipv4-api` valid choices: [`auto-safe`, `auto`, `ip-api.com`, `ipinfo.io`, `ip.sb`, `ident.me`, `tnedi.me`, `ipapi.co`, `ipleak.net`, `my-ip.io`, `ifconfig.co`, `reallyfreegeoip.org`, `freeipapi.com`, `myip.la`]
|
|
240
|
+
ℹ️ `ipv4-api` valid choices: [`auto-safe`, `auto`, `ip-api.com`, `ipinfo.io`, `ip.sb`, `ident.me`, `tnedi.me`, `ipapi.co`, `ipleak.net`, `my-ip.io`, `ifconfig.co`, `reallyfreegeoip.org`, `freeipapi.com`, `myip.la`, `ipquery.io`, `ipwho.is`, `wtfismyip.com`]
|
|
203
241
|
|
|
204
242
|
ℹ️ The default value: `auto-safe`
|
|
205
243
|
|
|
@@ -207,20 +245,80 @@ Public IP and Location Info:
|
|
|
207
245
|
> ipspot --ipv4-api="ipinfo.io"
|
|
208
246
|
Private IP:
|
|
209
247
|
|
|
210
|
-
|
|
248
|
+
IPv4: 192.168.1.35
|
|
249
|
+
|
|
250
|
+
IPv6: fe80::e1bd:f78:b233:21c9
|
|
251
|
+
|
|
252
|
+
Public IP and Location Info:
|
|
253
|
+
|
|
254
|
+
IPv4:
|
|
255
|
+
|
|
256
|
+
API: ipinfo.io
|
|
257
|
+
City: Nuremberg
|
|
258
|
+
Country: Germany
|
|
259
|
+
Country Code: DE
|
|
260
|
+
IP: xx.xx.xx.xx
|
|
261
|
+
Latitude: 49.4527
|
|
262
|
+
Longitude: 11.0783
|
|
263
|
+
Organization: Hetzner Online GmbH
|
|
264
|
+
Region: Bavaria
|
|
265
|
+
Timezone: Europe/Berlin
|
|
266
|
+
|
|
267
|
+
IPv6:
|
|
268
|
+
|
|
269
|
+
API: ip.sb
|
|
270
|
+
City: N/A
|
|
271
|
+
Country: Germany
|
|
272
|
+
Country Code: DE
|
|
273
|
+
IP: xx:xx:xx:xx::xx
|
|
274
|
+
Latitude: 51.2993
|
|
275
|
+
Longitude: 9.491
|
|
276
|
+
Organization: Hetzner Online
|
|
277
|
+
Region: N/A
|
|
278
|
+
Timezone: Europe/Berlin
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### IPv6 API
|
|
282
|
+
|
|
283
|
+
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`, `ifconfig.co`, `reallyfreegeoip.org`, `myip.la`, `freeipapi.com`]
|
|
284
|
+
|
|
285
|
+
ℹ️ The default value: `auto-safe`
|
|
286
|
+
|
|
287
|
+
```console
|
|
288
|
+
> ipspot --ipv6-api="ip.sb"
|
|
289
|
+
Private IP:
|
|
290
|
+
|
|
291
|
+
IPv4: 192.168.1.35
|
|
292
|
+
|
|
293
|
+
IPv6: fe80::e1bd:f78:b233:21c9
|
|
211
294
|
|
|
212
295
|
Public IP and Location Info:
|
|
213
296
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
297
|
+
IPv4:
|
|
298
|
+
|
|
299
|
+
API: ipinfo.io
|
|
300
|
+
City: Nuremberg
|
|
301
|
+
Country: Germany
|
|
302
|
+
Country Code: DE
|
|
303
|
+
IP: xx.xx.xx.xx
|
|
304
|
+
Latitude: 49.4527
|
|
305
|
+
Longitude: 11.0783
|
|
306
|
+
Organization: Hetzner Online GmbH
|
|
307
|
+
Region: Bavaria
|
|
308
|
+
Timezone: Europe/Berlin
|
|
309
|
+
|
|
310
|
+
IPv6:
|
|
311
|
+
|
|
312
|
+
API: ip.sb
|
|
313
|
+
City: N/A
|
|
314
|
+
Country: Germany
|
|
315
|
+
Country Code: DE
|
|
316
|
+
IP: xx:xx:xx:xx::xx
|
|
317
|
+
Latitude: 51.2993
|
|
318
|
+
Longitude: 9.491
|
|
319
|
+
Organization: Hetzner Online
|
|
320
|
+
Region: N/A
|
|
321
|
+
Timezone: Europe/Berlin
|
|
224
322
|
```
|
|
225
323
|
|
|
226
324
|
#### No Geolocation
|
|
@@ -229,12 +327,21 @@ Public IP and Location Info:
|
|
|
229
327
|
> ipspot --no-geo
|
|
230
328
|
Private IP:
|
|
231
329
|
|
|
232
|
-
|
|
330
|
+
IPv4: 192.168.1.35
|
|
331
|
+
|
|
332
|
+
IPv6: fe80::5c40:769f:22de:c196
|
|
233
333
|
|
|
234
334
|
Public IP:
|
|
235
335
|
|
|
236
|
-
|
|
237
|
-
|
|
336
|
+
IPv4:
|
|
337
|
+
|
|
338
|
+
API: tnedi.me
|
|
339
|
+
IP: xx.xx.xx.xx
|
|
340
|
+
|
|
341
|
+
IPv6:
|
|
342
|
+
|
|
343
|
+
API: ip.sb
|
|
344
|
+
IP: xx:xx:xx:xx::xx
|
|
238
345
|
```
|
|
239
346
|
|
|
240
347
|
## Issues & Bug Reports
|
|
@@ -269,6 +376,39 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
269
376
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
270
377
|
|
|
271
378
|
## [Unreleased]
|
|
379
|
+
## [0.6] - 2025-11-18
|
|
380
|
+
### Added
|
|
381
|
+
- `ForceIPHTTPAdapter` class
|
|
382
|
+
- `_get_json_force_ip` function
|
|
383
|
+
- Support [ifconfig.co](https://ifconfig.co/json) IPv6 API
|
|
384
|
+
- Support [reallyfreegeoip.org](https://reallyfreegeoip.org/json/) IPv6 API
|
|
385
|
+
- Support [myip.la](https://api.myip.la/en?json) IPv6 API
|
|
386
|
+
- Support [freeipapi.com](https://freeipapi.com/api/json) IPv6 API
|
|
387
|
+
### Changed
|
|
388
|
+
- [freeipapi.com](https://freeipapi.com/api/json) IPv4 API bug fixed
|
|
389
|
+
- `README.md` updated
|
|
390
|
+
### Removed
|
|
391
|
+
- `IPv4HTTPAdapter` class
|
|
392
|
+
- `_get_json_ipv4_forced` function
|
|
393
|
+
## [0.5] - 2025-10-17
|
|
394
|
+
### Added
|
|
395
|
+
- `setup-warp` action
|
|
396
|
+
- Support [ipwho.is](https://ipwho.is/)
|
|
397
|
+
- Support [ipquery.io](http://api.ipquery.io/?format=json)
|
|
398
|
+
- Support [wtfismyip.com](https://wtfismyip.com/json)
|
|
399
|
+
- Support [ident.me](https://ident.me/json) IPv6 API
|
|
400
|
+
- Support [tnedi.me](https://tnedi.me/json) IPv6 API
|
|
401
|
+
- Support [ip.sb](https://api.ip.sb/geoip) IPv6 API
|
|
402
|
+
- Support [ipleak.net](https://ipleak.net/json/) IPv6 API
|
|
403
|
+
- Support [my-ip.io](https://www.my-ip.io/) IPv6 API
|
|
404
|
+
- `is_ipv6` function
|
|
405
|
+
- `get_private_ipv6` function
|
|
406
|
+
- `get_public_ipv6` function
|
|
407
|
+
- `IPv6API` enum
|
|
408
|
+
- `--ipv6-api` argument
|
|
409
|
+
### Changed
|
|
410
|
+
- Test system modified
|
|
411
|
+
- `README.md` updated
|
|
272
412
|
## [0.4] - 2025-06-09
|
|
273
413
|
### Added
|
|
274
414
|
- Support [ipapi.co](https://ipapi.co/json/)
|
|
@@ -328,7 +468,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
328
468
|
- `--no-geo` argument
|
|
329
469
|
- Logo
|
|
330
470
|
|
|
331
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
471
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.6...dev
|
|
472
|
+
[0.6]: https://github.com/openscilab/ipspot/compare/v0.5...v0.6
|
|
473
|
+
[0.5]: https://github.com/openscilab/ipspot/compare/v0.4...v0.5
|
|
332
474
|
[0.4]: https://github.com/openscilab/ipspot/compare/v0.3...v0.4
|
|
333
475
|
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
334
476
|
[0.2]: https://github.com/openscilab/ipspot/compare/v0.1...v0.2
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ipspot/__init__.py,sha256=B47uAOAidLkXps1A4zDig6NN2kB3HcdMC20UOrrEfB4,281
|
|
2
|
+
ipspot/__main__.py,sha256=xKHY_tc94SWktkIHV0O9YADAHPZvT8yXOFSm_L2yMno,105
|
|
3
|
+
ipspot/cli.py,sha256=MdyMh-9miuT8zQgklVrvsimaXnqD17sviWRJbUWlk58,4637
|
|
4
|
+
ipspot/ipv4.py,sha256=ZTv4HFyyxpiF8X-N8Pgk9_aJ7f9T58SpuuB3UKS0WII,21571
|
|
5
|
+
ipspot/ipv6.py,sha256=zqP_ImJQeKpVIhmGQZmPLJsOuviUNnVg70tVhk4UQU0,13991
|
|
6
|
+
ipspot/params.py,sha256=UqhiD8ybTnxRpecZ9RudKXxXyD-j63z23b6Cqbv1LJs,1881
|
|
7
|
+
ipspot/utils.py,sha256=wZxz6e14aDqK0g27AdiRMrbiz8pk4UfxZvbFRDZzhSI,4445
|
|
8
|
+
ipspot-0.6.dist-info/licenses/AUTHORS.md,sha256=5ZvxP1KnuVkurB4psQDSqnqiAyn44q1aeiXUsnYgps4,411
|
|
9
|
+
ipspot-0.6.dist-info/licenses/LICENSE,sha256=0aOd4wzZRoSH_35UZXRHS7alPFTtuFEBJrajLuonEIw,1067
|
|
10
|
+
ipspot-0.6.dist-info/METADATA,sha256=tYTL9nqZZP5YhOzV0IE2Y0CkUAytt-7b_dXWAdUCO08,14879
|
|
11
|
+
ipspot-0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
ipspot-0.6.dist-info/entry_points.txt,sha256=DJVLepYr8H3UcvWekU5Jy-tcr_xmWrphzgWGNOd3hlk,43
|
|
13
|
+
ipspot-0.6.dist-info/top_level.txt,sha256=v0WgE1z2iCy_bXU53fVcllwHLTvGNBIvq8u3KPC2_Sc,7
|
|
14
|
+
ipspot-0.6.dist-info/RECORD,,
|
ipspot-0.4.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
ipspot/__init__.py,sha256=e_1FqqNDD9eoDVw9eKL_1pS7du-Sei3lm6A4UFjYiUo,211
|
|
2
|
-
ipspot/__main__.py,sha256=xKHY_tc94SWktkIHV0O9YADAHPZvT8yXOFSm_L2yMno,105
|
|
3
|
-
ipspot/cli.py,sha256=M23l9ftqKbdDGhb906q4ywJmcjkHW3VWijyXj76APC4,3222
|
|
4
|
-
ipspot/ipv4.py,sha256=zGFvzD2GeM2lFxMWc6lcCphBaj5qY0kxiHE0FBhDoTc,19998
|
|
5
|
-
ipspot/params.py,sha256=hdArSL10EEq4vtfxA51ftCkn5WfsAzsxdoX1T11yCdQ,1419
|
|
6
|
-
ipspot/utils.py,sha256=YtkyWGljpFc72rSGljsARjlVrF9hkE7Gl78_5uex1yI,1870
|
|
7
|
-
ipspot-0.4.dist-info/licenses/AUTHORS.md,sha256=5ZvxP1KnuVkurB4psQDSqnqiAyn44q1aeiXUsnYgps4,411
|
|
8
|
-
ipspot-0.4.dist-info/licenses/LICENSE,sha256=0aOd4wzZRoSH_35UZXRHS7alPFTtuFEBJrajLuonEIw,1067
|
|
9
|
-
ipspot-0.4.dist-info/METADATA,sha256=8D5MqjjxrCDU60BDPikNoX75iE5bMKVMQX_kFw03WiU,10875
|
|
10
|
-
ipspot-0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
ipspot-0.4.dist-info/entry_points.txt,sha256=DJVLepYr8H3UcvWekU5Jy-tcr_xmWrphzgWGNOd3hlk,43
|
|
12
|
-
ipspot-0.4.dist-info/top_level.txt,sha256=v0WgE1z2iCy_bXU53fVcllwHLTvGNBIvq8u3KPC2_Sc,7
|
|
13
|
-
ipspot-0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|