Skip to content

Commit

Permalink
0.5.0 (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
NeonDaniel authored Feb 27, 2024
2 parents 34d0da4 + 39ddf44 commit 6bda638
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 18 deletions.
22 changes: 20 additions & 2 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
uses: neongeckocom/.github/.github/workflows/docker_build_tests.yml@master
unit_tests:
strategy:
max-parallel: 1
matrix:
python-version: [3.7, 3.8, 3.9, '3.10']
runs-on: ubuntu-latest
Expand All @@ -29,8 +30,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements/requirements.txt
pip install -r requirements/test_requirements.txt
pip install . -r requirements/test_requirements.txt
- name: Export credentials
run: |
mkdir -p ~/.local/share/neon
Expand All @@ -43,6 +43,13 @@ jobs:
AV_API_KEY: ${{secrets.alpha_vantage_key}}
OWM_KEY: ${{secrets.open_weather_map_key}}
GENERIC_CONTROLLER_CONFIG: ${{secrets.generic_controller_config}}

- name: Test Client
run: |
pytest tests/test_client.py --doctest-modules --junitxml=tests/client-test-results.xml
env:
MAP_MAKER_KEY: ${{secrets.map_maker_key}}

- name: Test Cached API
run: |
pytest tests/test_cached_api.py --doctest-modules --junitxml=tests/cached-api-test-results.xml
Expand Down Expand Up @@ -79,6 +86,17 @@ jobs:
name: owm-api-test-results
path: tests/owm-api-test-results.xml

- name: Test Map Maker API
run: |
pytest tests/test_map_maker_api.py --doctest-modules --junitxml=tests/map-maker-api-test-results.xml
env:
MAP_MAKER_KEY: ${{secrets.map_maker_key}}
- name: Upload Map Maker API test results
uses: actions/upload-artifact@v2
with:
name: map-maker-api-test-results
path: tests/map-maker-api-test-results.xml

- name: Test Generic API
run: |
pytest tests/test_generic_controller.py --doctest-modules --junitxml=tests/generic-controller-test-results.xml
Expand Down
12 changes: 6 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Changelog

## [0.4.2a1](https://github.com/NeonGeckoCom/neon_api_proxy/tree/0.4.2a1) (2023-07-04)
## [0.4.3a2](https://github.com/NeonGeckoCom/neon_api_proxy/tree/0.4.3a2) (2024-02-26)

[Full Changelog](https://github.com/NeonGeckoCom/neon_api_proxy/compare/0.4.1a1...0.4.2a1)
[Full Changelog](https://github.com/NeonGeckoCom/neon_api_proxy/compare/0.4.3a1...0.4.3a2)

**Merged pull requests:**

- Fix unit test automation to run on PR to `master` [\#86](https://github.com/NeonGeckoCom/neon_api_proxy/pull/86) ([NeonDaniel](https://github.com/NeonDaniel))
- Update geolocation tests [\#91](https://github.com/NeonGeckoCom/neon_api_proxy/pull/91) ([NeonDaniel](https://github.com/NeonDaniel))

## [0.4.1a1](https://github.com/NeonGeckoCom/neon_api_proxy/tree/0.4.1a1) (2023-07-04)
## [0.4.3a1](https://github.com/NeonGeckoCom/neon_api_proxy/tree/0.4.3a1) (2023-12-28)

[Full Changelog](https://github.com/NeonGeckoCom/neon_api_proxy/compare/0.4.0...0.4.1a1)
[Full Changelog](https://github.com/NeonGeckoCom/neon_api_proxy/compare/0.4.2...0.4.3a1)

**Merged pull requests:**

- Add docker publication to release workflow [\#84](https://github.com/NeonGeckoCom/neon_api_proxy/pull/84) ([NeonDaniel](https://github.com/NeonDaniel))
- Add Map Maker API with unit tests [\#88](https://github.com/NeonGeckoCom/neon_api_proxy/pull/88) ([NeonDaniel](https://github.com/NeonDaniel))



Expand Down
1 change: 1 addition & 0 deletions neon_api_proxy/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __str__(self):
ALPHA_VANTAGE = "alpha_vantage"
OPEN_WEATHER_MAP = "open_weather_map"
WOLFRAM_ALPHA = "wolfram_alpha"
MAP_MAKER = "map_maker"
FINANCIAL_MODELING_PREP = "financial_modeling_prep"
NOT_IMPLEMENTED = "not_implemented"
TEST_API = "api_test_endpoint"
Expand Down
72 changes: 72 additions & 0 deletions neon_api_proxy/client/map_maker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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_coordinates(location: str) -> (float, float):
"""
Get coordinates for the requested location
@param location: Search term, i.e. City, Address, Landmark
@returns: coordinate latitude, longitude
"""
resp = _make_api_call({'address': location})
if resp['status_code'] != 200:
raise RuntimeError(f"API Request failed: {resp['content']}")
coords = resp['content'][0]['lat'], resp['content'][0]['lon']
LOG.info(f"Resolved: {coords}")
return float(coords[0]), float(coords[1])


def get_address(lat: float, lon: float) -> dict:
"""
Get a dict location for the specified coordinates
@param lat: latitude of point to look up
@param lon: longitude of point to look up
@returns: dict location (equivalent to Geopy Location.raw)
"""
resp = _make_api_call({'lat': lat, "lon": lon})
if resp['status_code'] != 200:
raise RuntimeError(f"API Request failed: {resp['content']}")
address = resp['content']['address']
if not address.get('city'):
LOG.debug(f"Response missing city, trying to find alternate tag in: "
f"{address.keys()}")
address['city'] = address.get('town') or address.get('village')
LOG.info(f"Resolved: {address}")
return address


def _make_api_call(request_data: dict) -> dict:
resp = request_api(NeonAPI.MAP_MAKER, request_data)
if resp['status_code'] == 200:
resp['content'] = json.loads(resp['content'])
return resp
17 changes: 10 additions & 7 deletions neon_api_proxy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from ovos_config.config import Configuration
from neon_utils.configuration_utils import NGIConfig
from ovos_config.locations import get_xdg_config_save_path

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
from neon_api_proxy.services.wolfram_api import WolframAPI
Expand All @@ -47,6 +49,7 @@ class NeonAPIProxyController:
'wolfram_alpha': WolframAPI,
'alpha_vantage': AlphaVantageAPI,
'open_weather_map': OpenWeatherAPI,
'map_maker': MapMakerAPI,
'api_test_endpoint': TestAPI
}

Expand All @@ -63,7 +66,7 @@ def _init_config() -> dict:
from neon_api_proxy.config import get_proxy_config
legacy_config = get_proxy_config()
if legacy_config:
return legacy_config
return legacy_config.get("SERVICES") or legacy_config
legacy_config_file = join(get_xdg_config_save_path(),
"ngi_auth_vars.yml")
if isfile(legacy_config_file):
Expand All @@ -82,16 +85,16 @@ def init_service_instances(self, service_class_mapping: dict) -> dict:
and instance of python class representing it
"""
service_mapping = dict()
for item in list(service_class_mapping):
api_key = self.config.get("SERVICES",
self.config).get(item,
{}).get("api_key") \
if self.config else None
for item in service_class_mapping:
api_key = self.config.get(item, {}).get("api_key") if self.config \
else None
try:
if api_key is None:
LOG.warning(f"No API key for {item} in {self.config}")
service_mapping[item] = \
service_class_mapping[item](api_key=api_key)
except Exception as e:
LOG.info(e)
LOG.error(e)
return service_mapping

def resolve_query(self, query: dict) -> dict:
Expand Down
109 changes: 109 additions & 0 deletions neon_api_proxy/services/map_maker_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 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 os import getenv
from time import time, sleep
from requests import Response
from ovos_utils.log import LOG

from neon_api_proxy.cached_api import CachedAPI


class MapMakerAPI(CachedAPI):
"""
API for querying My Maps API (geocoder.maps.co).
"""

def __init__(self, api_key: str = None, cache_seconds=604800): # Cache week
super().__init__("map_maker")
self._api_key = api_key or getenv("MAP_MAKER_KEY")
if not self._api_key:
raise RuntimeError(f"No API key provided for Map Maker")
self._rate_limit_seconds = 1
self._last_query = time()
self.cache_timeout = timedelta(seconds=cache_seconds)
self.geocode_url = "https://geocode.maps.co/search"
self.reverse_url = "https://geocode.maps.co/reverse"

def handle_query(self, **kwargs) -> dict:
"""
Handles an incoming query and provides a response
:param kwargs:
'lat' - optional str latitude
'lon' - optional str longitude
'address' - optional string address/place to resolve
:return: dict containing `status_code`, `content`, `encoding`
from URL response
"""
lat = kwargs.get("lat")
lon = kwargs.get("lon", kwargs.get("lng"))
address = kwargs.get('address')

if not (address or (lat and lon)):
# Missing data for lookup
return {"status_code": -1,
"content": f"Incomplete request data: {kwargs}",
"encoding": None}

if self._rate_limit_seconds:
sleep_time = round(self._rate_limit_seconds -
(time() - self._last_query), 3)
if sleep_time > 0:
LOG.info(f"Waiting {sleep_time}s before next API query")
sleep(sleep_time)

if lat and lon:
# Lookup address for coordinates
try:
response = self._query_reverse(float(lat), float(lon))
except ValueError as e:
return {"status_code": -1,
"content": repr(e),
"encoding": None}
else:
# Lookup coordinates for search term/address
response = self._query_geocode(address)
self._last_query = time()
return {"status_code": response.status_code,
"content": response.content,
"encoding": response.encoding}

def _query_geocode(self, address: str) -> Response:
query_str = urllib.parse.urlencode({"q": address,
"api_key": self._api_key})
request_url = f"{self.geocode_url}?{query_str}"
return self.get_with_cache_timeout(request_url, self.cache_timeout)

def _query_reverse(self, lat: float, lon: float):
query_str = urllib.parse.urlencode({"lat": lat, "lon": lon,
"api_key": self._api_key})
request_url = f"{self.reverse_url}?{query_str}"
return self.get_with_cache_timeout(request_url, self.cache_timeout)
41 changes: 39 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,28 @@ def test_request_neon_api_invalid_type_params(self):


class NeonAPIClientTests(unittest.TestCase):
map_maker_key = None

@classmethod
def setUpClass(cls) -> None:
def _override_find_key(*args, **kwargs):
raise Exception
raise Exception("Test Exception; no key found")
import neon_utils.authentication_utils
neon_utils.authentication_utils.find_generic_keyfile = _override_find_key

if os.getenv("MAP_MAKER_KEY"):
cls.map_maker_key = os.environ.pop("MAP_MAKER_KEY")

@classmethod
def tearDownClass(cls) -> None:
if cls.map_maker_key:
os.environ["MAP_MAKER_KEY"] = cls.map_maker_key

def test_client_init_no_keys(self):
from neon_api_proxy.client import NeonAPIProxyClient
client = NeonAPIProxyClient({"test": "test"})
self.assertEqual(set(client.service_instance_mapping.keys()), {"api_test_endpoint"})
self.assertEqual(set(client.service_instance_mapping.keys()),
{"api_test_endpoint"})

def test_client_lazy_load(self):
from neon_api_proxy.client import NeonAPI, request_api
Expand Down Expand Up @@ -294,3 +305,29 @@ def test_get_forecast_no_api_key(self):
self.assertIsInstance(data["minutely"], list)
self.assertIsInstance(data["hourly"], list)
self.assertIsInstance(data["daily"], list)


class MapMakerTests(unittest.TestCase):
def test_get_coordinates(self):
from neon_api_proxy.client.map_maker import get_coordinates

# Valid request
lat, lon = get_coordinates("Kirkland")
self.assertIsInstance(lat, float)
self.assertIsInstance(lon, float)

# Invalid request
with self.assertRaises(RuntimeError):
get_coordinates("")

def test_get_address(self):
from neon_api_proxy.client.map_maker import get_address

# Valid Request
address = get_address(VALID_LAT, VALID_LNG)
self.assertEqual(address['state'], "Washington")
self.assertEqual(address['city'], "Renton", address)

# Invalid Request
with self.assertRaises(RuntimeError):
get_address('', '')
Loading

0 comments on commit 6bda638

Please sign in to comment.