Skip to content

Commit

Permalink
Initial support for ip-api geolocation
Browse files Browse the repository at this point in the history
Needs paid API support before use with Neon services
  • Loading branch information
NeonDaniel committed May 14, 2024
1 parent 95038a9 commit c2e60e7
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 1 deletion.
1 change: 1 addition & 0 deletions neon_api_proxy/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __str__(self):
WOLFRAM_ALPHA = "wolfram_alpha"
MAP_MAKER = "map_maker"
FINANCIAL_MODELING_PREP = "financial_modeling_prep"
IP_API = "ip_api"
NOT_IMPLEMENTED = "not_implemented"
TEST_API = "api_test_endpoint"

Expand Down
48 changes: 48 additions & 0 deletions neon_api_proxy/client/ip_geolocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2022 Neongecko.com Inc.
# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds,
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
# BSD-3 License
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import json

from ovos_utils.log import LOG
from neon_api_proxy.client import NeonAPI, request_api


def get_location(ip_addr: str) -> dict:
"""
Get a dict location for the requested public IP Address
@param ip_addr: Public IP address to geolocate
@returns: dict location
"""
resp = request_api(NeonAPI.IP_API, {"ip": ip_addr})
if resp['status_code'] == 200:
resp['content'] = json.loads(resp['content'])
if resp['content']['status'] != "success":
resp['status_code'] = 500
LOG.warning(f"Got failure response: {resp}")
LOG.info(f"Got location: {resp['content']}")
return resp['content']
5 changes: 4 additions & 1 deletion neon_api_proxy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from neon_utils.configuration_utils import NGIConfig
from ovos_config.locations import get_xdg_config_save_path

from neon_api_proxy.services.ip_geolocation_api import IpGeolocationAPI
from neon_api_proxy.services.map_maker_api import MapMakerAPI
from neon_api_proxy.services.owm_api import OpenWeatherAPI
from neon_api_proxy.services.alpha_vantage_api import AlphaVantageAPI
Expand All @@ -50,6 +51,7 @@ class NeonAPIProxyController:
'alpha_vantage': AlphaVantageAPI,
'open_weather_map': OpenWeatherAPI,
'map_maker': MapMakerAPI,
"ip_api": IpGeolocationAPI,
'api_test_endpoint': TestAPI
}

Expand Down Expand Up @@ -89,7 +91,8 @@ def init_service_instances(self, service_class_mapping: dict) -> dict:
api_key = self.config.get(item, {}).get("api_key") if self.config \
else None
try:
if api_key is None and item != 'api_test_endpoint':
if api_key is None and item not in ('api_test_endpoint',
"ip_api"):
LOG.warning(f"No API key for {item} in "
f"{list(self.config.keys())}")
service_mapping[item] = \
Expand Down
70 changes: 70 additions & 0 deletions neon_api_proxy/services/ip_geolocation_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2022 Neongecko.com Inc.
# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds,
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
# BSD-3 License
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import urllib.parse

from datetime import timedelta
from time import time, sleep
from requests import Response
from ovos_utils.log import LOG

from neon_api_proxy.cached_api import CachedAPI


class IpGeolocationAPI(CachedAPI):
"""
API for querying IP Geolocation (ip-api.com).
"""

def __init__(self, api_key=None, cache_seconds=604800): # Cache week
super().__init__("ip_api")
self.cache_timeout = timedelta(seconds=cache_seconds)
if api_key:
self.geolocate_api = "https://members.ip-api.com/json/"
else:
self.geolocate_api = "http://ip-api.com/json/"

def handle_query(self, **kwargs) -> dict:
"""
Handles an incoming query and provides a response
:param kwargs:
'ip' - IP Address to location
:return: dict containing `status_code`, `content`, `encoding`
from URL response
"""
ip = kwargs.get('ip')
try:
# TODO: Build URL with API key
response = self.get_with_cache_timeout(f"{self.geolocate_api}{ip}")
return {"status_code": response.status_code,
"content": response.content,
"encoding": response.encoding}
except Exception as e:
return {"status_code": 500,
"content": repr(e),
"encoding": None}
23 changes: 23 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,26 @@ def test_get_address(self):
# Invalid Request
with self.assertRaises(RuntimeError):
get_address('', '')


class IpGeolocationTests(unittest.TestCase):
def test_geolocate(self):
from neon_api_proxy.client.ip_geolocation import get_location
WA_IP = '50.47.222.4'
DO_IP = '146.190.53.128'
INVALID_IP = '10.0.0.1'

wa_loc = get_location(WA_IP)
ca_loc = get_location(DO_IP)
invalid = get_location(INVALID_IP)

self.assertEqual(wa_loc['status'], 'success')
self.assertAlmostEqual(wa_loc['lat'], 48.0, 0)
self.assertAlmostEqual(wa_loc['lon'], -122.0, 0)

self.assertEqual(ca_loc['status'], 'success')
self.assertAlmostEqual(ca_loc['lat'], 37.0, 0)
self.assertAlmostEqual(ca_loc['lon'], -122.0, 0)

self.assertEqual(invalid['status'], 'fail')
self.assertIsInstance(invalid['message'], str, invalid)
77 changes: 77 additions & 0 deletions tests/test_ip_geolocation_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2022 Neongecko.com Inc.
# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds,
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
# BSD-3 License
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import json
import os
import sys
import unittest

from requests import Response

sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
from neon_api_proxy.services.ip_geolocation_api import IpGeolocationAPI

WA_IP = '50.47.222.4'
DO_IP = '146.190.53.128'
INVALID_IP = '10.0.0.1'


class TestIpGeolocationAPI(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.api = IpGeolocationAPI()

def test_lookup(self):
resp = self.api.handle_query(ip=WA_IP)
self.assertEqual(resp['status_code'], 200, resp)
location = json.loads(resp['content'].decode(resp['encoding']))
self.assertEqual(location['query'], WA_IP)
self.assertEqual(location['country'], 'United States')
self.assertEqual(location['regionName'], 'Washington')
self.assertEqual(location['timezone'], 'America/Los_Angeles')
self.assertAlmostEqual(location['lat'], 48.0, 0)
self.assertAlmostEqual(location['lon'], -122.0, 0)

resp = self.api.handle_query(ip=DO_IP)
self.assertEqual(resp['status_code'], 200, resp)
location = json.loads(resp['content'].decode(resp['encoding']))
self.assertEqual(location['query'], DO_IP)
self.assertEqual(location['country'], 'United States')
self.assertEqual(location['regionName'], 'California')
self.assertEqual(location['timezone'], 'America/Los_Angeles')
self.assertAlmostEqual(location['lat'], 37.0, 0)
self.assertAlmostEqual(location['lon'], -122.0, 0)

resp = self.api.handle_query(ip=INVALID_IP)
self.assertEqual(resp['status_code'], 200, resp)
location = json.loads(resp['content'].decode(resp['encoding']))
self.assertEqual(location['status'], 'fail')


if __name__ == '__main__':
unittest.main()

0 comments on commit c2e60e7

Please sign in to comment.