diff --git a/CHANGELOG.md b/CHANGELOG.md index df37289..eabb6c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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 [ipapi.co](https://ipapi.co/json/) ## [0.3] - 2025-05-19 ### Added - `is_ipv4` function diff --git a/README.md b/README.md index 71ee288..51c895d 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Public IP and Location Info: #### IPv4 API -ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`, `identme`, `tnedime`] +ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`, `identme`, `tnedime`, `ipapi_co`] ℹ️ The default value: `auto` diff --git a/ipspot/ipv4.py b/ipspot/ipv4.py index a07c751..d235bef 100644 --- a/ipspot/ipv4.py +++ b/ipspot/ipv4.py @@ -112,6 +112,39 @@ def _ipsb_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]] return {"status": False, "error": str(e)} +def _ipapi_co_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 ipapi.co. + + :param geo: geolocation flag + :param timeout: timeout value for API + """ + try: + with requests.Session() as session: + session.mount("http://", IPv4HTTPAdapter()) + session.mount("https://", IPv4HTTPAdapter()) + response = session.get("https://ipapi.co/json/", headers=REQUEST_HEADERS, timeout=timeout) + response.raise_for_status() + data = response.json() + result = {"status": True, "data": {"ip": data.get("ip"), "api": "ipapi.co"}} + if geo: + geo_data = { + "city": data.get("city"), + "region": data.get("region"), + "country": data.get("country_name"), + "country_code": data.get("country_code"), + "latitude": data.get("latitude"), + "longitude": data.get("longitude"), + "organization": data.get("org"), + "timezone": data.get("timezone") + } + result["data"].update(geo_data) + return result + except Exception as e: + return {"status": False, "error": str(e)} + + def _ipapi_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]] =5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]: """ @@ -258,6 +291,7 @@ def get_public_ipv4(api: IPv4API=IPv4API.AUTO, geo: bool=False, IPv4API.IPSB: _ipsb_ipv4, IPv4API.IPAPI: _ipapi_ipv4, IPv4API.IPINFO: _ipinfo_ipv4, + IPv4API.IPAPI_CO: _ipapi_co_ipv4 } if api == IPv4API.AUTO: diff --git a/ipspot/params.py b/ipspot/params.py index bd88cc6..5dfe0e3 100644 --- a/ipspot/params.py +++ b/ipspot/params.py @@ -23,6 +23,7 @@ class IPv4API(Enum): AUTO = "auto" IPAPI = "ipapi" + IPAPI_CO = "ipapi_co" IPINFO = "ipinfo" IPSB = "ipsb" IDENTME = "identme" diff --git a/tests/test_ipv4.py b/tests/test_ipv4.py index 4323830..493c874 100644 --- a/tests/test_ipv4.py +++ b/tests/test_ipv4.py @@ -93,6 +93,25 @@ def test_public_ipv4_auto_net_error(): assert not result["status"] assert result["error"] == "All attempts failed." +def test_public_ipv4_ipapi_co_success(): + result = get_public_ipv4(api=IPv4API.IPAPI_CO, geo=True) + assert result["status"] + assert is_ipv4(result["data"]["ip"]) + assert set(result["data"].keys()) == DATA_ITEMS + assert result["data"]["api"] == "ipapi.co" + + +def test_public_ipv4_ipapi_co_timeout_error(): + result = get_public_ipv4(api=IPv4API.IPAPI_CO, geo=True, timeout="5") + assert not result["status"] + + +def test_public_ipv4_ipapi_co_net_error(): + with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")): + result = get_public_ipv4(api=IPv4API.IPAPI_CO) + assert not result["status"] + assert result["error"] == "No Internet" + def test_public_ipv4_ipapi_success(): result = get_public_ipv4(api=IPv4API.IPAPI, geo=True)