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 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, geo: bool=False,
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
- print(" IP: {private_result[data][ip]}".format(private_result=private_result) if private_result["status"]
35
- else " Error: {private_result[error]}".format(private_result=private_result))
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
- public_result = get_public_ipv4(
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 public_result["status"]:
49
- for name, parameter in sorted(public_result["data"].items()):
63
+ if public_ipv4_result["status"]:
64
+ for name, parameter in sorted(public_ipv4_result["data"].items()):
50
65
  print(
51
- " {name}: {parameter}".format(
66
+ " {name}: {parameter}".format(
52
67
  name=PARAMETERS_NAME_MAP[name],
53
68
  parameter=_filter_parameter(parameter)))
54
69
  else:
55
- print(" Error: {public_result[error]}".format(public_result=public_result))
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 requests
7
- from requests.adapters import HTTPAdapter
8
- from urllib3.poolmanager import PoolManager
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 = _get_json_ipv4_forced(url="https://ifconfig.co/json", timeout=timeout)
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 = _get_json_ipv4_forced(url="https://ipapi.co/json/", timeout=timeout)
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 = _get_json_ipv4_forced(url="http://ip-api.com/json/", timeout=timeout)
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 = _get_json_ipv4_forced(url="https://ipinfo.io/json", timeout=timeout)
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 = _get_json_ipv4_forced(url="https://reallyfreegeoip.org/json/", timeout=timeout)
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 = _get_json_ipv4_forced(url="https://api.myip.la/en?json", timeout=timeout)
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 = _get_json_ipv4_forced(url="https://freeipapi.com/api/json", timeout=timeout)
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": None,
431
- "timezone": data.get("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.4"
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.4
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.4
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 :: 2 - Pre-Alpha
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.4](https://github.com/openscilab/ipspot/archive/v0.4.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
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.4`
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.4
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
- 10.36.18.154
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
- API: ip-api.com
189
- City: Southampton
190
- Country: United Kingdom
191
- Country Code: GB
192
- IP: xx.xx.xx.xx
193
- Latitude: 50.9097
194
- Longitude: -1.4043
195
- Organization: N/A
196
- Region: England
197
- Timezone: Europe/London
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
- 10.36.18.154
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
- API: ipinfo.io
215
- City: Leatherhead
216
- Country: N/A
217
- Country Code: GB
218
- IP: xx.xx.xx.xx
219
- Latitude: 51.2965
220
- Longitude: -0.3338
221
- Organization: AS212238 Datacamp Limited
222
- Region: England
223
- Timezone: Europe/London
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
- IP: 10.36.18.154
330
+ IPv4: 192.168.1.35
331
+
332
+ IPv6: fe80::5c40:769f:22de:c196
233
333
 
234
334
  Public IP:
235
335
 
236
- API: ipinfo.io
237
- IP: xx.xx.xx.xx
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.4...dev
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,,
@@ -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