ipspot 0.5__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/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:
@@ -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,8 @@ 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
432
373
  }
433
374
  result["data"].update(geo_data)
434
375
  return result
@@ -445,7 +386,7 @@ def _ipquery_io_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
445
386
  :param timeout: timeout value for API
446
387
  """
447
388
  try:
448
- data = _get_json_ipv4_forced(url="https://api.ipquery.io/?format=json", timeout=timeout)
389
+ data = _get_json_force_ip(url="https://api.ipquery.io/?format=json", timeout=timeout, version="ipv4")
449
390
  result = {"status": True, "data": {"ip": data["ip"], "api": "ipquery.io"}}
450
391
  if geo:
451
392
  loc = data.get("location", {})
@@ -475,7 +416,7 @@ def _ipwho_is_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
475
416
  :param timeout: timeout value for API
476
417
  """
477
418
  try:
478
- data = _get_json_ipv4_forced(url="https://ipwho.is", timeout=timeout)
419
+ data = _get_json_force_ip(url="https://ipwho.is", timeout=timeout, version="ipv4")
479
420
  result = {"status": True, "data": {"ip": data["ip"], "api": "ipwho.is"}}
480
421
  if geo:
481
422
  connection = data.get("connection", {})
ipspot/ipv6.py CHANGED
@@ -4,7 +4,8 @@ import ipaddress
4
4
  import socket
5
5
  from typing import Union, Dict, List, Tuple
6
6
  from .params import IPv6API
7
- from .utils import is_loopback, _get_json_standard, _attempt_with_retries
7
+ from .utils import is_loopback, _attempt_with_retries
8
+ from .utils import _get_json_standard, _get_json_force_ip
8
9
 
9
10
 
10
11
  def is_ipv6(ip: str) -> bool:
@@ -176,6 +177,120 @@ def _my_ip_io_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
176
177
  return {"status": False, "error": str(e)}
177
178
 
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
+
179
294
  IPV6_API_MAP = {
180
295
  IPv6API.IP_SB: {
181
296
  "thread_safe": True,
@@ -202,6 +317,26 @@ IPV6_API_MAP = {
202
317
  "geo": True,
203
318
  "function": _my_ip_io_ipv6
204
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
+ }
205
340
  }
206
341
 
207
342
 
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"
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.
@@ -50,6 +50,10 @@ class IPv6API(Enum):
50
50
  TNEDI_ME = "tnedi.me"
51
51
  IPLEAK_NET = "ipleak.net"
52
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"
53
57
 
54
58
 
55
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.5
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.5
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 :: 3 - 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.5](https://github.com/openscilab/ipspot/archive/v0.5.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.5`
112
+ - `pip install ipspot==0.6`
112
113
 
113
114
 
114
115
  ## Usage
@@ -164,7 +165,7 @@ Dynamic: summary
164
165
  ```console
165
166
  > ipspot --version
166
167
 
167
- 0.5
168
+ 0.6
168
169
  ```
169
170
 
170
171
  #### Info
@@ -179,11 +180,11 @@ Dynamic: summary
179
180
  |___||_| |____/ | .__/ \___/ \__|
180
181
  |_|
181
182
 
182
- __ __ ___ ____
183
- \ \ / / _ / _ \ | ___|
184
- \ \ / / (_)| | | | |___ \
185
- \ V / _ | |_| | _ ___) |
186
- \_/ (_) \___/ (_)|____/
183
+ __ __ ___ __
184
+ \ \ / / _ / _ \ / /_
185
+ \ \ / / (_)| | | | | '_ \
186
+ \ V / _ | |_| | _ | (_) |
187
+ \_/ (_) \___/ (_) \___/
187
188
 
188
189
 
189
190
 
@@ -207,16 +208,31 @@ Private IP:
207
208
 
208
209
  Public IP and Location Info:
209
210
 
210
- API: ip-api.com
211
- City: Southampton
212
- Country: United Kingdom
213
- Country Code: GB
214
- IP: xx.xx.xx.xx
215
- Latitude: 50.9097
216
- Longitude: -1.4043
217
- Organization: N/A
218
- Region: England
219
- 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
220
236
  ```
221
237
 
222
238
  #### IPv4 API
@@ -264,7 +280,7 @@ Public IP and Location Info:
264
280
 
265
281
  #### IPv6 API
266
282
 
267
- ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`]
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`]
268
284
 
269
285
  ℹ️ The default value: `auto-safe`
270
286
 
@@ -360,6 +376,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
360
376
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
361
377
 
362
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
363
393
  ## [0.5] - 2025-10-17
364
394
  ### Added
365
395
  - `setup-warp` action
@@ -438,7 +468,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
438
468
  - `--no-geo` argument
439
469
  - Logo
440
470
 
441
- [Unreleased]: https://github.com/openscilab/ipspot/compare/v0.5...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
442
473
  [0.5]: https://github.com/openscilab/ipspot/compare/v0.4...v0.5
443
474
  [0.4]: https://github.com/openscilab/ipspot/compare/v0.3...v0.4
444
475
  [0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
@@ -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,14 +0,0 @@
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=-HGv-yAsFI56I06qNFDquVVGP9ZhfWwih1Vs3JtemZQ,23825
5
- ipspot/ipv6.py,sha256=svGgydUUSoXeuT2hLgi5CDDy3n2PtV-b6uDvP5t_iKI,8754
6
- ipspot/params.py,sha256=PVnz3F1jB5aIle_fScGoF-0MfREhMxcTCXGEXmimUQs,1741
7
- ipspot/utils.py,sha256=YtkyWGljpFc72rSGljsARjlVrF9hkE7Gl78_5uex1yI,1870
8
- ipspot-0.5.dist-info/licenses/AUTHORS.md,sha256=5ZvxP1KnuVkurB4psQDSqnqiAyn44q1aeiXUsnYgps4,411
9
- ipspot-0.5.dist-info/licenses/LICENSE,sha256=0aOd4wzZRoSH_35UZXRHS7alPFTtuFEBJrajLuonEIw,1067
10
- ipspot-0.5.dist-info/METADATA,sha256=Sbzs3OQ0QZH22X8bNrcLxWbYb1zKBtKwbPf9AVAEon0,13824
11
- ipspot-0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- ipspot-0.5.dist-info/entry_points.txt,sha256=DJVLepYr8H3UcvWekU5Jy-tcr_xmWrphzgWGNOd3hlk,43
13
- ipspot-0.5.dist-info/top_level.txt,sha256=v0WgE1z2iCy_bXU53fVcllwHLTvGNBIvq8u3KPC2_Sc,7
14
- ipspot-0.5.dist-info/RECORD,,
File without changes