diff --git a/CHANGELOG.md b/CHANGELOG.md index f0204786..76377528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,76 @@ # Changelog -## [1.9.2a3](https://github.com/NeonGeckoCom/neon-utils/tree/1.9.2a3) (2024-04-04) +## [1.10.2a9](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a9) (2024-06-25) -[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.9.2a2...1.9.2a3) +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a8...1.10.2a9) **Merged pull requests:** -- Add ovos-core 0.0.8 compat. for CommonQuery skills [\#508](https://github.com/NeonGeckoCom/neon-utils/pull/508) ([NeonDaniel](https://github.com/NeonDaniel)) +- Fix bug causing old token to be used with requests after refreshing [\#530](https://github.com/NeonGeckoCom/neon-utils/pull/530) ([NeonDaniel](https://github.com/NeonDaniel)) -## [1.9.2a2](https://github.com/NeonGeckoCom/neon-utils/tree/1.9.2a2) (2024-04-02) +## [1.10.2a8](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a8) (2024-06-17) -[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.9.2a1...1.9.2a2) +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a7...1.10.2a8) **Merged pull requests:** -- Loosen ovos-utils dependency to allow 0.1 [\#507](https://github.com/NeonGeckoCom/neon-utils/pull/507) ([NeonDaniel](https://github.com/NeonDaniel)) +- Mitigate issues with expired HANA tokens [\#529](https://github.com/NeonGeckoCom/neon-utils/pull/529) ([NeonDaniel](https://github.com/NeonDaniel)) -## [1.9.2a1](https://github.com/NeonGeckoCom/neon-utils/tree/1.9.2a1) (2024-03-07) +## [1.10.2a7](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a7) (2024-05-31) -[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.9.1...1.9.2a1) +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a6...1.10.2a7) **Merged pull requests:** -- Mark `neon_must_respond` method as deprecated [\#505](https://github.com/NeonGeckoCom/neon-utils/pull/505) ([NeonDaniel](https://github.com/NeonDaniel)) +- Refactor to use pytz instead of pendulum [\#527](https://github.com/NeonGeckoCom/neon-utils/pull/527) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [1.10.2a6](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a6) (2024-05-31) + +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a5...1.10.2a6) + +**Merged pull requests:** + +- Resolve Test Failures [\#528](https://github.com/NeonGeckoCom/neon-utils/pull/528) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [1.10.2a5](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a5) (2024-05-21) + +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a4...1.10.2a5) + +**Merged pull requests:** + +- Refactor to use HANA to send email instead of MQ [\#526](https://github.com/NeonGeckoCom/neon-utils/pull/526) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [1.10.2a4](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a4) (2024-05-11) + +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a3...1.10.2a4) + +**Merged pull requests:** + +- Update deprecated import to supported path [\#524](https://github.com/NeonGeckoCom/neon-utils/pull/524) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [1.10.2a3](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a3) (2024-05-10) + +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a2...1.10.2a3) + +**Merged pull requests:** + +- Fix ovos-core 0.0.8 Common Query compat. [\#523](https://github.com/NeonGeckoCom/neon-utils/pull/523) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [1.10.2a2](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a2) (2024-05-08) + +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.2a1...1.10.2a2) + +**Merged pull requests:** + +- Remove old patch [\#522](https://github.com/NeonGeckoCom/neon-utils/pull/522) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [1.10.2a1](https://github.com/NeonGeckoCom/neon-utils/tree/1.10.2a1) (2024-05-07) + +[Full Changelog](https://github.com/NeonGeckoCom/neon-utils/compare/1.10.1...1.10.2a1) + +**Merged pull requests:** + +- Enable `hana` backend URL to be configured [\#521](https://github.com/NeonGeckoCom/neon-utils/pull/521) ([NeonDaniel](https://github.com/NeonDaniel)) diff --git a/neon_utils/hana_utils.py b/neon_utils/hana_utils.py index a6e5850a..b4d16666 100644 --- a/neon_utils/hana_utils.py +++ b/neon_utils/hana_utils.py @@ -29,19 +29,39 @@ import requests import json +from typing import Optional from os import makedirs from os.path import join, isfile, isdir, dirname from time import time from ovos_utils.log import LOG from ovos_utils.xdg_utils import xdg_cache_home -_DEFAULT_BACKEND_URL = "https://hana.neonaiservices.com" +_DEFAULT_BACKEND_URL = None _client_config = {} _headers = {} -def _get_client_config_path(url: str = _DEFAULT_BACKEND_URL): - url_key = hash(url) +def set_default_backend_url(url: Optional[str] = None): + """ + Set the default backend URL + @param url: HANA backend url to use, else read from configuration + """ + global _DEFAULT_BACKEND_URL + if not url: + from ovos_config.config import Configuration + url = Configuration().get('hana', {}).get('url') or \ + "https://hana.neonaiservices.com" + if url and url != _DEFAULT_BACKEND_URL: + LOG.info(f"Updating HANA backend URL to {url}") + _DEFAULT_BACKEND_URL = url + global _client_config + global _headers + _client_config = {} + _headers = {} + + +def _get_client_config_path(url: str): + url_key = url.split('/')[2] return join(xdg_cache_home(), "neon", f"hana_token_{url_key}.json") @@ -112,6 +132,11 @@ def _refresh_token(backend_address: str): raise ServerException(f"Error updating token from {backend_address}. " f"{update.status_code}: {update.text}") _client_config = update.json() + + # Update request headers with new token + global _headers + _headers['Authorization'] = f"Bearer {_client_config['access_token']}" + client_config_path = _get_client_config_path(backend_address) with open(client_config_path, "w+") as f: json.dump(_client_config, f, indent=2) @@ -126,16 +151,45 @@ def request_backend(endpoint: str, request_data: dict, @param server_url: Base URL of Hana server to query @returns: dict response """ + global _client_config + global _headers + if not server_url: + set_default_backend_url() + server_url = _DEFAULT_BACKEND_URL + if server_url != _DEFAULT_BACKEND_URL and _client_config: + LOG.info(f"Using new remote: {server_url}") + _client_config = {} + _headers = {} _init_client(server_url) - if time() >= _client_config.get("expiration", 0): + if _client_config.get("expiration", 0) - time() < 30: try: _refresh_token(server_url) except ServerException as e: LOG.error(e) _get_token(server_url) - resp = requests.post(f"{server_url}/{endpoint.lstrip('/')}", - json=request_data, headers=_headers) + request_kwargs = {"url": f"{server_url}/{endpoint.lstrip('/')}", + "json": request_data, "headers": _headers} + resp = requests.post(**request_kwargs) + if resp.status_code == 502: + # This is raised occasionally on valid requests. Need to resolve in HANA + resp = requests.post(**request_kwargs) if resp.ok: return resp.json() + else: + try: + error = resp.json()["detail"] + # Token is actually expired, refresh and retry + if error == "Invalid or expired token.": + LOG.warning(f"Token is expired. time={time()}|" + f"expiration={_client_config.get('expiration')}") + _refresh_token(server_url) + resp = requests.post(**request_kwargs) + if resp.ok: + return resp.json() + except Exception as e: + LOG.error(e) + # Clear cached config to force re-evaluation on next request + _client_config = {} + _headers = {} raise ServerException(f"Error response {resp.status_code}: {resp.text}") diff --git a/neon_utils/location_utils.py b/neon_utils/location_utils.py index 0234030f..84716078 100644 --- a/neon_utils/location_utils.py +++ b/neon_utils/location_utils.py @@ -25,18 +25,19 @@ # 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. +from time import time -import pendulum +import pytz from datetime import datetime from typing import Optional, Union + from dateutil.tz import tzlocal -from geopy.exc import GeocoderServiceError -from geopy.geocoders import Nominatim from timezonefinder import TimezoneFinder from re import sub from ovos_utils.log import LOG +from neon_utils.hana_utils import request_backend # geocode.maps.co nominatim.openstreetmap.org _NOMINATIM_DOMAIN = "nominatim.openstreetmap.org" @@ -63,22 +64,30 @@ def get_full_location(address: Union[str, tuple], None if service is not available """ try: - nominatim = Nominatim(user_agent="neon-ai", domain=_NOMINATIM_DOMAIN, - timeout=10) if isinstance(address, str): - location = nominatim.geocode(address, addressdetails=True, - language=lang) + response = request_backend("proxy/geolocation/geocode", + {"address": address}) + coords = (response.get('lat'), response.get('lon')) else: - location = nominatim.reverse(address, addressdetails=True, - language=lang) + coords = address - dict_location = location.raw + dict_location = request_backend("proxy/geolocation/reverse", + {"lat": coords[0], "lon": coords[1]}) dict_location['address']['country'] = sub(f'[0-9]', '', dict_location['address']. get('country')) + if lang: + try: + # TODO: make this optional with a deprecation notice + from geopy.geocoders import Nominatim + resp = Nominatim(user_agent="neon-ai", domain=_NOMINATIM_DOMAIN, + timeout=10).reverse(coords, language=lang) + return resp.raw + except ImportError: + LOG.error("geopy not installed") + except Exception as e: + LOG.exception(e) return dict_location - except GeocoderServiceError as e: - LOG.error(e) except Exception as e: LOG.exception(e) return None @@ -90,14 +99,14 @@ def get_coordinates(gps_loc: dict) -> (float, float): :param gps_loc: dict of "city", "state", "country" :return: lat, lng float values """ - coordinates = Nominatim(user_agent="neon-ai", domain=_NOMINATIM_DOMAIN, - timeout=10) try: - location = coordinates.geocode(gps_loc) + request_str = ', '.join((x for x in [gps_loc.get('city'), + gps_loc.get('state'), + gps_loc.get('country')] if x)) + location = request_backend("proxy/geolocation/geocode", + {"address": request_str}) LOG.debug(f"{location}") - return location.latitude, location.longitude - except GeocoderServiceError as e: - LOG.error(e) + return float(location.get('lat')), float(location.get('lon')) except Exception as x: LOG.exception(x) return -1, -1 @@ -112,24 +121,21 @@ def get_location(lat, lng) -> (str, str, str, str): :return: city, county, state, country """ try: - address = Nominatim(user_agent="neon-ai", domain=_NOMINATIM_DOMAIN, - timeout=10) - except GeocoderServiceError as e: - LOG.error(e) - return None + location = request_backend("proxy/geolocation/reverse", + {"lat": lat, "lon": lng}) + + dict_location = location.get('address') except Exception as x: LOG.exception(x) return None - location = address.reverse([lat, lng], language="en-US") LOG.debug(f"{location}") - LOG.debug(f"{location.raw}") - LOG.debug(f"{location.raw.get('address')}") - city = location.raw.get('address').get('city') or \ - location.raw.get('address').get('town') or \ - location.raw.get('address').get('village') - county = location.raw.get('address').get('county') - state = location.raw.get('address').get('state') - country = location.raw.get('address').get('country') + city = dict_location.get('city') or \ + dict_location.get('town') or \ + dict_location.get('village') or \ + dict_location.get('hamlet') + county = dict_location.get('county') + state = dict_location.get('state') + country = dict_location.get('country') return city, county, state, country @@ -139,10 +145,11 @@ def get_timezone(lat, lng) -> (str, float): Note that some coordinates do not have a city, but may have a county. :param lat: latitude :param lng: longitude - :return: timezone name, offset from GMT + :return: timezone name, offset in hours from UTC """ timezone = TimezoneFinder().timezone_at(lng=float(lng), lat=float(lat)) - offset = pendulum.from_timestamp(0, timezone).offset_hours + offset = pytz.timezone(timezone).utcoffset( + datetime.now()).total_seconds() / 3600 return timezone, offset diff --git a/neon_utils/metrics_utils.py b/neon_utils/metrics_utils.py index 4fc53b8e..4cd0e231 100644 --- a/neon_utils/metrics_utils.py +++ b/neon_utils/metrics_utils.py @@ -67,7 +67,11 @@ def start(self): self.start_time = time() def stop(self): - self.time = time() - self.start_time + try: + self.time = time() - self.start_time + except TypeError: + LOG.error("stop called before start!") + self.time = None return self.time def report(self): diff --git a/neon_utils/mq_utils.py b/neon_utils/mq_utils.py index 73d874da..a598d74f 100644 --- a/neon_utils/mq_utils.py +++ b/neon_utils/mq_utils.py @@ -29,8 +29,10 @@ import logging import uuid -from ovos_utils.log import deprecated +from ovos_utils.log import deprecated, log_deprecation +log_deprecation("This module has moved to neon_mq_connector.utils.client_utils", + "2.0.0") try: from threading import Event from pika.channel import Channel @@ -47,7 +49,7 @@ logging.getLogger("pika").setLevel(logging.CRITICAL) _default_mq_config = { - "server": "api.neon.ai", + "server": "mq.neonaiservices.com", "port": 5672, "users": { "mq_handler": { @@ -60,6 +62,8 @@ class NeonMQHandler(MQConnector): def __init__(self, config: dict, service_name: str, vhost: str): + log_deprecation("Import from neon_mq_connector.utils.client_utils", + "2.0.0") super().__init__(config, service_name) self.vhost = vhost import pika @@ -70,13 +74,11 @@ def __init__(self, config: dict, service_name: str, vhost: str): @deprecated("Use `neon_mq_connector.client.send_mq_request`", "2.0.0") def get_mq_response(vhost: str, request_data: dict, target_queue: str, response_queue: str = None, timeout: int = 30) -> dict: - # TODO: Remove in v1.0.0 DM return send_mq_request(vhost, request_data, target_queue, response_queue, timeout, True) -# TODO: Mark deprecation after stable neon_mq_connector release -# @deprecated("Use `neon_mq_connector.client.send_mq_request`", "2.0.0") +@deprecated("Import from neon_mq_connector.utils.client_utils", "2.0.0") def send_mq_request(vhost: str, request_data: dict, target_queue: str, response_queue: str = None, timeout: int = 30, expect_response: bool = True) -> dict: diff --git a/neon_utils/skills/common_play_skill.py b/neon_utils/skills/common_play_skill.py index 495af6c6..9cf9d89e 100644 --- a/neon_utils/skills/common_play_skill.py +++ b/neon_utils/skills/common_play_skill.py @@ -48,7 +48,7 @@ from neon_utils.skills.neon_skill import NeonSkill -from ovos_utils.skills.audioservice import AudioServiceInterface as AudioService +from ovos_bus_client.apis.ocp import ClassicAudioServiceInterface as AudioService class CPSMatchLevel(Enum): diff --git a/neon_utils/skills/common_query_skill.py b/neon_utils/skills/common_query_skill.py index d51c6fe5..5aa97725 100644 --- a/neon_utils/skills/common_query_skill.py +++ b/neon_utils/skills/common_query_skill.py @@ -65,7 +65,6 @@ def handles_visuals(platform): return platform in VISUAL_DEVICES -# TODO: Consider deprecation and implementing ovos_workshop directly class CommonQuerySkill(NeonSkill, _CQS): """Question answering skills should be based on this class. @@ -168,6 +167,8 @@ def __handle_query_action(self, message): data = message.data.get("callback_data") or {} if data.get("answer"): self.speak(data["answer"]) + else: + LOG.error(f"no answer provided in: {message.data.keys()}") # Invoke derived class to provide playback data self.CQS_action(phrase, data) self.bus.emit(message.forward("mycroft.skill.handler.complete", @@ -191,6 +192,7 @@ def __handle_question_query(self, message): level = result[1] answer = result[2] callback = result[3] if len(result) > 3 else None + callback["answer"] = answer confidence = self.__calc_confidence(match, search_phrase, level) self.bus.emit(message.response({"phrase": search_phrase, "skill_id": self.skill_id, diff --git a/neon_utils/skills/kiosk_skill.py b/neon_utils/skills/kiosk_skill.py index ac8b9793..16247556 100644 --- a/neon_utils/skills/kiosk_skill.py +++ b/neon_utils/skills/kiosk_skill.py @@ -198,8 +198,7 @@ def stop(self): if user in self._active_users: self.end_interaction(message) - def _on_event_error(self, error, message, handler_info, - skill_data, speak_errors): + def _on_event_error(self, error, message, *args, **kwargs): """ Override error handling to speak custom exception for active sessions """ @@ -208,5 +207,4 @@ def _on_event_error(self, error, message, handler_info, LOG.exception(error) self.handle_error(message) else: - super()._on_event_error(error, message, handler_info, - skill_data, speak_errors) + super()._on_event_error(error, message, *args, **kwargs) diff --git a/neon_utils/skills/neon_fallback_skill.py b/neon_utils/skills/neon_fallback_skill.py index cd46e833..6becaed8 100644 --- a/neon_utils/skills/neon_fallback_skill.py +++ b/neon_utils/skills/neon_fallback_skill.py @@ -44,7 +44,6 @@ from ovos_utils.gui import is_gui_connected from ovos_utils.log import LOG, log_deprecation, deprecated from ovos_utils.skills import get_non_properties -from ovos_utils.skills.settings import save_settings from ovos_utils.xdg_utils import xdg_cache_home from ovos_workshop.skills import OVOSSkill from ovos_workshop.skills.fallback import FallbackSkillV1 @@ -53,7 +52,8 @@ from neon_utils.file_utils import resolve_neon_resource_file from neon_utils.location_utils import to_system_time from neon_utils.message_utils import dig_for_message, resolve_message, get_message_user -from neon_utils.skills.neon_skill import CACHE_TIME_OFFSET, DEFAULT_SPEED_MODE, SPEED_MODE_EXTENSION_TIME +from neon_utils.skills.neon_skill import CACHE_TIME_OFFSET, DEFAULT_SPEED_MODE, SPEED_MODE_EXTENSION_TIME, NeonSkill, \ + save_settings from neon_utils.user_utils import get_user_prefs @@ -875,23 +875,3 @@ def init_dialog(self, root_directory: Optional[str] = None): """ log_deprecation("Use `load_dialog_files`", "2.0.0") self.load_dialog_files(root_directory) - - def add_event(self, name: str, handler: callable, - handler_info: Optional[str] = None, once: bool = False, - speak_errors: bool = True): - # TODO: Remove with ovos-workshop==0.0.13 - try: - # Patching FakeBus compat. with MessageBusClient - if hasattr(self.bus, "ee"): - emitter = self.bus.ee - else: - emitter = self.bus.emitter - if handler_info == "mycroft.skill.handler" and \ - emitter.listeners(name): - LOG.warning(f"Not re-registering intent handler {name}") - return - except Exception as e: - LOG.exception(e) - OVOSSkill.add_event(self, name, handler, handler_info, once, - speak_errors) - diff --git a/neon_utils/skills/neon_skill.py b/neon_utils/skills/neon_skill.py index 572f72f9..4bf7d7e6 100644 --- a/neon_utils/skills/neon_skill.py +++ b/neon_utils/skills/neon_skill.py @@ -44,7 +44,6 @@ from ovos_utils.gui import is_gui_connected from ovos_utils.skills import get_non_properties from ovos_utils.xdg_utils import xdg_cache_home -from ovos_utils.skills.settings import save_settings, get_local_settings from ovos_utils.log import deprecated, log_deprecation from neon_utils.location_utils import to_system_time from neon_utils.logger import LOG @@ -52,14 +51,9 @@ from neon_utils.cache_utils import LRUCache from neon_utils.file_utils import resolve_neon_resource_file from neon_utils.user_utils import get_user_prefs +from neon_utils.hana_utils import request_backend, ServerException from ovos_workshop.skills.ovos import OVOSSkill -try: - from neon_utils.mq_utils import send_mq_request -except ImportError: - LOG.warning("MQ Dependencies not installed") - send_mq_request = None - try: from ovos_plugin_manager.language import OVOSLangDetectionFactory, \ OVOSLangTranslationFactory @@ -77,6 +71,25 @@ CACHE_TIME_OFFSET = 24*60*60 # seconds in 24 hours +@deprecated("deprecated without replacement, skill settings no longer shipped in skill folder", "2.0.0") +def save_settings(skill_dir, skill_settings): + """Save skill settings to file.""" + if skill_dir.endswith("/settings.json"): + settings_path = skill_dir + else: + settings_path = os.path.join(skill_dir, 'settings.json') + + settings = JsonStorage(settings_path) + for k, v in skill_settings.items(): + settings[k] = v + try: + settings.store() + except Exception: + LOG.exception(f'error saving skill settings to {settings_path}') + else: + LOG.info(f'Skill settings successfully saved to {settings_path}') + + class NeonSkill(OVOSSkill): def __init__(self, name=None, bus=None, **kwargs): OVOSSkill.__init__(self, name, bus, **kwargs) @@ -363,15 +376,18 @@ def send_email(self, title, body, message=None, email_addr=None, if not email_addr and message: email_addr = get_user_prefs(message)["user"].get("email") - if email_addr and send_mq_request: + if email_addr: LOG.info("Send email via Neon Server") request_data = {"recipient": email_addr, "subject": title, "body": body, "attachments": attachments} - data = send_mq_request("/neon_emails", request_data, - "neon_emails_input") - return data.get("success") + try: + request_backend("/email", request_data) + return True + except ServerException as e: + LOG.error(e) + return False else: LOG.warning("Attempting to send email via Mycroft Backend") super().send_email(title, body) @@ -877,22 +893,3 @@ def init_dialog(self, root_directory: Optional[str] = None): """ log_deprecation("Use `load_dialog_files`", "2.0.0") self.load_dialog_files(root_directory) - - def add_event(self, name: str, handler: callable, - handler_info: Optional[str] = None, once: bool = False, - speak_errors: bool = True): - # TODO: Remove with ovos-workshop==0.0.13 - try: - # Patching FakeBus compat. with MessageBusClient - if hasattr(self.bus, "ee"): - emitter = self.bus.ee - else: - emitter = self.bus.emitter - if handler_info == "mycroft.skill.handler" and \ - emitter.listeners(name): - LOG.warning(f"Not re-registering intent handler {name}") - return - except Exception as e: - LOG.exception(e) - OVOSSkill.add_event(self, name, handler, handler_info, once, - speak_errors) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8399f642..0082c8f2 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ ovos-bus-client~=0.0.3 combo-lock~=0.2 -pendulum~=3.0.0 +pytz>=2022.1 timezonefinder~=5.2 nltk~=3.5 pyyaml>=5.4,<7.0 diff --git a/tests/hana_util_tests.py b/tests/hana_util_tests.py index 6eb81778..2944c046 100644 --- a/tests/hana_util_tests.py +++ b/tests/hana_util_tests.py @@ -25,10 +25,13 @@ # 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 unittest + from os import remove from os.path import join, dirname, isfile +from time import time from unittest.mock import patch @@ -70,6 +73,28 @@ def test_request_backend(self, config_path): self.assertIsInstance(resp['answer'], str) # TODO: Test invalid route, invalid request data + @patch("neon_utils.hana_utils._get_client_config_path") + @patch("neon_utils.hana_utils._refresh_token") + def test_request_backend_refresh_token(self, refresh_token, config_path): + config_path.return_value = self.test_path + + import neon_utils.hana_utils + from neon_utils.hana_utils import request_backend + neon_utils.hana_utils.set_default_backend_url(self.test_server) + neon_utils.hana_utils._init_client(self.test_server) + real_client_config = neon_utils.hana_utils._client_config + neon_utils.hana_utils._client_config['expiration'] = time() + 29 + neon_utils.hana_utils._refresh_token = refresh_token + resp = request_backend("/neon/get_response", + {"lang_code": "en-us", + "utterance": "how are you", + "user_profile": {}}, self.test_server) + self.assertEqual(resp['lang_code'], "en-us") + self.assertIsInstance(resp['answer'], str) + refresh_token.assert_called_once_with(self.test_server) + + neon_utils.hana_utils._client_config = real_client_config + @patch("neon_utils.hana_utils._get_client_config_path") def test_00_get_token(self, config_path): config_path.return_value = self.test_path @@ -129,12 +154,31 @@ def _write_token(*_, **__): def test_config_path(self): from neon_utils.hana_utils import _get_client_config_path path_1 = _get_client_config_path("https://hana.neonaialpha.com") - default = _get_client_config_path() + default = _get_client_config_path("https://hana.neonaiservices.com") self.assertNotEqual(path_1, default) self.assertEqual(dirname(path_1), dirname(default)) # TODO: Test invalid refresh + @patch("ovos_config.config.Configuration") + def test_set_default_backend_url(self, config): + import neon_utils.hana_utils + from neon_utils.hana_utils import set_default_backend_url + neon_utils.hana_utils._DEFAULT_BACKEND_URL = None + config.return_value = dict() + + set_default_backend_url() + self.assertEqual(neon_utils.hana_utils._DEFAULT_BACKEND_URL, + "https://hana.neonaiservices.com") + + set_default_backend_url("https://hana.neonaialpha.com") + self.assertEqual(neon_utils.hana_utils._DEFAULT_BACKEND_URL, + "https://hana.neonaialpha.com") + + set_default_backend_url() + self.assertEqual(neon_utils.hana_utils._DEFAULT_BACKEND_URL, + "https://hana.neonaiservices.com") + if __name__ == '__main__': unittest.main() diff --git a/tests/location_util_tests.py b/tests/location_util_tests.py index aeb641b9..ec5048b5 100644 --- a/tests/location_util_tests.py +++ b/tests/location_util_tests.py @@ -74,6 +74,10 @@ def test_get_location_from_coords(self): self.assertEqual(location, ("Seattle", "King County", "Washington", "United States")) + # Test 'hamlet' location + location = get_location(34.46433387046654, -81.99487538579375) + self.assertIsInstance(location[0], str) + def test_get_timezone_from_coords(self): from neon_utils.location_utils import get_timezone lat = 47.6038321 @@ -84,6 +88,12 @@ def test_get_timezone_from_coords(self): self.assertIsInstance(offset, float) self.assertIn(offset, (-7.0, -8.0)) + lat = 35.0000 + lon = 103.000 + timezone, offset = get_timezone(lat, lon) + self.assertEqual(timezone, "Asia/Shanghai") + self.assertEqual(offset, 8.0) + def test_to_system_time(self): from neon_utils.location_utils import to_system_time tz_aware_dt = datetime.now(gettz("America/NewYork")) diff --git a/tests/metric_util_tests.py b/tests/metric_util_tests.py index bde9c64b..7b3f83b5 100644 --- a/tests/metric_util_tests.py +++ b/tests/metric_util_tests.py @@ -53,6 +53,10 @@ def test_stopwatch_simple(self): sleep(sleep_time) self.assertEqual(round(stopwatch.time, 2), sleep_time) + stopwatch = Stopwatch() + stopwatch.stop() + self.assertIsNone(stopwatch.time) + def test_stopwatch_reuse(self): sleep_time = 0.5 stopwatch = Stopwatch() diff --git a/tests/neon_skill_tests.py b/tests/neon_skill_tests.py index a2c48f3b..514132ab 100644 --- a/tests/neon_skill_tests.py +++ b/tests/neon_skill_tests.py @@ -759,7 +759,7 @@ def is_valid(_): test_results["validator"] = True return False - on_fail = Mock() + on_fail = Mock(return_value="fail") def skill_response_thread(s: MycroftSkill, idx: str): resp = s.get_response(test_dialog, validator=is_valid, diff --git a/tests/net_util_tests.py b/tests/net_util_tests.py index 3f6c0048..9be1faa7 100644 --- a/tests/net_util_tests.py +++ b/tests/net_util_tests.py @@ -33,6 +33,8 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +TCP_TEST = "mq.neonaiservices.com" + class NetUtilTests(unittest.TestCase): @classmethod @@ -119,18 +121,18 @@ def mock_socket(*args, **kwargs): def test_check_url_connection_invalid_url(self): from neon_utils.net_utils import check_url_response - self.assertFalse(check_url_response("https://api.neon.ai")) + self.assertFalse(check_url_response(f"https://{TCP_TEST}")) def test_check_online_valid_online(self): from neon_utils.net_utils import check_online self.assertTrue(check_online()) self.assertTrue(check_online(("google.com", "github.com"))) - self.assertTrue(check_online(("api.neon.ai", "google.com"))) + self.assertTrue(check_online((TCP_TEST, "google.com"))) self.assertTrue(check_online(("", "google.com"))) def test_check_online_invalid_offline(self): from neon_utils.net_utils import check_online - self.assertFalse(check_online(("api.neon.ai",))) + self.assertFalse(check_online((TCP_TEST,))) self.assertFalse(check_online(("",))) def test_check_online_valid_offline(self): @@ -152,7 +154,7 @@ def test_check_online_invalid_params(self): def test_check_port_is_open(self): from neon_utils.net_utils import check_port_is_open - self.assertTrue(check_port_is_open("api.neon.ai", 5672)) + self.assertTrue(check_port_is_open(TCP_TEST, 5672)) self.assertFalse(check_port_is_open("www.neon.ai", 5672)) diff --git a/tests/web_util_tests.py b/tests/web_util_tests.py index e1fa5091..034fb740 100644 --- a/tests/web_util_tests.py +++ b/tests/web_util_tests.py @@ -39,13 +39,12 @@ def test_scrape_page_for_links(self): try: links = scrape_page_for_links("neon.ai") self.assertIsInstance(links, dict) - self.assertIn("about us", links.keys()) + self.assertIn("company", links.keys()) # TODO: Update test to validate absolute and relative URL paths # Relative href - self.assertIn(links["about us"], - ("https://neon.ai/aboutus", - "http://neon.ai/aboutus", - "https://neon.ai/index.php/aboutus")) + self.assertIn(links["company"], + ("https://neon.ai/company", + "https://neon.ai/company/")) except ConnectTimeout: LOG.error("Github testing breaks here") diff --git a/version.py b/version.py index 69a697a5..5f48db7e 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "1.9.2a3" +__version__ = "1.10.2a9"