From 9e8713c5d46ff59cf9f187692acc6428c0e7ed88 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 9 Jun 2025 22:14:14 -0400 Subject: [PATCH 1/6] `_ipwho_is_ipv4` implemented and added to `IPv4API` dict --- ipspot/ipv4.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ipspot/ipv4.py b/ipspot/ipv4.py index 820e173..d53a201 100644 --- a/ipspot/ipv4.py +++ b/ipspot/ipv4.py @@ -436,6 +436,36 @@ def _freeipapi_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, floa return {"status": False, "error": str(e)} +def _ipwho_is_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5 + ) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]: + """ + Get public IP and geolocation using ipwho.is. + + :param geo: geolocation flag + :param timeout: timeout value for API + """ + try: + data = _get_json_standard(url="https://ipwho.is", timeout=timeout) + result = {"status": True, "data": {"ip": data["ip"], "api": "ipwho.is"}} + if geo: + connection = data.get("connection", {}) + timezone = data.get("timezone", {}) + geo_data = { + "city": data.get("city"), + "region": data.get("region"), + "country": data.get("country"), + "country_code": data.get("country_code"), + "latitude": data.get("latitude"), + "longitude": data.get("longitude"), + "organization": connection.get("org"), + "timezone": timezone.get("id") + } + result["data"].update(geo_data) + return result + except Exception as e: + return {"status": False, "error": str(e)} + + IPV4_API_MAP = { IPv4API.IFCONFIG_CO: { "thread_safe": False, @@ -497,6 +527,11 @@ def _freeipapi_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, floa "geo": True, "function": _myip_la_ipv4, }, + IPv4API.IPWHO_IS: { + "thread_safe": False, + "geo": True, + "function": _ipwho_is_ipv4, + }, } From b274df062c27d3d772264a229dc6084486bd4cea Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 9 Jun 2025 22:14:26 -0400 Subject: [PATCH 2/6] add `IPWHO_IS` to `IPv4API` enum --- ipspot/params.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ipspot/params.py b/ipspot/params.py index 5cc2cf9..d8be9a9 100644 --- a/ipspot/params.py +++ b/ipspot/params.py @@ -35,6 +35,7 @@ class IPv4API(Enum): REALLYFREEGEOIP_ORG = "reallyfreegeoip.org" MYIP_LA = "myip.la" FREEIPAPI_COM = "freeipapi.com" + IPWHO_IS = "ipwho.is" PARAMETERS_NAME_MAP = { From d6713fa83728ade6a5a7aa9e705550f5d6d0f47b Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 9 Jun 2025 22:14:57 -0400 Subject: [PATCH 3/6] add testcases for `ipwho.is` --- tests/test_ipv4.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_ipv4.py b/tests/test_ipv4.py index f9d8056..43a44ee 100644 --- a/tests/test_ipv4.py +++ b/tests/test_ipv4.py @@ -211,6 +211,26 @@ def test_public_ipv4_myip_la_net_error(): result = get_public_ipv4(api=IPv4API.MYIP_LA) +def test_public_ipv4_ipwho_is_success(): + result = get_public_ipv4(api=IPv4API.IPWHO_IS, geo=True) + assert result["status"] + assert is_ipv4(result["data"]["ip"]) + assert set(result["data"].keys()) == DATA_ITEMS + assert result["data"]["api"] == "ipwho.is" + + +def test_public_ipv4_ipwho_is_timeout_error(): + result = get_public_ipv4(api=IPv4API.IPWHO_IS, geo=True, timeout="5") + assert not result["status"] + + +def test_public_ipv4_ipwho_is_net_error(): + with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")): + result = get_public_ipv4(api=IPv4API.IPWHO_IS) + assert not result["status"] + assert result["error"] == "No Internet" + + def test_public_ipv4_freeipapi_com_success(): result = get_public_ipv4(api=IPv4API.FREEIPAPI_COM, geo=True) assert result["status"] From 73dbd7cdcf3bfb07160f57134fe733287e975c52 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 9 Jun 2025 22:15:03 -0400 Subject: [PATCH 4/6] `README.md` updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c393e2d..4c5e373 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Public IP and Location Info: #### IPv4 API -ℹ️ `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`] +ℹ️ `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`, `ipwho.is`] ℹ️ The default value: `auto-safe` From 857f80c49dc76cd987ad247096346254ad3c675c Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 9 Jun 2025 22:15:07 -0400 Subject: [PATCH 5/6] `CHANGELOG.md` updated --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 268343a..5335bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Support [ipwho.is](https://ipwho.is/) +### Changed ## [0.4] - 2025-06-09 ### Added - Support [ipapi.co](https://ipapi.co/json/) From f3b5076336ebd84b2b807a87d5027982c08ca9b2 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Tue, 10 Jun 2025 09:15:28 -0400 Subject: [PATCH 6/6] use `_get_json_ipv4_forced` --- ipspot/ipv4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipspot/ipv4.py b/ipspot/ipv4.py index d53a201..9f5552f 100644 --- a/ipspot/ipv4.py +++ b/ipspot/ipv4.py @@ -445,7 +445,7 @@ def _ipwho_is_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5 :param timeout: timeout value for API """ try: - data = _get_json_standard(url="https://ipwho.is", timeout=timeout) + data = _get_json_ipv4_forced(url="https://ipwho.is", timeout=timeout) result = {"status": True, "data": {"ip": data["ip"], "api": "ipwho.is"}} if geo: connection = data.get("connection", {})