From 3a77617718e04828e84a70bb51e81e324df06a65 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 29 Dec 2023 00:38:14 +0000 Subject: [PATCH 01/87] 0.1.0 alpha 3 (#204) rebase 0.0.38 --- .github/workflows/coverage.yml | 1 - .github/workflows/unit_tests.yml | 1 - CHANGELOG.md | 37 - examples/change_wakeword.py | 26 - examples/core_status.py | 10 - examples/count_utterances.py | 29 - examples/fuzzywuzzy_intent_engine.py | 83 -- examples/gui_tracking.py | 48 - examples/intent_api.py | 32 - examples/live_translate_satellite.py | 29 - examples/location.py | 18 - examples/mark1_animations.py | 33 - examples/mark1_game_of_life.py | 14 - examples/mark1_image_rotate.py | 36 - examples/mark1_pixel_wise.py | 50 - examples/mark1_space_invader.py | 12 - examples/music.txt | 8 - examples/padaos_intent_engine.py | 79 -- examples/private_settings.py | 19 - examples/remote_skill_settings.py | 51 - examples/send_file_over_bus.py | 46 - examples/translation_utils.py | 20 - examples/universal_chat.py | 38 - examples/watchdog.py | 21 - ovos_utils/__init__.py | 43 +- ovos_utils/configuration.py | 377 ------ ovos_utils/device_input.py | 1 + ovos_utils/enclosure/__init__.py | 76 -- ovos_utils/enclosure/api.py | 345 ----- ovos_utils/enclosure/mark1/__init__.py | 8 - ovos_utils/enclosure/mark1/eyes/__init__.py | 513 -------- .../enclosure/mark1/faceplate/__init__.py | 410 ------ .../enclosure/mark1/faceplate/animations.py | 570 -------- .../mark1/faceplate/cellular_automaton.py | 485 ------- ovos_utils/enclosure/mark1/faceplate/icons.py | 239 ---- ovos_utils/events.py | 58 +- ovos_utils/file_utils.py | 7 +- ovos_utils/fingerprinting.py | 471 ------- ovos_utils/gui.py | 1150 +---------------- ovos_utils/intents/__init__.py | 143 -- ovos_utils/intents/converse.py | 205 --- .../intents/intent_service_interface.py | 585 --------- ovos_utils/intents/layers.py | 161 --- ovos_utils/json_helper.py | 4 - ovos_utils/log.py | 8 +- ovos_utils/messagebus.py | 528 -------- ovos_utils/metrics.py | 2 +- ovos_utils/network_utils.py | 5 +- ovos_utils/ovos_service_api.py | 227 ---- ovos_utils/parse.py | 4 +- ovos_utils/process_utils.py | 5 +- ovos_utils/res/fallback_mycroft.conf | 461 ------- .../platform_fingerprints/spoofed_ovos.json | 26 - ovos_utils/security.py | 12 +- ovos_utils/signal.py | 6 +- ovos_utils/skills.py | 35 + ovos_utils/skills/__init__.py | 116 -- ovos_utils/skills/api.py | 74 -- ovos_utils/skills/audioservice.py | 469 ------- ovos_utils/skills/locations.py | 155 --- ovos_utils/skills/settings.py | 132 -- ovos_utils/sound.py | 117 ++ ovos_utils/sound/__init__.py | 276 ---- ovos_utils/sound/alsa.py | 102 -- ovos_utils/sound/pulse.py | 161 --- ovos_utils/ssml.py | 4 +- ovos_utils/system.py | 65 +- ovos_utils/time.py | 3 +- ovos_utils/version.py | 7 +- requirements/extras.txt | 6 +- requirements/test.txt | 3 - setup.py | 7 - test/unittests/test_audio_utils.py | 173 --- test/unittests/test_enclosure.py | 33 - test/unittests/test_events.py | 8 +- test/unittests/test_gui.py | 450 ------- test/unittests/test_intents.py | 353 ----- test/unittests/test_skills.py | 225 ---- test/unittests/test_sound.py | 39 - test/unittests/test_system.py | 24 - 80 files changed, 204 insertions(+), 10709 deletions(-) delete mode 100644 examples/change_wakeword.py delete mode 100644 examples/core_status.py delete mode 100644 examples/count_utterances.py delete mode 100644 examples/fuzzywuzzy_intent_engine.py delete mode 100644 examples/gui_tracking.py delete mode 100644 examples/intent_api.py delete mode 100644 examples/live_translate_satellite.py delete mode 100644 examples/location.py delete mode 100644 examples/mark1_animations.py delete mode 100644 examples/mark1_game_of_life.py delete mode 100644 examples/mark1_image_rotate.py delete mode 100644 examples/mark1_pixel_wise.py delete mode 100644 examples/mark1_space_invader.py delete mode 100644 examples/music.txt delete mode 100644 examples/padaos_intent_engine.py delete mode 100644 examples/private_settings.py delete mode 100644 examples/remote_skill_settings.py delete mode 100644 examples/send_file_over_bus.py delete mode 100644 examples/translation_utils.py delete mode 100644 examples/universal_chat.py delete mode 100644 examples/watchdog.py delete mode 100644 ovos_utils/configuration.py delete mode 100644 ovos_utils/enclosure/__init__.py delete mode 100644 ovos_utils/enclosure/api.py delete mode 100644 ovos_utils/enclosure/mark1/__init__.py delete mode 100644 ovos_utils/enclosure/mark1/eyes/__init__.py delete mode 100644 ovos_utils/enclosure/mark1/faceplate/__init__.py delete mode 100644 ovos_utils/enclosure/mark1/faceplate/animations.py delete mode 100644 ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py delete mode 100644 ovos_utils/enclosure/mark1/faceplate/icons.py delete mode 100644 ovos_utils/fingerprinting.py delete mode 100644 ovos_utils/intents/__init__.py delete mode 100644 ovos_utils/intents/converse.py delete mode 100644 ovos_utils/intents/intent_service_interface.py delete mode 100644 ovos_utils/intents/layers.py delete mode 100644 ovos_utils/ovos_service_api.py delete mode 100644 ovos_utils/res/fallback_mycroft.conf delete mode 100644 ovos_utils/res/platform_fingerprints/spoofed_ovos.json create mode 100644 ovos_utils/skills.py delete mode 100644 ovos_utils/skills/__init__.py delete mode 100644 ovos_utils/skills/api.py delete mode 100644 ovos_utils/skills/audioservice.py delete mode 100644 ovos_utils/skills/locations.py delete mode 100644 ovos_utils/skills/settings.py create mode 100644 ovos_utils/sound.py delete mode 100644 ovos_utils/sound/__init__.py delete mode 100644 ovos_utils/sound/alsa.py delete mode 100644 ovos_utils/sound/pulse.py delete mode 100644 requirements/test.txt delete mode 100644 test/unittests/test_audio_utils.py delete mode 100644 test/unittests/test_enclosure.py delete mode 100644 test/unittests/test_gui.py delete mode 100644 test/unittests/test_intents.py delete mode 100644 test/unittests/test_skills.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fc2630fb..85599026 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,7 +23,6 @@ jobs: python -m pip install build wheel - name: Install repo run: | - pip install -r requirements/test.txt pip install -e .[extras] - name: Generate coverage report run: | diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 53c1a11b..d94b0e7e 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -52,7 +52,6 @@ jobs: python -m pip install build wheel - name: Install core repo run: | - pip install -r requirements/test.txt pip install -e .[extras] - name: Install test dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index e64070d5..3b425147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,5 @@ # Changelog -## [V0.0.37a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a4) (2023-12-28) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a3...V0.0.37a4) - -**Merged pull requests:** - -- deprecate bus utils [\#207](https://github.com/OpenVoiceOS/ovos-utils/pull/207) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.37a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a3) (2023-12-28) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a2...V0.0.37a3) - -**Closed issues:** - -- decouple concerns from bus/workshop [\#205](https://github.com/OpenVoiceOS/ovos-utils/issues/205) -- ROADMAP - ovos-utils 0.1.0 [\#117](https://github.com/OpenVoiceOS/ovos-utils/issues/117) - -**Merged pull requests:** - -- LAST ALPHA [\#206](https://github.com/OpenVoiceOS/ovos-utils/pull/206) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.37a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a2) (2023-12-18) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a1...V0.0.37a2) - -**Fixed bugs:** - -- update imports for py 3.10 compat [\#202](https://github.com/OpenVoiceOS/ovos-utils/pull/202) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.37a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a1) (2023-11-08) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36...V0.0.37a1) - -**Fixed bugs:** - -- elevate sound media role [\#201](https://github.com/OpenVoiceOS/ovos-utils/pull/201) ([emphasize](https://github.com/emphasize)) - \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/examples/change_wakeword.py b/examples/change_wakeword.py deleted file mode 100644 index c8e1d113..00000000 --- a/examples/change_wakeword.py +++ /dev/null @@ -1,26 +0,0 @@ -from ovos_utils.configuration import update_mycroft_config -from ovos_utils.lang.phonemes import get_phonemes - - -def create_wakeword(word, sensitivity): - # sensitivity is a bitch to do automatically - # IDEA make some web ui or whatever to tweak it experimentally - phonemes = get_phonemes(word) - config = { - "listener": { - "wake_word": word - }, - word: { - "andromeda": { - "module": "pocketsphinx", - "phonemes": phonemes, - "sample_rate": 16000, - "threshold": sensitivity, - "lang": "en-us" - } - } - } - update_mycroft_config(config) - - -create_wakeword("andromeda", "1e-25") \ No newline at end of file diff --git a/examples/core_status.py b/examples/core_status.py deleted file mode 100644 index 00423e31..00000000 --- a/examples/core_status.py +++ /dev/null @@ -1,10 +0,0 @@ -from ovos_utils.skills import skills_loaded -from time import sleep - -loaded = False -while not loaded: - loaded = skills_loaded() - sleep(0.5) - -print("Skills all loaded!") - diff --git a/examples/count_utterances.py b/examples/count_utterances.py deleted file mode 100644 index 8275326f..00000000 --- a/examples/count_utterances.py +++ /dev/null @@ -1,29 +0,0 @@ -from ovos_utils.messagebus import listen_for_message -from ovos_utils.log import LOG -from ovos_utils import wait_for_exit_signal - -heard = 0 -spoken = 0 - - -def handle_speak(message): - global spoken - spoken += 1 - LOG.info("Mycroft spoke {n} sentences since start".format(n=spoken)) - - -def handle_hear(message): - global heard - heard += 1 - LOG.info("Mycroft responded to {n} sentences since start".format(n=heard)) - - -bus = listen_for_message("speak", handle_speak) -listen_for_message("recognize_loop:utterance", handle_hear, bus=bus) # re utilize bus - -wait_for_exit_signal() # wait for ctrl+c - -# cleanup is a good practice! -bus.remove_all_listeners("speak") -bus.remove_all_listeners("recognize_loop:utterance") -bus.close() diff --git a/examples/fuzzywuzzy_intent_engine.py b/examples/fuzzywuzzy_intent_engine.py deleted file mode 100644 index dd0d9d11..00000000 --- a/examples/fuzzywuzzy_intent_engine.py +++ /dev/null @@ -1,83 +0,0 @@ -from ovos_utils.intents.engines import BaseIntentEngine -from ovos_utils.skills.intent_provider import IntentEngineSkill - -from mycroft.configuration.config import Configuration -from mycroft.skills.core import MycroftSkill, Message - -from fuzzywuzzy import process - - -class FuzzyEngine(BaseIntentEngine): - def __init__(self): - self.name = "fuzzy" - BaseIntentEngine.__init__(self, self.name) - self.config = Configuration.get().get(self.name, {}) - - def calc_intent(self, query): - """ return best intent for this query """ - data = {"conf": 0, - "utterance": query, - "name": None} - - best_match = None - best_score = 0 - for intent in self.intent_samples: - samples = self.intent_samples[intent] - match, score = process.extractOne(query, samples) - if score > best_score: - best_score = score - best_match = (intent, match) - - if best_match is not None: - intent, match = best_match - data["name"] = intent - data["match"] = match - data["conf"] = best_score - - entities = [] - for entity in self.entity_samples: - samples = self.entity_samples - for s in samples: - if s in query: - entities.append((entity, s)) - - data["entities"] = [{entity: s} for entity, s in entities] - return data - - -# engine skill for mycroft -class FuzzyEngineSkill(IntentEngineSkill): - def initialize(self): - priority = 5 - engine = FuzzyEngine() - self.bind_engine(engine, priority) - - -class FuzzySkill(MycroftSkill): - def register_fuzzy_intent(self, name, samples, handler): - message = "fuzzy:register_intent" - name = str(self.skill_id) + ':' + name - data = {"name": name, "samples": samples} - - self.bus.emit(Message(message, data)) - self.add_event(name, handler, 'mycroft.skill.handler') - - def register_fuzzy_entity(self, name, samples): - message = "fuzzy:register_entity" - name = str(self.skill_id) + ':' + name - data = {"name": name, "samples": samples} - self.bus.emit(Message(message, data)) - - -def create_skill(): - return FuzzySkill() - -# install this file as a regular skill (fuzzy_engine/__init__.py) - -## to import and create fuzzy skills - -# from os.path import dirname -# skills_dir = dirname(dirname(__file__)) -# import sys -# sys.path.append(skills_dir) -# from fuzzy_engine import PadaosSkill \ No newline at end of file diff --git a/examples/gui_tracking.py b/examples/gui_tracking.py deleted file mode 100644 index 58f27a63..00000000 --- a/examples/gui_tracking.py +++ /dev/null @@ -1,48 +0,0 @@ -from ovos_utils.gui import GUITracker -from ovos_utils import wait_for_exit_signal - - -class MyGUIEventTracker(GUITracker): - # GUI event handlers - # user can/should subclass this - def on_idle(self, namespace): - print("IDLE", namespace) - timestamp = self.idle_ts - - def on_active(self, namespace): - # NOTE: page has not been loaded yet - # event will fire right after this one - print("ACTIVE", namespace) - # check namespace values, they should all be set before this event - values = self.gui_values[namespace] - - def on_new_page(self, page, namespace, index): - print("NEW PAGE", namespace, index, namespace) - # check all loaded pages - for n in self.gui_pages: # list of named tuples - nspace = n.name # namespace / skill_id - pages = n.pages # ordered list of page uris - - def on_gui_value(self, namespace, key, value): - # WARNING this will pollute logs quite a lot, and you will get - # duplicates, better to check values on a different event, - # demonstrated in on_active - print("VALUE", namespace, key, value) - - -g = MyGUIEventTracker() - -print("device has screen:", g.can_display()) -print("mycroft-gui installed:", g.is_gui_installed()) -print("gui connected:", g.is_gui_connected()) - - -# check registered idle screens -print("Registered idle screens:") -for name in g.idle_screens: - namespace = g.idle_screens[name] - print(" - ", name, ":", namespace) - - -# just block listening for events until ctrl + C -wait_for_exit_signal() diff --git a/examples/intent_api.py b/examples/intent_api.py deleted file mode 100644 index 733dac9f..00000000 --- a/examples/intent_api.py +++ /dev/null @@ -1,32 +0,0 @@ -from ovos_utils.intents import IntentQueryApi -from pprint import pprint - - -intents = IntentQueryApi() - -pprint(intents.get_skill("who are you")) -pprint(intents.get_skill("set volume to 100%")) - -exit() -# loaded skills -pprint(intents.get_skills_manifest()) -pprint(intents.get_active_skills()) - -# intent parsing -pprint(intents.get_adapt_intent("who are you")) -pprint(intents.get_padatious_intent("who are you")) -pprint(intents.get_intent("who are you")) # intent that will trigger - -# skill from utterance -pprint(intents.get_skill("who are you")) - -# registered intents -pprint(intents.get_adapt_manifest()) -pprint(intents.get_padatious_manifest()) -pprint(intents.get_intent_manifest()) # all of the above - -# registered vocab -pprint(intents.get_entities_manifest()) # padatious entities / .entity files -pprint(intents.get_vocab_manifest()) # adapt vocab / .voc files -pprint(intents.get_regex_manifest()) # adapt regex / .rx files -pprint(intents.get_keywords_manifest()) # all of the above diff --git a/examples/live_translate_satellite.py b/examples/live_translate_satellite.py deleted file mode 100644 index 40a7b88d..00000000 --- a/examples/live_translate_satellite.py +++ /dev/null @@ -1,29 +0,0 @@ -from ovos_utils.messagebus import get_mycroft_bus, listen_for_message -from ovos_utils import wait_for_exit_signal -from ovos_utils.lang.translate import say_in_language - -# from ovos_utils.lang.translate import translate_to_mp3 -# from ovos_utils.sound import play_mp3 - -bus_ip = "0.0.0.0" # enter a remote ip here, remember bus is unencrypted! careful with opening firewalls -bus = get_mycroft_bus(host=bus_ip) - -TARGET_LANG = "pt" - - -def translate(message): - utterance = message.data["utterance"] - say_in_language(utterance, lang=TARGET_LANG) # will play .mp3 directly - - # if you need more control - # path = translate_to_mp3(utterance, lang=TARGET_LANG) - # play_mp3(path, cmd="play %1") # using sox - - -listen_for_message("speak", translate, bus=bus) - - -wait_for_exit_signal() # wait for ctrl+c - -bus.remove_all_listeners("speak") -bus.close() diff --git a/examples/location.py b/examples/location.py deleted file mode 100644 index 6e8dfda5..00000000 --- a/examples/location.py +++ /dev/null @@ -1,18 +0,0 @@ -from ovos_utils.location import geolocate, reverse_geolocate -from pprint import pprint - - -full_addr = "798 N 1415 Rd, Lawrence, KS 66049, United States" -vague_addr = "Lawrence Kansas" -country = "Portugal" - -data = geolocate(country) -pprint(data) - - -lat = 38.9730682373638 -lon = -95.2361831665156 - -data = reverse_geolocate(lat, lon) - -pprint(data) \ No newline at end of file diff --git a/examples/mark1_animations.py b/examples/mark1_animations.py deleted file mode 100644 index 688c2056..00000000 --- a/examples/mark1_animations.py +++ /dev/null @@ -1,33 +0,0 @@ -from ovos_utils.enclosure.mark1.faceplate.icons import HollowHeartIcon, \ - HeartIcon, SkullIcon, Boat -from ovos_utils.enclosure.mark1.faceplate.animations import LeftRight, \ - HorizontalScroll - - -class SailingBoat(Boat, HorizontalScroll): - pass - - -class MovingHeart(HeartIcon, LeftRight): - pass - - -class MovingHeart2(HollowHeartIcon, LeftRight): - pass - - -class MovingSkull(SkullIcon, LeftRight): - pass - - -from time import sleep -from ovos_utils.messagebus import get_mycroft_bus - - -bus = get_mycroft_bus("192.168.1.70") - -boat = MovingHeart(bus=bus) - -for faceplate in boat: - faceplate.display() - sleep(0.5) \ No newline at end of file diff --git a/examples/mark1_game_of_life.py b/examples/mark1_game_of_life.py deleted file mode 100644 index 12f48cdc..00000000 --- a/examples/mark1_game_of_life.py +++ /dev/null @@ -1,14 +0,0 @@ -from ovos_utils.enclosure.mark1.faceplate.cellular_automaton import GoL -from ovos_utils.messagebus import get_mycroft_bus - -bus = get_mycroft_bus("192.168.1.70") - - -game_of_life = GoL(bus=bus) - - -def handle_new_frame(grid): - grid.display(invert=False) - - -game_of_life.run(0.5, handle_new_frame) \ No newline at end of file diff --git a/examples/mark1_image_rotate.py b/examples/mark1_image_rotate.py deleted file mode 100644 index 4c43c105..00000000 --- a/examples/mark1_image_rotate.py +++ /dev/null @@ -1,36 +0,0 @@ -from ovos_utils.enclosure.mark1.faceplate.icons import Boat, MusicIcon, StormIcon, \ - SnowIcon, SunnyIcon, PartlyCloudyIcon, PlusIcon, SkullIcon, CrossIcon, \ - HollowHeartIcon, HeartIcon, DeadFishIcon, InfoIcon, \ - ArrowLeftIcon, JarbasAI, WarningIcon -from ovos_utils.messagebus import get_mycroft_bus -from time import sleep - -bus = get_mycroft_bus("192.168.1.70") - -images = [Boat(bus=bus), - MusicIcon(bus=bus), - StormIcon(bus=bus), - SunnyIcon(bus=bus), - PartlyCloudyIcon(bus=bus), - PlusIcon(bus=bus), - CrossIcon(bus=bus), - SnowIcon(bus=bus), - SkullIcon(bus=bus), - HeartIcon(bus=bus), - HollowHeartIcon(bus=bus), - ArrowLeftIcon(bus=bus), - JarbasAI(bus=bus), - WarningIcon(bus=bus), - InfoIcon(bus=bus), - DeadFishIcon(bus=bus)] - -from ovos_utils.enclosure.mark1.faceplate.icons import SpaceInvader1, \ - SpaceInvader2, SpaceInvader3, SpaceInvader4 -images = [SpaceInvader1(bus=bus), - SpaceInvader2(bus=bus), - SpaceInvader3(bus=bus), - SpaceInvader4(bus=bus)] -for faceplate in images: - faceplate.print() - faceplate.display() - sleep(3) diff --git a/examples/mark1_pixel_wise.py b/examples/mark1_pixel_wise.py deleted file mode 100644 index c4cc4eb9..00000000 --- a/examples/mark1_pixel_wise.py +++ /dev/null @@ -1,50 +0,0 @@ -from ovos_utils.enclosure.mark1.faceplate.icons import FaceplateGrid -from ovos_utils.messagebus import get_mycroft_bus -from time import sleep - - -bus = get_mycroft_bus("192.168.1.70") - - -grid = FaceplateGrid(bus=bus) - -# grid is white -grid.display() -sleep(2) - -# grid is black -grid.invert() -grid.display() -sleep(2) - -# read pixels -n_pixels = len(grid) -assert grid.height == 8 -assert grid.width == 32 -assert grid[1][1] == 1 # 1 == black / pixel off - -try: - grid[grid.height] -except IndexError: - pass # 0 <= i < grid.height -try: - grid[grid.height-1][grid.width] -except IndexError: - pass # 0 <= j < grid.width - -# create a dotted line -grid[4][0] = 0 # white -grid[4][5] = 0 # white -grid[4][10] = 0 # white -grid[4][15] = 0 # white -grid[4][20] = 0 # white -grid[4][25] = 0 # white -grid[4][30] = 0 # white -grid.display() - -# optionally disable invert -# if 1 == white / pixel on -# makes more sense to you -sleep(2) -grid.display(invert=False) - diff --git a/examples/mark1_space_invader.py b/examples/mark1_space_invader.py deleted file mode 100644 index 906f990e..00000000 --- a/examples/mark1_space_invader.py +++ /dev/null @@ -1,12 +0,0 @@ -from ovos_utils.enclosure.mark1.faceplate.cellular_automaton import \ - SpaceInvader -from ovos_utils.messagebus import get_mycroft_bus -from time import sleep - -bus = get_mycroft_bus("192.168.1.70") - -game_of_life = SpaceInvader(bus=bus) - -for grid in game_of_life: - grid.display(invert=False) - sleep(2) diff --git a/examples/music.txt b/examples/music.txt deleted file mode 100644 index b375a00c..00000000 --- a/examples/music.txt +++ /dev/null @@ -1,8 +0,0 @@ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXXXXX XX XXXXXXXXXXXXX -XXXXXXXXXXXX X XXXXXXXXXXXXX -XXXXXXXXXXXXX XXX XXXXXXXXXXXXXX \ No newline at end of file diff --git a/examples/padaos_intent_engine.py b/examples/padaos_intent_engine.py deleted file mode 100644 index 15eb223a..00000000 --- a/examples/padaos_intent_engine.py +++ /dev/null @@ -1,79 +0,0 @@ -from ovos_utils.intents.engines import BaseIntentEngine -from ovos_utils.skills.intent_provider import IntentEngineSkill - -from mycroft.configuration.config import Configuration -from mycroft.skills.core import MycroftSkill, Message - -from padaos import IntentContainer - - -class PadaosEngine(BaseIntentEngine): - def __init__(self): - self.name = "padaos" - BaseIntentEngine.__init__(self, self.name) - self.config = Configuration.get().get(self.name, {}) - self.container = IntentContainer() - - def add_intent(self, name, samples): - self.container.add_intent(name, samples) - - def remove_intent(self, name): - self.container.remove_intent(name) - - def add_entity(self, name, samples): - self.container.add_entity(name, samples) - - def remove_entity(self, name): - self.container.remove_entity(name) - - def train(self, single_thread=False): - """ train all registered intents and entities""" - # Padaos is simply regex, it handles this when registering - pass - - def calc_intent(self, query): - """ return best intent for this query """ - data = {"conf": 0, - "utterance": query, - "name": None} - data.update(self.container.calc_intent(query)) - return data - - -# engine skill for mycroft -class PadaosEngineSkill(IntentEngineSkill): - def initialize(self): - priority = 4 - engine = PadaosEngine() - self.bind_engine(engine, priority) - - -class PadaosSkill(MycroftSkill): - def register_padaos_intent(self, name, samples, handler): - message = "padaos:register_intent" - name = str(self.skill_id) + ':' + name - data = {"name": name, "samples": samples} - - self.bus.emit(Message(message, data)) - self.add_event(name, handler, 'mycroft.skill.handler') - - def register_padaos_entity(self, name, samples): - message = "padaos:register_entity" - name = str(self.skill_id) + ':' + name - data = {"name": name, "samples": samples} - self.bus.emit(Message(message, data)) - - -def create_skill(): - return PadaosSkill() - - -# install this file as a regular skill (padaos_engine/__init__.py) - -## to import and create padaos skills - -# from os.path import dirname -# skills_dir = dirname(dirname(__file__)) -# import sys -# sys.path.append(skills_dir) -# from padaos_engine import PadaosSkill \ No newline at end of file diff --git a/examples/private_settings.py b/examples/private_settings.py deleted file mode 100644 index 959ea93b..00000000 --- a/examples/private_settings.py +++ /dev/null @@ -1,19 +0,0 @@ -from ovos_utils.skills.settings import PrivateSettings - - -with PrivateSettings("testskill.jarbasai") as settings: - print(settings.path) # ~/.cache/json_database/testskill.jarbasai.json - settings["key"] = "value" - - meta = settings.settingsmeta - # can be used for displaying in GUI - """ - {'skillMetadata': {'sections': [{'fields': [{'label': 'Key', - 'name': 'key', - 'type': 'text', - 'value': 'value'}], - 'name': 'testskill.jarbasai'}]}} - """ - - # auto saved when leaving "with" context - # you can also manually call settings.store() if not using "with" context \ No newline at end of file diff --git a/examples/remote_skill_settings.py b/examples/remote_skill_settings.py deleted file mode 100644 index 293b8039..00000000 --- a/examples/remote_skill_settings.py +++ /dev/null @@ -1,51 +0,0 @@ -from ovos_utils.skills.settings import get_all_remote_settings, get_remote_settings - -""" -WARNING: selene backend does not use a proper skill_id, if you have -skills with same name but different author settings will overwrite each -other on the backend - -2 way sync with Selene is also not implemented and causes a lot of issues, -these problems have been reported over 9 months ago (and counting) - -I strongly recommend you validate returned data as much as possible, -maybe ask the user to confirm any action, DO NOT automate settings sync - -skill matching is currently done by checking "if {skill} in string" -once mycroft fixes it on their side this will start using a proper -unique identifier - -THIS METHOD IS NOT ALWAYS SAFE -""" - -# get raw skill settings payload -all_remote_settings = get_all_remote_settings() -""" -{'@0e813c09-8dbc-40ed-974c-5705274a7b55|mycroft-npr-news|20.08': {'custom_url': '', - 'station': 'not_set', - 'use_curl': True}, - '@0e813c09-8dbc-40ed-974c-5705274a7b55|mycroft-pairing|20.08': None, - '@0e813c09-8dbc-40ed-974c-5705274a7b55|mycroft-volume|20.08': {'ducking': True}, - (...) - 'mycroft-weather|20.08': {'units': 'default'}, - 'mycroft-wiki|20.08': None} -""" - -# search remote settings for a specific skill -voip_remote_settings = get_remote_settings("skill-voip") -""" -{'add_contact': False, - 'auto_answer': True, - 'auto_reject': False, - 'auto_speech': 'I am busy, call again later', - 'contact_address': 'user@sipxcom.com', - 'contact_name': 'name here', - 'delete_contact': False, - 'gateway': 'sipxcom.com', - 'password': 'SECRET', - 'sipxcom_gateway': 'https://sipx.mattkeys.net', - 'sipxcom_password': 'secret', - 'sipxcom_sync': False, - 'sipxcom_user': 'MattKeys', - 'user': 'user'} -""" \ No newline at end of file diff --git a/examples/send_file_over_bus.py b/examples/send_file_over_bus.py deleted file mode 100644 index c7a602ea..00000000 --- a/examples/send_file_over_bus.py +++ /dev/null @@ -1,46 +0,0 @@ -from ovos_utils.messagebus import send_binary_data_message, \ - send_binary_file_message, decode_binary_message, listen_for_message - -from time import sleep -import json -from os.path import dirname, join - -# random file -my_file_path = join(dirname(__file__), "music.txt") -with open(my_file_path, "rb") as f: - original_binary = f.read() - - -def receive_file(message): - print("Receiving file") - path = message.data["path"] - print(path) - hex_data = message.data["binary"] - - # all accepted decode formats - binary_data = decode_binary_message(message) - print(binary_data == original_binary) - binary_data = decode_binary_message(hex_data) - print(binary_data == original_binary) - binary_data = decode_binary_message(message.data) - print(binary_data == original_binary) - binary_data = decode_binary_message(json.dumps(message.data)) - print(binary_data == original_binary) - binary_data = decode_binary_message(message.serialize()) - print(binary_data == original_binary) - - -listen_for_message("mycroft.binary.file", receive_file) -sleep(1) -send_binary_file_message(my_file_path) - - -def receive_binary(message): - print("Receiving binary data") - binary_data = decode_binary_message(message) - print(binary_data == original_binary) - - -listen_for_message("mycroft.binary.data", receive_binary) -sleep(1) -send_binary_data_message(original_binary) diff --git a/examples/translation_utils.py b/examples/translation_utils.py deleted file mode 100644 index 7abe1534..00000000 --- a/examples/translation_utils.py +++ /dev/null @@ -1,20 +0,0 @@ -from ovos_utils.lang import detect_lang, translate_text - -# detecting language -detect_lang("olá eu chamo-me joaquim") # "pt" -detect_lang("olá eu chamo-me joaquim", return_dict=True) -"""{'confidence': 0.9999939001351439, 'language': 'pt'}""" - -detect_lang("hello world") # "en" -detect_lang("This piece of text is in English. Този текст е на Български.", return_dict=True) -"""{'confidence': 0.28571342657428966, 'language': 'en'}""" - -# translating text -# - source lang will be auto detected using utils above -# - default target language is english -translate_text("olá eu chamo-me joaquim") -"""Hello I call myself joaquim""" - -# - you should specify source lang whenever possible to save 1 api call -translate_text("olá eu chamo-me joaquim", source_lang="pt", lang="es") -"""Hola, me llamo Joaquim""" diff --git a/examples/universal_chat.py b/examples/universal_chat.py deleted file mode 100644 index c764bb1d..00000000 --- a/examples/universal_chat.py +++ /dev/null @@ -1,38 +0,0 @@ -from ovos_utils.messagebus import send_message, listen_for_message -from ovos_utils.lang.translate import translate_text -from ovos_utils.lang.detect import detect_lang -from ovos_utils.log import LOG - - -OUTPUT_LANG = "pt" # received messages will be in this language -MYCROFT_LANG = "en" # mycroft is configured in this language - - -def handle_speak(message): - utt = message.data["utterance"] - utt = translate_text(utt, "pt") # source lang is auto detected - print("MYCROFT:", utt) - - -bus = listen_for_message("speak", handle_speak) - -print("Write in any language and mycroft will answer in {lang}".format(lang=OUTPUT_LANG)) - - -while True: - try: - utt = input("YOU:") - lang = detect_lang("utt") # source lang is auto detected, this is optional - if lang != MYCROFT_LANG: - utt = translate_text(utt) - send_message("recognizer_loop:utterance", - {"utterances": [utt]}, - bus=bus # re-utilize the bus connection - ) - except KeyboardInterrupt: - break - except Exception as e: - LOG.exception(e) - -bus.remove_all_listeners("speak") -bus.close() diff --git a/examples/watchdog.py b/examples/watchdog.py deleted file mode 100644 index 5e20fe25..00000000 --- a/examples/watchdog.py +++ /dev/null @@ -1,21 +0,0 @@ -from ovos_utils.messagebus import send_message -from ovos_utils.log import LOG -from ovos_utils import create_daemon, wait_for_exit_signal -import random -from time import sleep - - -def alert(): - LOG.info("Alerting user of some event using Mycroft") - send_message("speak", {"utterance": "Alert! something happened"}) - - -def did_something_happen(): - while True: - if random.choice([True, False]): - alert() - sleep(10) - - -create_daemon(did_something_happen) # check for something in background -wait_for_exit_signal() # wait for ctrl+c \ No newline at end of file diff --git a/ovos_utils/__init__.py b/ovos_utils/__init__.py index 1fa3e1f7..98912ab8 100644 --- a/ovos_utils/__init__.py +++ b/ovos_utils/__init__.py @@ -13,17 +13,12 @@ import datetime import re from functools import lru_cache, wraps -from os.path import isdir, join from threading import Thread, Event -from time import monotonic_ns -from time import sleep +from time import monotonic_ns, sleep import kthread -# TODO: Deprecate below imports -from ovos_utils.file_utils import resolve_ovos_resource_file, resolve_resource_file -from ovos_utils.network_utils import get_ip, get_external_ip, is_connected_dns, is_connected_http, is_connected -from ovos_utils.log import LOG, deprecated +from ovos_utils.log import LOG def threaded_timeout(timeout=5): @@ -33,7 +28,7 @@ def threaded_timeout(timeout=5): Adapted from https://github.com/OpenJarbas/InGeo @param timeout: Timeout in seconds to wait before terminating the process """ - + def deco(func): @wraps(func) def wrapper(*args, **kwargs): @@ -66,41 +61,11 @@ def func_wrapped(): class classproperty(property): """Decorator for a Class-level property. Credit to Denis Rhyzhkov on Stackoverflow: https://stackoverflow.com/a/13624858/1280629""" + def __get__(self, owner_self, owner_cls): return self.fget(owner_cls) -@deprecated("Anything depending on `mycroft`" - "should install `ovos-core` as a dependency", "0.1.0") -def ensure_mycroft_import(): - # TODO: Deprecate in 0.1.0 - try: - import mycroft - except ImportError: - import sys - from ovos_utils import get_mycroft_root - MYCROFT_ROOT_PATH = get_mycroft_root() - if MYCROFT_ROOT_PATH is not None: - sys.path.append(MYCROFT_ROOT_PATH) - else: - raise - - -@deprecated("Code should import from the current" - "namespace; other system paths are irrelevant.", "0.1.0") -def get_mycroft_root(): - # TODO: Deprecate in 0.1.0 - paths = [ - "/opt/venvs/mycroft-core/lib/python3.7/site-packages/", # mark1/2 - "/opt/venvs/mycroft-core/lib/python3.4/site-packages/ ", # old mark1 installs - "/home/pi/mycroft-core" # picroft - ] - for p in paths: - if isdir(join(p, "mycroft")): - return p - return None - - def timed_lru_cache( _func=None, *, seconds: int = 7000, maxsize: int = 128, typed: bool = False ): diff --git a/ovos_utils/configuration.py b/ovos_utils/configuration.py deleted file mode 100644 index 01d95093..00000000 --- a/ovos_utils/configuration.py +++ /dev/null @@ -1,377 +0,0 @@ -import json -from os import makedirs -from os.path import join, expanduser, exists, isfile - -import ovos_utils.xdg_utils as xdg -from ovos_utils.log import LOG, deprecated - - -# TODO - deprecate this submodule in 0.1.0 -# note that a couple of these are also used inside ovos-utils -# perhaps those usages should also move into workshop ? - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_default_lang(): - try: - from ovos_config.locale import get_default_lang as _get - return _get() - except ImportError: - return read_mycroft_config().get("lang", "en-us") - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def find_user_config(): - try: - from ovos_config.locations import find_user_config as _get - return _get() - except ImportError: - - return join(get_xdg_config_save_path(), get_config_filename()) - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_webcache_location(): - return join(get_xdg_config_save_path(), 'web_cache.json') - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_config_locations(default=True, web_cache=True, system=True, - old_user=True, user=True): - try: - from ovos_config.locations import get_config_locations as _get - return _get(default, web_cache, system, old_user, user) - except ImportError: - locs = [] - ovos_cfg = get_ovos_config() - if default: - locs.append(ovos_cfg["default_config_path"]) - if system: - locs.append(f"/etc/{ovos_cfg['base_folder']}/{ovos_cfg['config_filename']}") - if web_cache: - locs.append(get_webcache_location()) - if old_user: - locs.append(f"~/.{ovos_cfg['base_folder']}/{ovos_cfg['config_filename']}") - if user: - locs.append(f"{get_xdg_config_save_path()}/{ovos_cfg['config_filename']}") - return locs - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_ovos_config(): - try: - from ovos_config.meta import get_ovos_config as _get - return _get() - except ImportError: - return {"xdg": True, - "base_folder": "mycroft", - "config_filename": "mycroft.conf"} - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_base(): - try: - from ovos_config.meta import get_xdg_base as _get - return _get() - except ImportError: - return "mycroft" - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_config_locations(): - # This includes both the user config and - # /etc/xdg/mycroft/mycroft.conf - xdg_paths = list(reversed( - [join(p, get_config_filename()) - for p in get_xdg_config_dirs()] - )) - return xdg_paths - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_data_dirs(): - try: - from ovos_config.locations import get_xdg_data_dirs as _get - return _get() - except ImportError: - return [expanduser("~/.local/share/mycroft")] - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_config_dirs(folder=None): - try: - from ovos_config.locations import get_xdg_config_dirs as _get - return _get() - except ImportError: - folder = folder or get_xdg_base() - xdg_dirs = xdg.xdg_config_dirs() + [xdg.xdg_config_home()] - return [join(path, folder) for path in xdg_dirs] - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_cache_save_path(folder=None): - try: - from ovos_config.locations import get_xdg_cache_save_path as _get - return _get() - except ImportError: - folder = folder or get_xdg_base() - return join(xdg.xdg_cache_home(), folder) - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_data_save_path(): - try: - from ovos_config.locations import get_xdg_data_save_path as _get - return _get() - except ImportError: - return expanduser("~/.local/share/mycroft") - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_xdg_config_save_path(): - try: - from ovos_config.locations import get_xdg_config_save_path as _get - return _get() - except ImportError: - return expanduser("~/.config/mycroft") - - -@deprecated("XDG is always used.", "0.1.0") -def is_using_xdg(): - """ DEPRECATED """ - return True - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def set_xdg_base(*args, **kwargs): - try: - from ovos_config.meta import set_xdg_base as _set - _set(*args, **kwargs) - except: - pass - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def set_config_filename(*args, **kwargs): - try: - from ovos_config.meta import config_filename as _set - _set(*args, **kwargs) - except: - pass - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_config_filename(): - try: - from ovos_config.locale import get_config_filename as _get - return _get() - except ImportError: - return "mycroft.conf" - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def get_ovos_default_config_paths(): - try: - from ovos_config.meta import get_ovos_default_config_paths as _get - return _get() - except: - return ["/etc/OpenVoiceOS/ovos.conf"] - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def read_mycroft_config(): - try: - from ovos_config import Configuration - return Configuration() - except ImportError: - pass - path = expanduser(f"~/.config/mycroft/mycroft.conf") - if isfile(path): - with open(path) as f: - return json.load(f) - return { - # TODO - default cfg - "lang": "en-us" - } - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def update_mycroft_config(config, path=None, bus=None): - try: - from ovos_config.config import update_mycroft_config as _update - _update(config, path, bus) - except ImportError: - pass - # save in default user location - path = expanduser(f"~/.config/mycroft") - makedirs(path, exist_ok=True) - with open(f"{path}/mycroft.conf", "w") as f: - json.dump(config, f, indent=2) - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def set_default_config(*args, **kwargs): - try: - from ovos_config.meta import set_default_config as _set - _set(*args, **kwargs) - except: - pass - - -@deprecated("configuration moved to the `ovos_config` package.", "0.1.0") -def save_ovos_core_config(*args, **kwargs): - try: - from ovos_config.meta import save_ovos_config as _set - _set(*args, **kwargs) - except: - pass - - -try: - from ovos_config.models import ( - LocalConf, - ReadOnlyConfig, - MycroftUserConfig, - MycroftDefaultConfig, - MycroftSystemConfig, - MycroftXDGConfig - ) -except ImportError: - LOG.warning("configuration classes moved to the `ovos_config.models` package. " - "This submodule will be removed in ovos_utils 0.1.0") - from combo_lock import NamedLock - import yaml - from ovos_utils.json_helper import load_commented_json, merge_dict - - - class LocalConf(dict): - """Config dictionary from file.""" - allow_overwrite = True - # lock is shared among all subclasses, - # regardless of what file is being edited only one file should change at a time - # this ensure orderly behaviour in anything monitoring changes, - # eg FileWatcher util, configuration.patch bus handlers - __lock = NamedLock("ovos_config") - - def __init__(self, path): - super().__init__(self) - self.path = path - if path: - self.load_local(path) - - def _get_file_format(self, path=None): - """The config file format - supported file extensions: - - json (.json) - - commented json (.conf) - - yaml (.yaml/.yml) - - returns "yaml" or "json" - """ - path = path or self.path - if not path: - return "dict" - if path.endswith(".yml") or path.endswith(".yaml"): - return "yaml" - else: - return "json" - - def load_local(self, path=None): - """Load local json file into self. - - Args: - path (str): file to load - """ - path = path or self.path - if not path: - LOG.error(f"in memory configuration, nothing to load") - return - if exists(path) and isfile(path): - with self.__lock: - try: - if self._get_file_format(path) == "yaml": - with open(path) as f: - config = yaml.safe_load(f) - else: - config = load_commented_json(path) - if config: - for key in config: - self.__setitem__(key, config[key]) - LOG.debug(f"Configuration {path} loaded") - else: - LOG.debug(f"Empty config found at: {path}") - except Exception as e: - LOG.exception(f"Error loading configuration '{path}'") - else: - LOG.debug(f"Configuration '{path}' not defined, skipping") - - def reload(self): - self.load_local(self.path) - - def store(self, path=None): - path = path or self.path - if not path: - LOG.error(f"in memory configuration, no save location") - return - with self.__lock: - if self._get_file_format(path) == "yaml": - with open(path, 'w+') as f: - yaml.dump(dict(self), f, allow_unicode=True, - default_flow_style=False, sort_keys=False) - else: - with open(path, 'w+') as f: - json.dump(self, f, indent=2) - - def merge(self, conf): - merge_dict(self, conf) - - - class ReadOnlyConfig(LocalConf): - """ read only """ - - def __init__(self, path, allow_overwrite=False): - super().__init__(path) - self.allow_overwrite = allow_overwrite - - def reload(self): - old = self.allow_overwrite - self.allow_overwrite = True - super().reload() - self.allow_overwrite = old - - def __setitem__(self, key, value): - if not self.allow_overwrite: - raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") - super().__setitem__(key, value) - - def merge(self, *args, **kwargs): - if not self.allow_overwrite: - raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") - super().merge(*args, **kwargs) - - def store(self, path=None): - if not self.allow_overwrite: - raise PermissionError(f"{self.path} is read only! it can not be modified at runtime") - super().store(path) - - - class MycroftDefaultConfig(ReadOnlyConfig): - def __init__(self): - super().__init__(join(get_xdg_config_save_path(), get_config_filename())) - - def set_root_config_path(self, root_config): - # in case we got it wrong / non standard - self.path = root_config - self.reload() - - - class MycroftSystemConfig(ReadOnlyConfig): - def __init__(self, allow_overwrite=False): - super().__init__("/etc/mycroft/mycroft.conf", allow_overwrite) - - - class RemoteConf(LocalConf): - def __init__(self, cache=get_webcache_location()): - super(RemoteConf, self).__init__(cache) - - - MycroftXDGConfig = MycroftUserConfig = MycroftDefaultConfig diff --git a/ovos_utils/device_input.py b/ovos_utils/device_input.py index 17e9b21b..6ab4bae5 100644 --- a/ovos_utils/device_input.py +++ b/ovos_utils/device_input.py @@ -1,5 +1,6 @@ import subprocess from distutils.spawn import find_executable + from ovos_utils.gui import is_gui_installed from ovos_utils.log import LOG diff --git a/ovos_utils/enclosure/__init__.py b/ovos_utils/enclosure/__init__.py deleted file mode 100644 index e9135711..00000000 --- a/ovos_utils/enclosure/__init__.py +++ /dev/null @@ -1,76 +0,0 @@ -from ovos_utils.system import MycroftRootLocations -from ovos_utils.fingerprinting import detect_platform, MycroftPlatform -from enum import Enum -from os.path import exists -from typing import Optional -from ovos_utils.log import deprecated, log_deprecation - -log_deprecation("ovos_utils.enclosure has been deprecated!", "0.1.0") - - -class MycroftEnclosures(str, Enum): - # TODO: Deprecate in 0.1.0 - PICROFT = "picroft" - BIGSCREEN = "kde" - OVOS = "OpenVoiceOS" - OLD_MARK1 = "mycroft_mark_1(old)" - MARK1 = "mycroft_mark_1" - MARK2 = "mycroft_mark_2" - HOLMESV = "HolmesV" - OLD_HOLMES = "mycroft-lib" - GENERIC = "generic" - OTHER = "unknown" - - -@deprecated("This method is deprecated. Code should import from the current" - "namespace; other system paths are irrelevant.", "0.1.0") -def enclosure2rootdir(enclosure: MycroftEnclosures = None) -> Optional[str]: - """ - Find the default installed core location for a specific platform. - @param enclosure: MycroftEnclosures object to get root path for - @return: string default root path - """ - # TODO: Deprecate in 0.1.0 - enclosure = enclosure or detect_enclosure() - if enclosure == MycroftEnclosures.OLD_MARK1: - return MycroftRootLocations.OLD_MARK1 - elif enclosure == MycroftEnclosures.MARK1: - return MycroftRootLocations.MARK1 - elif enclosure == MycroftEnclosures.MARK2: - return MycroftRootLocations.MARK2 - elif enclosure == MycroftEnclosures.PICROFT: - return MycroftPlatform.PICROFT - elif enclosure == MycroftEnclosures.OVOS: - return MycroftPlatform.OVOS - elif enclosure == MycroftEnclosures.BIGSCREEN: - return MycroftPlatform.BIGSCREEN - return None - - -@deprecated("This method is deprecated. Platform-specific code should" - "use ovos_utils.fingerprinting.detect_platform directly", "0.1.0") -def detect_enclosure() -> MycroftEnclosures: - """ - Determine which enclosure is present on this file system. - @return: MycroftEnclosures object detected - """ - # TODO: Deprecate in 0.1.0 - platform = detect_platform() - if platform == MycroftPlatform.MARK1: - if exists(MycroftRootLocations.OLD_MARK1): - return MycroftEnclosures.OLD_MARK1 - return MycroftEnclosures.MARK1 - elif platform == MycroftPlatform.MARK2: - return MycroftEnclosures.MARK2 - elif platform == MycroftPlatform.PICROFT: - return MycroftEnclosures.PICROFT - elif platform == MycroftPlatform.OVOS: - return MycroftEnclosures.OVOS - elif platform == MycroftPlatform.BIGSCREEN: - return MycroftEnclosures.BIGSCREEN - elif platform == MycroftPlatform.HOLMESV: - return MycroftEnclosures.HOLMESV - elif platform == MycroftPlatform.OLD_HOLMES: - return MycroftEnclosures.OLD_HOLMES - - return MycroftEnclosures.OTHER diff --git a/ovos_utils/enclosure/api.py b/ovos_utils/enclosure/api.py deleted file mode 100644 index 3427bdcb..00000000 --- a/ovos_utils/enclosure/api.py +++ /dev/null @@ -1,345 +0,0 @@ -from ovos_utils.log import log_deprecation - -log_deprecation("EnclosureApi has moved to ovos_bus_client.apis.enclosure", "0.1.0") - - -try: - from ovos_bus_client.apis.enclosure import EnclosureApi - -except ImportError: - from ovos_utils.fakebus import Message, dig_for_message - - class EnclosureAPI: - """ - This API is intended to be used to interface with the hardware - that is running Mycroft. It exposes all possible commands which - can be sent to a Mycroft enclosure implementation. - - Different enclosure implementations may implement this differently - and/or may ignore certain API calls completely. For example, - the eyes_color() API might be ignore on a Mycroft that uses simple - LEDs which only turn on/off, or not at all on an implementation - where there is no face at all. - """ - - def __init__(self, bus=None, skill_id=""): - self.bus = bus - self.skill_id = skill_id - - def set_bus(self, bus): - self.bus = bus - - def set_id(self, skill_id): - self.skill_id = skill_id - - def _get_source_message(self): - return dig_for_message() or \ - Message("", context={"destination": ["enclosure"], - "skill_id": self.skill_id}) - - def register(self, skill_id=""): - """Registers a skill as active. Used for speak() and speak_dialog() - to 'patch' a previous implementation. Somewhat hacky. - DEPRECATED - unused - """ - source_message = self._get_source_message() - skill_id = skill_id or self.skill_id - self.bus.emit(source_message.forward("enclosure.active_skill", - {"skill_id": skill_id})) - - def reset(self): - """The enclosure should restore itself to a started state. - Typically this would be represented by the eyes being 'open' - and the mouth reset to its default (smile or blank). - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.reset")) - - def system_reset(self): - """The enclosure hardware should reset any CPUs, etc.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.reset")) - - def system_mute(self): - """Mute (turn off) the system speaker.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.mute")) - - def system_unmute(self): - """Unmute (turn on) the system speaker.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.unmute")) - - def system_blink(self, times): - """The 'eyes' should blink the given number of times. - Args: - times (int): number of times to blink - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.blink", - {'times': times})) - - def eyes_on(self): - """Illuminate or show the eyes.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.on")) - - def eyes_off(self): - """Turn off or hide the eyes.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.off")) - - def eyes_blink(self, side): - """Make the eyes blink - Args: - side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.blink", - {'side': side})) - - def eyes_narrow(self): - """Make the eyes look narrow, like a squint""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.narrow")) - - def eyes_look(self, side): - """Make the eyes look to the given side - Args: - side (str): 'r' for right - 'l' for left - 'u' for up - 'd' for down - 'c' for crossed - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.look", - {'side': side})) - - def eyes_color(self, r=255, g=255, b=255): - """Change the eye color to the given RGB color - Args: - r (int): 0-255, red value - g (int): 0-255, green value - b (int): 0-255, blue value - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.color", - {'r': r, 'g': g, 'b': b})) - - def eyes_setpixel(self, idx, r=255, g=255, b=255): - """Set individual pixels of the Mark 1 neopixel eyes - Args: - idx (int): 0-11 for the right eye, 12-23 for the left - r (int): The red value to apply - g (int): The green value to apply - b (int): The blue value to apply - """ - source_message = self._get_source_message() - if idx < 0 or idx > 23: - raise ValueError(f'idx ({idx}) must be between 0-23') - self.bus.emit(source_message.forward("enclosure.eyes.setpixel", - {'idx': idx, - 'r': r, 'g': g, 'b': b})) - - def eyes_fill(self, percentage): - """Use the eyes as a type of progress meter - Args: - percentage (int): 0-49 fills the right eye, 50-100 also covers left - """ - source_message = self._get_source_message() - if percentage < 0 or percentage > 100: - raise ValueError(f'percentage ({percentage}) must be between 0-100') - self.bus.emit(source_message.forward("enclosure.eyes.fill", - {'percentage': percentage})) - - def eyes_brightness(self, level=30): - """Set the brightness of the eyes in the display. - Args: - level (int): 1-30, bigger numbers being brighter - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.level", - {'level': level})) - - def eyes_reset(self): - """Restore the eyes to their default (ready) state.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.reset")) - - def eyes_spin(self): - """Make the eyes 'roll' - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.spin")) - - def eyes_timed_spin(self, length): - """Make the eyes 'roll' for the given time. - Args: - length (int): duration in milliseconds of roll, None = forever - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.timedspin", - {'length': length})) - - def eyes_volume(self, volume): - """Indicate the volume using the eyes - Args: - volume (int): 0 to 11 - """ - source_message = self._get_source_message() - if volume < 0 or volume > 11: - raise ValueError('volume ({}) must be between 0-11'. - format(str(volume))) - self.bus.emit(source_message.forward("enclosure.eyes.volume", - {'volume': volume})) - - def mouth_reset(self): - """Restore the mouth display to normal (blank)""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.reset")) - - def mouth_talk(self): - """Show a generic 'talking' animation for non-synched speech""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.talk")) - - def mouth_think(self): - """Show a 'thinking' image or animation""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.think")) - - def mouth_listen(self): - """Show a 'thinking' image or animation""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.listen")) - - def mouth_smile(self): - """Show a 'smile' image or animation""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.smile")) - - def mouth_viseme(self, start, viseme_pairs): - """ Send mouth visemes as a list in a single message. - - Args: - start (int): Timestamp for start of speech - viseme_pairs: Pairs of viseme id and cumulative end times - (code, end time) - - codes: - 0 = shape for sounds like 'y' or 'aa' - 1 = shape for sounds like 'aw' - 2 = shape for sounds like 'uh' or 'r' - 3 = shape for sounds like 'th' or 'sh' - 4 = neutral shape for no sound - 5 = shape for sounds like 'f' or 'v' - 6 = shape for sounds like 'oy' or 'ao' - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.viseme_list", - {"start": start, - "visemes": viseme_pairs})) - - def mouth_text(self, text=""): - """Display text (scrolling as needed) - Args: - text (str): text string to display - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.text", - {'text': text})) - - def mouth_display(self, img_code="", x=0, y=0, refresh=True): - """Display images on faceplate. Currently supports images up to 16x8, - or half the face. You can use the 'x' parameter to cover the other - half of the faceplate. - Args: - img_code (str): text string that encodes a black and white image - x (int): x offset for image - y (int): y offset for image - refresh (bool): specify whether to clear the faceplate before - displaying the new image or not. - Useful if you'd like to display multiple images - on the faceplate at once. - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward('enclosure.mouth.display', - {'img_code': img_code, - 'xOffset': x, - 'yOffset': y, - 'clearPrev': refresh})) - - def mouth_display_png(self, image_absolute_path, - invert=False, x=0, y=0, refresh=True): - """ Send an image to the enclosure. - - Args: - image_absolute_path (string): The absolute path of the image - invert (bool): inverts the image being drawn. - x (int): x offset for image - y (int): y offset for image - refresh (bool): specify whether to clear the faceplate before - displaying the new image or not. - Useful if you'd like to display muliple images - on the faceplate at once. - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.display_image", - {'img_path': image_absolute_path, - 'xOffset': x, - 'yOffset': y, - 'invert': invert, - 'clearPrev': refresh})) - - def weather_display(self, img_code, temp): - """Show a the temperature and a weather icon - - Args: - img_code (char): one of the following icon codes - 0 = sunny - 1 = partly cloudy - 2 = cloudy - 3 = light rain - 4 = raining - 5 = stormy - 6 = snowing - 7 = wind/mist - temp (int): the temperature (either C or F, not indicated) - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.weather.display", - {'img_code': img_code, - 'temp': temp})) - - def activate_mouth_events(self): - """Enable movement of the mouth with speech""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward('enclosure.mouth.events.activate')) - - def deactivate_mouth_events(self): - """Disable movement of the mouth with speech""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward( - 'enclosure.mouth.events.deactivate')) - - def get_eyes_color(self): - """Get the eye RGB color for all pixels - Returns: - (list) pixels - list of (r,g,b) tuples for each eye pixel - """ - source_message = self._get_source_message() - message = source_message.forward("enclosure.eyes.rgb.get") - response = self.bus.wait_for_response(message, "enclosure.eyes.rgb") - if response: - return response.data["pixels"] - raise TimeoutError("Enclosure took too long to respond") - - def get_eyes_pixel_color(self, idx): - """Get the RGB color for a specific eye pixel - Returns: - (r,g,b) tuples for selected pixel - """ - if idx < 0 or idx > 23: - raise ValueError(f'idx ({idx}) must be between 0-23') - return self.get_eyes_color()[idx] diff --git a/ovos_utils/enclosure/mark1/__init__.py b/ovos_utils/enclosure/mark1/__init__.py deleted file mode 100644 index 6f51bbdd..00000000 --- a/ovos_utils/enclosure/mark1/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from ovos_utils.enclosure.api import EnclosureAPI -from ovos_utils.log import log_deprecation - -log_deprecation("ovos_utils.enclosure.mark1 moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") - - -class Mark1EnclosureAPI(EnclosureAPI): - """ Mark1 enclosure, messagebus API""" diff --git a/ovos_utils/enclosure/mark1/eyes/__init__.py b/ovos_utils/enclosure/mark1/eyes/__init__.py deleted file mode 100644 index 334161eb..00000000 --- a/ovos_utils/enclosure/mark1/eyes/__init__.py +++ /dev/null @@ -1,513 +0,0 @@ -from ovos_utils.log import log_deprecation - -log_deprecation("ovos_utils.enclosure.mark1.eyes moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") - - -try: - from ovos_mark1.eyes import * -except: - from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI - from ovos_utils.messagebus import get_mycroft_bus - from ovos_utils.colors import Color - from ovos_utils import rotate_list - from time import sleep - - - class EyePixel: - def __init__(self, index, api, color=None): - self.index = index - self.api = api - self.color = color or Color() - - @property - def rgb(self): - return self.color.rgb255 - - def sync_color(self): - r, g, b = self.api.get_eyes_pixel_color(self.index) - color = Color.from_rgb(r, g, b) - self.change_color(color) - - def update_color(self): - self.change_color(self.color) - - def change_color(self, name): - if isinstance(name, str): - self.color = Color.from_name(name) - elif isinstance(name, Color): - self.color = name - else: - raise ValueError("not a Color object") - r, g, b = self.rgb - self.api.eyes_setpixel(self.index, r, g, b) - - def set_saturation(self, value): - self.color.set_saturation(value) - self.update_color() - - def set_luminance(self, value): - self.color.set_luminance(value) - self.update_color() - - def set_hue(self, value): - self.color.set_hue(value) - self.update_color() - - def __repr__(self): - return "Pixel_" + str(self.index) + ":" + self.color.color_description - - - class Eye(list): - def __init__(self, pixel_range, bus=None, color=None): - super().__init__() - self.bus = bus or get_mycroft_bus() - self.api = Mark1EnclosureAPI(self.bus) - for idx in range(pixel_range[0], pixel_range[1]): - pixel = EyePixel(idx, self.api) - self.append(pixel) - self.color = Color() - if color: - self.change_color(color) - else: - self.sync_color() - - def sync_color(self): - for p in self: - p.sync_color() - sleep(0.05) - - def update_color(self): - for p in self: - p.update_color() - sleep(0.05) - - def change_color(self, name): - if isinstance(name, str): - self.color = Color.from_name(name) - elif isinstance(name, Color): - self.color = name - else: - raise ValueError("not a Color object") - for led in self: - led.change_color(self.color) - # writer bugs out if messages sent too fast - sleep(0.05) - - def saturation_spin(self, speed=0.05): - values = [] - for idx, pixel in enumerate(self): - sat = 0.09 * idx - pixel.set_saturation(sat) - values.append(sat) - sleep(speed) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self[idx].set_saturation(value) - sleep(speed) - - def luminance_spin(self, speed=0.05): - values = [] - for idx, pixel in enumerate(self): - sat = 0.05 * idx - pixel.set_luminance(sat) - values.append(sat) - sleep(speed) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self[idx].set_luminance(value) - sleep(speed) - - def hue_spin(self, speed=0.05): - values = [] - for idx, pixel in enumerate(self): - sat = 0.083 * idx - pixel.set_hue(sat) - values.append(sat) - sleep(speed) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self[idx].set_hue(value) - sleep(speed) - - def set_hue(self, hue): - for pixel in self: - pixel.color.set_hue(hue) - pixel.update_color() - - def set_luminance(self, value): - for pixel in self: - pixel.color.set_luminance(value) - self.update_color() - - def set_saturation(self, value): - for pixel in self: - pixel.color.set_saturation(value) - self.update_color() - - def on(self): - self.set_luminance(1) - - def off(self): - self.set_luminance(0) - - def blink_once(self): - """ - Make the eye blink - """ - raise NotImplementedError - - def blink(self, speed=0.5): - """ - Make the right eye blink in a loop - """ - while True: - self.blink_once() - sleep(speed) - - - class RightEye(Eye): - def __init__(self, bus, color=None): - super().__init__(bus=bus, pixel_range=(12, 24), color=color) - - def sync_color(self): - pixels = self.api.get_eyes_color()[12:] - for idx, (r, g, b) in enumerate(pixels): - self[idx].color = Color.from_rgb(r, g, b) - self.update_color() - - def blink_once(self): - """ - Make the right eye blink - """ - self.api.eyes_blink("r") - - - class LeftEye(Eye): - def __init__(self, bus, color=None): - super().__init__(bus=bus, pixel_range=(0, 12), color=color) - - def sync_color(self): - pixels = self.api.get_eyes_color()[:12] - for idx, (r, g, b) in enumerate(pixels): - self[idx].color = Color.from_rgb(r, g, b) - self.update_color() - - def blink_once(self): - """ - Make the left eye blink - """ - self.api.eyes_blink("l") - - - class Eyes(list): - def __init__(self, bus=None, color=None): - super().__init__() - self.bus = bus or get_mycroft_bus() - self.api = Mark1EnclosureAPI(self.bus) - self.right = RightEye(self.bus) - self.left = LeftEye(self.bus) - self.color = Color() - if color: - self.change_color(color) - else: - self.sync_color() - - def __getitem__(self, item): - assert isinstance(item, int) - assert 0 <= item <= 23 - if item < 12: - return self.left[item] - return self.right[item - 12] - - def __setitem__(self, key, value): - assert isinstance(key, int) - assert 0 <= key <= 23 - if key < 12: - self.left[key] = value - self.right[key] = value - - def __iter__(self): - for i in range(len(self)): - yield self[i] - - def __len__(self): - return len(self.left) + len(self.right) - - def sync_color(self): - """ updates internal color value to current color """ - pixels = self.api.get_eyes_color() - for idx, (r, g, b) in enumerate(pixels): - self[idx].color = Color.from_rgb(r, g, b) - - def update_color(self): - """ updates arduino color to current pixels """ - for i in range(len(self) // 2): - self.left[i].update_color() - sleep(0.05) - self.right[i].update_color() - sleep(0.05) - - def change_color(self, name): - """ changes color of both eyes """ - if isinstance(name, str): - self.color = Color.from_name(name) - elif isinstance(name, Color): - self.color = name - else: - raise ValueError("not a Color object") - r, g, b = self.color.rgb255 - self.api.eyes_color(r, g, b) - for idx in range(len(self)): - self[idx].color = self.color - - # animations - def saturation_spin(self, speed=0.05): - values = [] - for idx in range(len(self) // 2): - sat = 0.09 * idx - values.append(sat) - self.left[idx].set_saturation(sat) - sleep(0.03) - self.right[idx].set_saturation(sat) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self.left[idx].set_saturation(value) - sleep(0.03) - self.right[idx].set_saturation(value) - sleep(speed) - - def luminance_spin(self, speed=0.05): - values = [] - for idx in range(len(self) // 2): - sat = 0.05 * idx - values.append(sat) - self.left[idx].set_luminance(sat) - sleep(0.03) - self.right[idx].set_luminance(sat) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self.left[idx].set_luminance(value) - sleep(0.03) - self.right[idx].set_luminance(value) - sleep(speed) - - def hue_spin(self, speed=0.05): - values = [] - for idx in range(len(self) // 2): - sat = 0.083 * idx - values.append(sat) - self.left[idx].set_hue(sat) - sleep(0.03) - self.right[idx].set_hue(sat) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - for pixel in self: - print(pixel) - self.left[idx].set_hue(value) - sleep(0.03) - self.right[idx].set_hue(value) - sleep(speed) - - def flash(self, speed=0.2): - while True: - sleep(speed) - self.on() - sleep(speed) - self.off() - - def rainbow_flash(self, speed=0.2): - colors = ["red", "orange", "yellow", "green", "cyan", "blue", - "violet", "purple"] - while True: - for color in colors: - sleep(speed) - self.off() - self.change_color(color) - sleep(speed) - self.on() - - def beacon(self, speed=0.1): - values = [i + i for i in range(30)] - while True: - for value in values: - self.set_brightness(value) - sleep(speed) - values.reverse() - - def rainbow_beacon(self, speed=0.1): - values = [i + i for i in range(30)] - values += reversed(values) - colors = ["red", "orange", "yellow", "green", "cyan", "blue", - "violet", "purple"] - self.set_brightness(0) - while True: - for color in colors: - for value in values: - sleep(speed) - self.set_brightness(value) - self.change_color(color) - - # Arduino API - def set_hue(self, hue): - self.right.set_hue(hue) - sleep(0.05) - self.left.set_hue(hue) - - def set_brightness(self, level): - """ - Set the brightness of the eyes in the display. - Args: - level (int): 1-30, bigger numbers being brighter - """ - self.api.eyes_brightness(level) - - def spin(self): - self.api.eyes_spin() - - def timed_spin(self, length): - self.api.eyes_timed_spin(length) - - def reset(self): - self.api.eyes_reset() - - def fill_once(self, percent): - """ - Use the eyes as a type of progress meter - Args: - percent (int): 0-49 fills the right eye, 50-100 also covers left - """ - self.api.eyes_fill(percent) - - def look(self, side): - """Make the eyes look to the given side - Args: - side (str): 'r' for right - 'l' for left - 'u' for up - 'd' for down - 'c' for crossed - """ - self.api.eyes_look(side) - - def look_right(self): - self.look("r") - - def look_left(self): - self.look("l") - - def look_up(self): - self.look("u") - - def look_down(self): - self.look("d") - - def cross(self): - self.look("c") - - def narrow(self): - """Make the eyes look narrow, like a squint""" - self.api.eyes_narrow() - - def on(self): - """Illuminate or show the eyes.""" - self.api.eyes_on() - - def off(self): - """Turn off or hide the eyes.""" - self.api.eyes_off() - - def blink_once(self, side="b"): - """Make the eyes blink - Args: - side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' - """ - self.api.eyes_blink(side) - - def blink_right_once(self): - self.right.blink_once() - - def blink_left_once(self): - self.left.blink_once() - - def blink(self, speed=0.5): - """ - Make the eyes blink in a loop - """ - while True: - self.blink_once() - sleep(speed) - - def blink_right(self, speed=0.5): - """ - Make the right eye blink in a loop - """ - self.right.blink(speed) - - def blink_left(self, speed=0.5): - """ - Make the left eyes blink in a loop - """ - self.left.blink(speed) - - def blink_alternate(self, speed=0.5): - """ - Make the eyes blink in a loop - """ - while True: - self.blink_right_once() - sleep(speed) - self.blink_left_once() - sleep(speed) - - def up_down(self, speed=0.8): - """ - Make the eyes blink in a loop - """ - while True: - self.look_up() - sleep(speed) - self.look_down() - sleep(speed) - - def left_right(self, speed=0.8): - """ - Make the eyes blink in a loop - """ - while True: - self.look_left() - sleep(speed) - self.look_right() - sleep(speed) - - def fill(self, speed=0.1): - values = [i for i in range(101)] - values += reversed(values) - while True: - for percent in values: - self.fill_once(percent) - sleep(speed) - - def rainbow_fill(self, speed=0.1): - values = [i for i in range(101)] - values += reversed(values) - colors = ["red", "orange", "yellow", "green", "cyan", "blue", - "violet", "purple"] - while True: - for color in colors: - for percent in values: - self.fill_once(percent) - sleep(speed) - if percent == 100: - self.change_color(color) diff --git a/ovos_utils/enclosure/mark1/faceplate/__init__.py b/ovos_utils/enclosure/mark1/faceplate/__init__.py deleted file mode 100644 index f6fedfbd..00000000 --- a/ovos_utils/enclosure/mark1/faceplate/__init__.py +++ /dev/null @@ -1,410 +0,0 @@ -from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI -from ovos_utils import create_loop -from ovos_utils.log import LOG, log_deprecation -from ovos_utils.messagebus import get_mycroft_bus -import random -from time import sleep -from collections.abc import MutableSequence -import copy - -log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") - - -try: - from ovos_mark1.faceplate import * -except ImportError: - - class FaceplateGrid(MutableSequence): - encoded = None - str_grid = None - pad_char = "." - - def __init__(self, grid=None, bus=None): - self.bus = bus or get_mycroft_bus() - self._api = Mark1EnclosureAPI(self.bus) - self.grid = [] - for x in range(8): - self.grid.append([]) - for y in range(32): - self.grid[x].append(0) - if self.encoded: - self.grid = self.decode(self.encoded).grid - elif self.str_grid is not None: - self.grid = FaceplateGrid(bus=self.bus)\ - .from_string(self.str_grid).grid - elif grid is not None: - self.grid = grid - - @property - def height(self): - return len(self.grid) - - @property - def width(self): - return max([len(r) for r in self.grid]) - - def display(self, invert=True, clear=True, x_offset=0, y_offset=0): - self._api.mouth_display(self.encode(invert), - x_offset, y_offset, clear) - - def print(self, draw_padding=True, invert=False): - print(self.to_string(draw_padding=draw_padding, invert=invert)) - - def encode(self, invert=False): - # to understand how this function works you need to understand how the - # Mark I arduino proprietary encoding works to display to the faceplate - - # https://mycroft-ai.gitbook.io/docs/skill-development/displaying-information/mark-1-display - - # Each char value str_gridesents a width number starting with B=1 - # then increment 1 for the next. ie C=2 - width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', - 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] - - height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] - - encode = width_codes[self.width - 1] - encode += height_codes[self.height - 1] - - # Turn the image pixels into binary values 1's and 0's - # the Mark I face plate encoding uses binary values to - # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] - binary_values = [] - for i in range(self.width): # pixels - for j in range(self.height): # lines - pixels = self.grid[j] - - if pixels[i] is None: # padding - pixels[i] = 0 - - if pixels[i] != 0: - if invert is False: - binary_values.append('1') - else: - binary_values.append('0') - else: - if invert is False: - binary_values.append('0') - else: - binary_values.append('1') - # these values are used to determine how binary values - # needs to be grouped together - number_of_bottom_pixel = 0 - - if self.height > 4: - number_of_top_pixel = 4 - number_of_bottom_pixel = self.height - 4 - else: - number_of_top_pixel = self.height - - # this loop will group together the individual binary values - # ie. binary_list = ['1111', '001', '0101', '100'] - binary_list = [] - binary_code = '' - increment = 0 - alternate = False - for val in binary_values: - binary_code += val - increment += 1 - if increment == number_of_top_pixel and alternate is False: - # binary code is reversed for encoding - binary_list.append(binary_code[::-1]) - increment = 0 - binary_code = '' - alternate = True - elif increment == number_of_bottom_pixel and alternate is True: - binary_list.append(binary_code[::-1]) - increment = 0 - binary_code = '' - alternate = False - # Code to let the Mark I arduino know where to place the - # pixels on the faceplate - pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] - for binary_values in binary_list: - number = int(binary_values, 2) - pixel_code = pixel_codes[number] - encode += pixel_code - return encode - - def decode(self, encoded, invert=False, pad=True): - codes = list(encoded) - - # Each char value str_gridesents a width number starting with B=1 - # then increment 1 for the next. ie C=2 - width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', - 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] - - height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] - - height = height_codes.index(codes[1]) + 1 - width = width_codes.index(codes[0]) + 1 - - # Code to let the Mark I arduino know where to place the - # pixels on the faceplate - pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] - codes.reverse() - binary_list = [] - for pixel_code in codes[:-2]: - number = pixel_codes.index(pixel_code.upper()) - bin_str = str(bin(number))[2:] - while not len(bin_str) == 4: - bin_str = "0" + bin_str - binary_list += [bin_str] - - binary_list.reverse() - - for idx, binary_code in enumerate(binary_list): - # binary code is reversed for encoding - binary_list[idx] = binary_code[::-1] - - binary_code = "".join(binary_list) - - # Turn the image pixels into binary values 1's and 0's - # the Mark I face plate encoding uses binary values to - # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] - grid = [] - # binary_code is a sequence of column by column - cols = [list(binary_code)[x:x + height] for x in - range(0, len(list(binary_code)), height)] - - for x in range(height): - row = [] - for y in range(width): - bit = int(cols[y][x]) - if invert: - if bit: - bit = 0 - else: - bit = 1 - row.append(bit) - grid.append(row) - - # handle padding - if pad: - if width < self.width: - n = int((self.width - width) / 2) - if invert: - padding = [1] * n - else: - padding = [0] * n - for idx, row in enumerate(grid): - grid[idx] = padding + row + padding - if height < self.height: - pass # TODO vertical padding - self.grid = grid - return self - - def from_string(self, str_grid): - rows = [r for r in str_grid.split("\n") if len(r)] - grid = [] - for r in rows: - row = [] - for char in list(r): - if char == " ": - row.append(1) - elif char == FaceplateGrid.pad_char: - row.append(None) - else: - row.append(0) - while len(row) < self.width: - row.append(None) - grid.append(row) - self.grid = grid - return self - - def to_string(self, draw_padding=False, invert=False): - str_grid = "" - for row in self.grid: - line = "" - for col in row: - if col is None and draw_padding: - line += self.pad_char - elif col == 1: - if invert: - line += "X" - else: - line += " " - elif col == 0: - if invert: - line += " " - else: - line += "X" - str_grid += line + "\n" - return str_grid - - def invert(self): - for x in range(self.height): - for y in range(self.width): - if self.grid[x][y] == 0: - self.grid[x][y] = 1 - elif self.grid[x][y] == 1: - self.grid[x][y] = 0 - return self - - def clear(self): - for x in range(self.height): - for y in range(self.width): - self.grid[x][y] = 0 - return self - - @property - def is_empty(self): - for x in range(self.height): - for y in range(self.width): - if self.grid[x][y] == 1: - return False - return True - - def randomize(self, n=200): - for i in range(n): - x = random.randint(0, self.height-1) - y = random.randint(0, self.width-1) - self.grid[x][y] = int(random.randint(0, 1)) - return self - - def __len__(self): - # number of pixels - return self.width * self.height - - def __delitem__(self, index): - self.grid.__delitem__(index) - - def insert(self, index, value): - self.grid.insert(index - 1, value) - - def __setitem__(self, index, value): - self.grid.__setitem__(index, value) - - def __getitem__(self, index): - return self.grid.__getitem__(index) - - - class FacePlateAnimation(FaceplateGrid): - - def __init__(self, grid=None, bus=None): - super().__init__(grid, bus) - self.finished = False - - def animate(self): - pass - - def __iter__(self): - while not self.finished: - self.animate() - yield self - - def start(self): - self.finished = False - - def stop(self): - self.finished = True - - def run(self, delay=0.5, callback=None, daemonic=False): - self.start() - - if delay < 0.4: - # writer bugs out if sending messages too rapidly - delay = 0.4 - - def step(callback=callback): - try: - if not self.finished: - self.animate() - if callback: - callback(self) - except Exception as e: - LOG.error(e) - - if daemonic: - create_loop(step, delay) - else: - while not self.finished: - step() - sleep(delay) - self.stop() - - def scroll_down(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - self.grid[y][x] = old[y - 1][x] - - def scroll_up(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if y == self.height - 1: - self.grid[y][x] = old[0][x] - else: - self.grid[y][x] = old[y + 1][x] - - def scroll_right(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - self.grid[y][x] = old[y][x - 1] - - def scroll_left(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if x == self.width -1: - self.grid[y][x] = old[y][0] - else: - self.grid[y][x] = old[y][x + 1] - - def move_down(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if y - 1 < 0: - self.grid[y][x] = 0 - else: - self.grid[y][x] = old[y - 1][x] - - def move_up(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if y == self.height - 1: - self.grid[y][x] = 0 - else: - self.grid[y][x] = old[y + 1][x] - - def move_right(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - self.grid[y][x] = old[y][x - 1] - - def move_left(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if x == self.width - 1: - self.grid[y][x] = 0 - else: - self.grid[y][x] = old[y][x + 1] - - - class BlackScreen(FaceplateGrid): - # Basically a util class to handle - # inverting on __init__ - str_grid = """ - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.invert() diff --git a/ovos_utils/enclosure/mark1/faceplate/animations.py b/ovos_utils/enclosure/mark1/faceplate/animations.py deleted file mode 100644 index cc50e40f..00000000 --- a/ovos_utils/enclosure/mark1/faceplate/animations.py +++ /dev/null @@ -1,570 +0,0 @@ -import copy -import random - -from ovos_utils.log import log_deprecation - -log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") - - -try: - from ovos_mark1.faceplate.animations import * -except ImportError: - from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation, BlackScreen - - # Base animations - # These are mostly meant to be subclassed (empty animations) - class HorizontalScroll(FacePlateAnimation): - def __init__(self, direction="right", grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("r") or direction.startswith("l") - self.direction = direction[0] - - def animate(self): - if self.direction == "r": - self.scroll_right() - else: - self.scroll_left() - if self.is_empty: - self.stop() - - - class VerticalScroll(FacePlateAnimation): - def __init__(self, direction="up", - grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("u") or direction.startswith("d") - self.direction = direction[0] - - def animate(self): - if self.direction == "u": - self.scroll_up() - else: - self.scroll_down() - if self.is_empty: - self.stop() - - - class LeftRight(FacePlateAnimation): - def __init__(self, direction="right", start="left", grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("r") or direction.startswith("l") - self.direction = direction[0] - - # start at right/left side/center - inverted = not isinstance(self, BlackScreen) - if start[0] == "l": - # left side - for y in range(self.height): - for x in range(self.width): - if not inverted and self.grid[y][x] == 1: - pass - elif inverted and self.grid[y][x] == 0: - pass - elif start[0] == "r": - pass # right side - else: - pass # center - print(self.grid[1]) - - def animate(self): - left_collision = False - right_collision = False - inverted = not isinstance(self, BlackScreen) - for y in range(self.height): - if inverted: - if self.grid[y][self.width - 1] == 0: - right_collision = True - if self.grid[y][0] == 0: - left_collision = True - else: - if self.grid[y][self.width - 1] == 1: - right_collision = True - if self.grid[y][0] == 1: - left_collision = True - if left_collision and right_collision: - return # No space left to animate - elif right_collision: - self.direction = "l" - elif left_collision: - self.direction = "r" - if self.direction == "r": - self.scroll_right() - else: - self.scroll_left() - if self.is_empty: - self.stop() - - - class UpDown(FacePlateAnimation): - def __init__(self, direction="up", grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("u") or direction.startswith("d") - self.direction = direction[0] - - def animate(self): - top_collision = False - bottom_collision = False - for x in range(self.width): - if self.grid[0][x] == 1: - top_collision = True - if self.grid[self.height - 1][x] == 1: - bottom_collision = True - - if top_collision and bottom_collision: - return # No space left to animate - elif top_collision: - self.direction = "d" - elif bottom_collision: - self.direction = "u" - if self.direction == "u": - self.scroll_up() - else: - self.scroll_down() - if self.is_empty: - self.stop() - - - class CollisionBox(FacePlateAnimation): - def __init__(self, - horizontal_direction=None, - vertical_direction=None, - grid=None, bus=None): - super().__init__(grid, bus) - assert horizontal_direction is None or \ - horizontal_direction.startswith("r") or \ - horizontal_direction.startswith("l") - assert vertical_direction is None or \ - vertical_direction.startswith("u") or \ - vertical_direction.startswith("d") - self.vertical_direction = vertical_direction[0] if \ - vertical_direction else None - self.horizontal_direction = horizontal_direction[0] if \ - horizontal_direction else None - - def animate(self): - left_collision = False - right_collision = False - top_collision = False - bottom_collision = False - for y in range(self.height): - if self.grid[y][self.width - 1] == 1: - right_collision = True - if self.grid[y][0] == 1: - left_collision = True - for x in range(self.width): - if self.grid[0][x] == 1: - top_collision = True - if self.grid[self.height - 1][x] == 1: - bottom_collision = True - - if top_collision and bottom_collision: - self.vertical_direction = None - elif top_collision: - self.vertical_direction = "d" - elif bottom_collision: - self.vertical_direction = "u" - - if left_collision and right_collision: - self.horizontal_direction = None - elif right_collision: - self.horizontal_direction = "l" - elif left_collision: - self.horizontal_direction = "r" - - if self.vertical_direction is None: - pass - elif self.vertical_direction == "u": - self.scroll_up() - elif self.vertical_direction == "d": - self.scroll_down() - - if self.horizontal_direction is None: - pass - elif self.horizontal_direction == "r": - self.scroll_right() - elif self.horizontal_direction == "l": - self.scroll_left() - - if self.is_empty: - self.stop() - - - # Ready to use animations - class SquareWave(HorizontalScroll): - def __init__(self, direction="r", frequency=3, - amplitude=4, grid=None, bus=None): - super().__init__(direction, grid, bus) - # frequency must be > 1 - # frequency is in number of pixels - assert 0 < frequency - # amplitude must be 2, 4 or 6. else it renders badly - # amplitude is in number of pixels - assert 0 < amplitude < self.height - assert divmod(amplitude, 2)[1] == 0 - - self.freq = frequency - self.amplitude = self.height - amplitude - - self._initial_grid() - self.invert() - - def _initial_grid(self): - # draws the initial state - count = 0 - top = True - a = self.amplitude // 2 - 1 - for x in range(self.width): - if top: - self.grid[a + 1][x] = 1 - else: - self.grid[-a - 1][x] = 1 - - for y in range(self.height): - if count == 0: - self.grid[y][x] = 1 - if y <= a: - self.grid[y][x] = 0 - elif y >= self.height - a: - self.grid[y][x] = 0 - count += 1 - if count == self.freq + 1: - count = 0 - top = not top - - - class StrayDot(CollisionBox): - def __init__(self, - start_x=None, - start_y=None, - horizontal_direction=None, - vertical_direction=None, - grid=None, bus=None): - horizontal_direction = horizontal_direction or \ - random.choice(["l", "r"]) - vertical_direction = vertical_direction or \ - random.choice(["u", "d"]) - super().__init__(horizontal_direction, vertical_direction, - grid, bus) - start_x = start_x or random.randint(0, self.width - 1) - start_y = start_y or random.randint(0, self.height - 1) - self.grid[start_y][start_x] = 1 - - - class ParticleBox(FacePlateAnimation): - def __init__(self, n_particles=5, bus=None): - super().__init__(bus=bus) - assert 0 < n_particles < 11 - self.n_particles = n_particles - self.particles = [] - - class Dot: - def __init__(self, idx, x, y, vx, vy): - self.x = x - self.y = y - self.vx = vx - self.vy = vy - self.idx = idx - - for i in range(n_particles): - vx = random.choice(["l", "r"]) - vy = random.choice(["u", "d"]) - x = random.randint(0, self.width - 1) - y = random.randint(0, self.height - 1) - while self.grid[y][x] == 1: - # 2 particles can't occupy same space - x = random.randint(0, self.width - 1) - y = random.randint(0, self.height - 1) - self.grid[y][x] = 1 - self.particles.append(Dot(i, x, y, vx, vy)) - - def render_particles(self): - self.clear() - for p in self.particles: - self.grid[p.y][p.x] = 1 - - def get_particle(self, x, y): - for p in self.particles: - if p.x == x and p.y == y: - return p - - def process_collisions(self): - # new particles after this turn - new_particles = copy.deepcopy(self.particles) - - # NOTE this is not a physics simulation! - # while it is behaving like an elastic collision - # if there is a 3+ particle collision results will be incorrect - # as long as only 2 particles collide it looks accurate - # max number of particles limited to 10 to minimize chance of this - # happening - for p in self.particles: - idx = p.idx - - # horizontal movement - if p.vx is None: - # not moving horizontally - if p.x != 0: - # check for collisions from left p2 -> p1 - p2 = self.get_particle(p.x - 1, p.y) - if p2 and p2.vx == "r": - # collision p2 -> p1 - if p.x == self.width - 1: - new_particles[idx].vx = None - else: - new_particles[idx].vx = "r" - new_particles[idx].x += 1 - if p.x != self.width - 1: - # check for collisions from right p1 <- p2 - p2 = self.get_particle(p.x + 1, p.y) - if p2 and p2.vx == "l": - # collision p1 <- p2 - if p.x == 0: - new_particles[idx].vx = None - else: - new_particles[idx].vx = "l" - new_particles[idx].x -= 1 - # moving right - elif p.vx == "r": - p2 = self.get_particle(p.x + 1, p.y) - if p.x == self.width - 1: - # border collision p1 -> | - new_particles[idx].vx = "l" - new_particles[idx].x -= 1 - elif p2: - # particle collision p1 -> p2 - if p2.vx is None: - # p2 moves, p1 stops - new_particles[idx].vx = None - elif p2.vx == "r": - # moving together - new_particles[idx].x += 1 - elif p2 and p2.vx == "l": - if p.x == 0: - new_particles[idx].vx = None - else: - # both change direction - new_particles[idx].vx = "l" - new_particles[idx].x -= 1 - else: - # move right - new_particles[idx].x += 1 - # moving left - elif p.vx == "l": - p2 = self.get_particle(p.x - 1, p.y) - if p.x == 0: - # border collision | <- p1 - new_particles[idx].vx = "r" - new_particles[idx].x += 1 - elif p2: - # particle collision p2 <- p1 - - if p2.vx is None: - # p2 moves, p1 stops - new_particles[idx].vx = None - elif p2.vx == "l": - # moving together, no collision - new_particles[idx].x -= 1 - elif p2.vx == "r": - if p.x == self.width - 1: - new_particles[idx].vx = None - else: - # both change direction - new_particles[idx].vx = "r" - new_particles[idx].x += 1 - else: - # move left - new_particles[idx].x -= 1 - - # vertical movement - if p.vy is None: - # not moving vertically - if p.y != 0: - # check for collisions from top p2 -> p1 - p2 = self.get_particle(p.x, p.y - 1) - if p2 and p2.vy == "d": - if p.y == self.height - 1: - new_particles[idx].vy = None - else: - # collision p2 -> p1 - new_particles[idx].vy = "d" - new_particles[idx].y += 1 - if p.y != self.height - 1: - # check for collisions from bottom p1 <- p2 - p2 = self.get_particle(p.x, p.y + 1) - if p2 and p2.vy == "u": - # collision p1 <- p2 - if p.y == 0: # on top - new_particles[idx].vy = None - else: - new_particles[idx].vy = "u" - new_particles[idx].y -= 1 - # moving down - elif p.vy == "d": - p2 = self.get_particle(p.x, p.y + 1) - if p.y == self.height - 1: - # border collision p1 -> | - new_particles[idx].vy = "u" - new_particles[idx].y -= 1 - elif p2: - # particle collision p1 -> p2 - - if p2.vy is None: - # p2 moves, p1 stops - new_particles[idx].vy = None - elif p2.vy == "d": - # moving together - new_particles[idx].y += 1 - elif p2.vy == "u": - if p.y == 0: - new_particles[ - idx].vy = None # wall absorbed momentum - else: - # both change direction - new_particles[idx].vy = "u" - new_particles[idx].y -= 1 - else: - # move down - new_particles[idx].y += 1 - # moving up - elif p.vy == "u": - - p2 = self.get_particle(p.x, p.y - 1) - if p.y == 0: - # border collision | <- p1 - new_particles[idx].vy = "d" - new_particles[idx].y += 1 - elif p2: - # particle collision p2 <- p1 - if p2.vy is None: - # p2 moves, p1 stops - new_particles[idx].vy = None - elif p2.vy == "u": - # moving together, no collision - new_particles[idx].y -= 1 - elif p2.vy == "d": - # both change direction - if p.y == self.height - 1: - new_particles[idx].vy = None - else: - new_particles[idx].vy = "d" - new_particles[idx].y += 1 - else: - # move left - new_particles[idx].y -= 1 - - # update processed particles - self.particles = new_particles - - def animate(self): - self.process_collisions() - self.render_particles() - - - class FallingDots(FacePlateAnimation): - def __init__(self, n=10, bus=None): - super().__init__(bus=bus) - self._create = True - assert 0 < n < 32 - self.n = n - - @property - def n_dots(self): - n = 0 - for y in range(self.height): - for x in range(self.width): - if self.grid[y][x]: - n += 1 - return n - - def animate(self): - self.move_down() - if self._create: - if random.choice([True, False]): - self._create = False - x = random.randint(0, self.width - 1) - self.grid[0][x] = 1 - if self.n_dots < self.n: - self._create = True - - - class StraightParticleShooter(FacePlateAnimation): - def __init__(self, period=None, bus=None): - super().__init__(bus=bus) - self.direction = "d" - self.period = period - self.counter = 0 - # draw shooter - self.grid[0][0] = 1 - self.grid[1][0] = 1 - self.grid[1][1] = 1 - self.grid[2][0] = 1 - - def line_down(self): - old = copy.deepcopy(self.grid) - for y in range(self.height): - self.grid[y][0] = old[y - 1][0] - self.grid[y][1] = old[y - 1][1] - - def line_up(self): - old = copy.deepcopy(self.grid) - for y in range(self.height): - if y == self.height - 1: - self.grid[y][0] = old[0][0] - self.grid[y][1] = old[0][1] - else: - self.grid[y][0] = old[y + 1][0] - self.grid[y][1] = old[y + 1][1] - - def scroll_particles(self): - old = copy.deepcopy(self.grid) - for x in range(2, self.width): - for y in range(self.height): - if old[y][x] == 1: - self.grid[y][x] = 0 - if x < self.width - 1: - self.grid[y][x + 1] = 1 - - @property - def num_particles(self): - n = 0 - for x in range(2, self.width): - for y in range(self.height): - if self.grid[y][x] == 1: - n += 1 - return n - - @property - def line(self): - for y in range(self.height): - if self.grid[y][0] == 1: - return y - return 0 - - def animate(self): - # collision detection - top_collision = False - bottom_collision = False - if self.grid[0][0] == 1: - top_collision = True - elif self.grid[self.height - 1][0] == 1: - bottom_collision = True - if top_collision: - self.direction = "d" - elif bottom_collision: - self.direction = "u" - - # bounce the "emitter" up and down - if self.direction == "u": - self.line_up() - else: - self.line_down() - - # create particles - period = self.period or random.randint(0, 20) - if self.num_particles < 1 or self.counter >= period: - self.grid[self.line + 1][2] = 1 - self.counter = 0 - - # animate particles - self.scroll_particles() - self.counter += 1 diff --git a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py b/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py deleted file mode 100644 index 23198546..00000000 --- a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py +++ /dev/null @@ -1,485 +0,0 @@ -import copy -import random - -from ovos_utils.log import log_deprecation - -log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") - - -try: - from ovos_mark1.faceplate.cellular_automaton import * -except: - from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation - - - # Game of Life Base - class GoL(FacePlateAnimation): - - def __init__(self, entropy=0, grid=None, bus=None): - super().__init__(grid, bus) - self.entropy = entropy - if self.is_empty: - self.randomize() - - def _live_neighbours(self, y, x): - """Returns the number of live neighbours.""" - count = 0 - if y > 0: - if self.grid[y - 1][x]: - count = count + 1 - if x > 0: - if self.grid[y - 1][x - 1]: - count = count + 1 - if self.width > (x + 1): - if self.grid[y - 1][x + 1]: - count = count + 1 - - if x > 0: - if self.grid[y][x - 1]: - count = count + 1 - if self.width > (x + 1): - if self.grid[y][x + 1]: - count = count + 1 - - if self.height > (y + 1): - if self.grid[y + 1][x]: - count = count + 1 - if x > 0: - if self.grid[y + 1][x - 1]: - count = count + 1 - if self.width > (x + 1): - if self.grid[y + 1][x + 1]: - count = count + 1 - - return count - - def animate(self): - """Game of Life turn""" - nt = copy.deepcopy(self.grid) - for y in range(0, self.height): - for x in range(0, self.width): - neighbours = self._live_neighbours(y, x) - if self.grid[y][x] == 0: - if neighbours == 3: - nt[y][x] = 1 - else: - if (neighbours < 2) or (neighbours > 3): - nt[y][x] = 0 - if nt == self.grid and self.entropy <= 0: - self.stop() - self.grid = nt - self.randomize(self.entropy) - if self.is_empty: - self.stop() - - - # Langtons Ant base - class _Ant: - def __init__(self, x, y, direction, height=8, width=32): - self.x = x - self.y = y - assert direction[0] in ["r", "l", "u", "d"] - self.direction = direction[0] - self.grid_height = height - self.grid_width = width - self.dead = False - - def move_forward(self): - if self.direction == "r": - self.x += 1 - if self.x == self.grid_width: - self.dead = True - elif self.direction == "d": - self.y += 1 - if self.y == self.grid_height: - self.dead = True - elif self.direction == "l": - self.x -= 1 - if self.x == -1: - self.dead = True - elif self.direction == "u": - self.y -= 1 - if self.y == -1: - self.dead = True - - def turn_right(self): - if self.direction == "r": - self.direction = "d" - elif self.direction == "d": - self.direction = "l" - elif self.direction == "l": - self.direction = "u" - elif self.direction == "u": - self.direction = "r" - - def turn_left(self): - if self.direction == "r": - self.direction = "u" - elif self.direction == "d": - self.direction = "r" - elif self.direction == "l": - self.direction = "d" - elif self.direction == "u": - self.direction = "l" - - - class _InfiniteAnt(_Ant): - def move_forward(self): - if self.direction == "r": - self.x += 1 - if self.x == self.grid_width: - self.x = 0 - elif self.direction == "d": - self.y += 1 - if self.y == self.grid_height: - self.y = 0 - elif self.direction == "l": - self.x -= 1 - if self.x == -1: - self.x = self.grid_width - 1 - elif self.direction == "u": - self.y -= 1 - if self.y == -1: - self.y = self.grid_height - 1 - - - class _ReverseAnt(_Ant): - def turn_left(self): - super().turn_right() - - def turn_right(self): - super().turn_left() - - - class _ReverseInfiniteAnt(_InfiniteAnt): - def turn_left(self): - super().turn_right() - - def turn_right(self): - super().turn_left() - - - class LangtonsAnt(FacePlateAnimation): - def __init__(self, ants=1, continuous=True, gen_reverse=False, - grid=None, bus=None): - super().__init__(grid=grid, bus=bus) - self.ants = [] - # if continuous loops around the board - # height + 1 -> 0 - # width + 1 -> 0 - # else ant is removed - self.continuous = continuous - if isinstance(ants, int): - # spawn N ants - assert 0 <= ants < 256 - for i in range(ants): - x = random.randint(0, self.width - 1) - y = random.randint(0, self.height - 1) - direction = random.choice(["u", "d", "l", "r"]) - reverse = False - if gen_reverse: - reverse = random.choice([True, False]) - ant = self.ant_factory(x, y, direction, reverse) - self.ants.append(ant) - elif isinstance(ants, list): - # Ant objects - self.ants = ants - for ant in self.ants: - assert isinstance(ant, _Ant) - else: - raise ValueError - - def ant_factory(self, x, y, direction, reverse=False): - # reverse ants exit black squares to the opposite direction - if self.continuous: - # loops around the board instead of dying - if reverse: - return _ReverseInfiniteAnt(x, y, direction) - return _InfiniteAnt(x, y, direction) - if reverse: - return _ReverseAnt(x, y, direction) - return _Ant(x, y, direction) - - def move_ants(self): - # copy grid, multiple ants might want to flip same square - # end result is the same so this is not a problem as long as it does - # not change during iteration - old_grid = copy.deepcopy(self.grid) - - for idx, ant in enumerate(self.ants): - if self.ants[idx].dead: - continue - if old_grid[ant.y][ant.x] == 1: - # black - self.ants[idx].turn_left() - else: - # white - self.ants[idx].turn_right() - # flip color - self.grid[ant.y][ant.x] = not old_grid[ant.y][ant.x] - # update ant position - self.ants[idx].move_forward() - - def animate(self): - self.move_ants() - # Stop condition -> all ants moved out of the board - dead_ants = [ant for ant in self.ants if ant.dead] - if len(dead_ants) == len(self.ants): - self.stop() - - - # Game of Life Animations - class SpaceInvader(GoL): - # This basically half "pulsar" - str_grid = """ - XXXXXXXXXXXX XXX XXXXXXXXXXXXX - XXXXXXXXXX X X X X X XXXXXXXXXXX - XXXXXXXX XX X XX XXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXX XXX XXXXXXXXXXXXX - XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX - XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - """ - - - # Langton's Ant animations - - # Single Ant - class LangtonsLineDisplacer(LangtonsAnt): - # see pattern here - # https://youtu.be/w6XQQhCgq5c?t=84 - - def __init__(self, x=None, y=None, continuous=True, bus=None): - super().__init__(0, continuous, bus=bus) - x = x if x is not None else random.randint(0, self.width - 1) - y = y if y is not None else random.randint(0, self.height - 1) - ant = self.ant_factory(x, y - 1, "u") - self.ants.append(ant) - # create initial line - for i in range(0, self.width): - self.grid[y][i] = 1 - - - # 2 Ants - class LangtonsAntsOscillator(LangtonsAnt): - # see pattern here - # https://youtu.be/w6XQQhCgq5c?t=103 - - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = "d" - dir2 = "u" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - - class LangtonsAntsOscillator2(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = dir2 = "u" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - - class LangtonsAntsOscillator3(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = "l" - dir2 = "r" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - - class LangtonsAntsOscillator4(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = dir2 = "l" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - - class LangtonsAntsOscillator5(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = "u" - dir2 = "d" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - - class LangtonsAntTrail(LangtonsAnt): - # see pattern here - # https://youtu.be/w6XQQhCgq5c?t=159 - - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x = x if x is not None else random.randint(0, self.width - 1) - y = y if y is not None else random.randint(0, self.height - 1) - dir1 = "u" - dir2 = "d" - ant1 = self.ant_factory(x, y - 1, dir1) - ant2 = self.ant_factory(x, y, dir2, reverse=True) - self.ants += [ant1, ant2] - - - class LangtonsAntDotTransporter(LangtonsAnt): - # https://youtu.be/w6XQQhCgq5c?t=171 - - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x = x if x is not None else random.randint(0, self.width - 1) - y = y if y is not None else random.randint(0, self.height - 1) - dir1 = dir2 = "u" - ant1 = self.ant_factory(x, y, dir1) - ant2 = self.ant_factory(x + 1, y, dir2, reverse=True) - self.ants += [ant1, ant2] - # initial grid - if y + 1 == self.height: - self.grid[0][x + 1] = 1 # bellow anti-ant - else: - self.grid[y + 1][x + 1] = 1 # bellow anti-ant - self.grid[y - 1][x] = 1 # above ant - - - # 1D / elementar automata - - class ElementarAutomata(FacePlateAnimation): - def __init__(self, direction="u", idx=0, seed=None, grid=None, bus=None): - super().__init__(grid, bus) - assert direction[0] in ["u", "d", "l", "r"] - self.direction = direction[0] - self.row = idx - self.initial_state(seed) - - def initial_state(self, seed=None): - if seed is not None: - line = seed - else: - line = [0 for i in range(self.width)] - self.grid[self.row] = line - - def rule(self): - # process the line - raise NotImplementedError - - def animate(self): - old = copy.deepcopy(self.grid) - new_line = self.rule() - if self.direction == "u": - self.move_up() - elif self.direction == "d": - self.move_down() - elif self.direction == "l": - self.move_left() - elif self.direction == "r": - self.move_right() - self.grid[self.row] = new_line - if old == self.grid: - self.stop() - - - class SierpinskiTriangle(ElementarAutomata): - def __init__(self, direction="u", seed=None, bus=None): - assert direction[0] in ["u", "d"] - if direction[0] == "u": - idx = -1 - else: - idx = 0 - super().__init__(direction, idx, seed=seed, bus=bus) - - def initial_state(self, seed=None): - if seed is not None: - line = seed - else: - line = [0 for i in range(self.width)] - idx = self.width // 2 - line[idx] = 1 - self.grid[self.row] = line - - def rule(self): - new_line = copy.deepcopy(self.grid[self.row]) - # handle middle - for i in range(1, self.width - 1): - left = self.grid[self.row][i - 1] - right = self.grid[self.row][i + 1] - if left and right: - new_line[i] = 0 - elif left or right: - new_line[i] = 1 - else: - new_line[i] = 0 - - # handle edges - # if self.grid[0][1] == 1: - # new_line[0] = 1 - # else: - # new_line[0] = 0 - # if self.grid[0][-2] == 1: - # new_line[-1] = 1 - # else: - # new_line[-1] = 0 - return new_line - - - class Rule110(ElementarAutomata): - def __init__(self, direction="u", seed=None, bus=None): - assert direction[0] in ["u", "d"] - if direction[0] == "u": - idx = -1 - else: - idx = 0 - super().__init__(direction, idx, seed=seed, bus=bus) - - def initial_state(self, seed=None): - if seed is not None: - line = seed - else: - line = [0 for i in range(self.width)] - idx = self.width - 1 - line[idx] = 1 - self.grid[self.row] = line - - def rule(self): - new_line = copy.deepcopy(self.grid[self.row]) - # handle middle - for i in range(1, self.width - 1): - left = self.grid[self.row][i - 1] - mid = self.grid[self.row][i] - right = self.grid[self.row][i + 1] - if str(left) + str(mid) + str(right) in \ - ["110", "101", "011", "010", "001"]: - new_line[i] = 1 - else: - new_line[i] = 0 - - # handle edges - if self.grid[0][1] == 1: - new_line[0] = 1 - else: - new_line[0] = 0 - if self.grid[0][-2] == 1: - new_line[-1] = 1 - else: - new_line[-1] = 0 - return new_line diff --git a/ovos_utils/enclosure/mark1/faceplate/icons.py b/ovos_utils/enclosure/mark1/faceplate/icons.py deleted file mode 100644 index f49c1e02..00000000 --- a/ovos_utils/enclosure/mark1/faceplate/icons.py +++ /dev/null @@ -1,239 +0,0 @@ -from ovos_utils.log import log_deprecation - -log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") - - -try: - from ovos_mark1.faceplate.icons import * -except: - from ovos_utils.enclosure.mark1.faceplate import FaceplateGrid, BlackScreen - - class MusicIcon(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXX XXXXXXXXXXXXX - XXXXXXXXXXXXXX XXXXXXXXXXXXX - XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX - XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX - XXXXXXXXXXXXX XX XXXXXXXXXXXXX - XXXXXXXXXXXX X XXXXXXXXXXXXX - XXXXXXXXXXXXX XXX XXXXXXXXXXXXXX - """ - - - class PlusIcon(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - """ - - - class HeartIcon(FaceplateGrid): - str_grid = """ - - xx xx - xxxx xxxx - xxxxxxxxx - xxxxxxx - xxxxx - xxx - x - """ - - - class HollowHeartIcon(FaceplateGrid): - str_grid = """ - - xx xx - x x x x - x x x - x x - x x - x x - x - """ - - - class SkullIcon(FaceplateGrid): - str_grid = """ - - xxxxxxx - x xxx x - xxxxxxxxx - xxx xxx - xxxxx - x x x - x x x - """ - - - class DeadFishIcon(FaceplateGrid): - str_grid = """ - - x xxxx - x x x x xx xxx - xxxxxxxxxxxxxxx - x x x x xxxxxx - x xxxx - """ - - - class InfoIcon(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXX - """ - - - class ArrowLeftIcon(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXXXXXX - XXXXXXXXXXXX XXXXXXXXXXXXXXXXX - XXXXXXXXXXX X XXXXXXXXXX - XXXXXXXXXX X XXXXXXXXXX - XXXXXXXXXXX X XXXXXXXXXX - XXXXXXXXXXXX XXXXXXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXXXXXX - """ - - - class WarningIcon(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX - XXXXXXXXXXXXXX XXXXXXXXXXXXXXX - XXXXXXXXXXXXX X XXXXXXXXXXXXXX - XXXXXXXXXXXX XXX XXXXXXXXXXXXX - XXXXXXXXXXX XXX XXXXXXXXXXXX - XXXXXXXXXX XXXXXXXXXXX - XXXXXXXXX XXX XXXXXXXXXX - XXXXXXXX X XXXXXXXXX - """ - - - class CrossIcon(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX - XXXXXXXXXXXX XXX XXXXXXXXXXX - XXXXXXXXXXXXX X XXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXX X XXXXXXXXXXXX - XXXXXXXXXXXX XXX XXXXXXXXXXX - XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX - """ - - - class JarbasAI(BlackScreen): - str_grid = """ - X XXXXXXXXXXXXXXXXXXXXXXX X - XX XXXXXXXXXXXXXXXXXXXXXXXX X XX - XX XXXXXXXXXX XXXXXXXXXXXXX X X - XX XXXXXXXXXX XXXXXXXXX X X - XX XX X X X XX X XXX X X - X X XX X XX XX X XX X X X X - X X XX X XXX XX X XX XXX X X X - X X XXX X X X X X - """ - - - class SpaceInvader1(BlackScreen): - str_grid = """ - XXXXXXXXXXXXXX XXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXX x x XXXXXXXXX - XXXXXXXXXX XX XX XXXXXXXXX - XXXXXXXXXXXXXX XXXXXXXXXXXXX - XXXXXXXXXX XXXXXXXXX - XXXXXXXXXX XXXXXXXXXXX XXXXXXXXX - XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - """ - - - class SpaceInvader2(BlackScreen): - str_grid = """ - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXXXX x x XXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXX X X XXXXXXXXX - XXXXXXXXXX XXXXXXXXX - XXXXXXXXXX XXX XXX XXXXXXXXX - XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX - XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX - """ - - - class SpaceInvader3(BlackScreen): - str_grid = """ - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXX - XXXXXXXXXXXX X X XXXXXXXXXXX - XXXXXXXXXX XXXXXXXXX - XXXXXXXXXX XX XXX XX XXXXXXXXX - XXXXXXXXXXXXXX XXXXXXXXXXXXX - XXXXXXXXXXXXX XXX XXXXXXXXXXXX - """ - - - class SpaceInvader4(BlackScreen): - str_grid = """ - XXXXXXXXXXXXX XXXXXXXXXXXX - XXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXXXXXXXXXXXX XXXXXXXXXXX - XXXXXXXXXXXX X XXXXXXXXXXX - XXXXXXXXXX XXXXXXXXX - XXXXXXXXXX XX XX XXXXXXXXX - XXXXXXXXXXXXXX X XXXXXXXXXXXXX - XXXXXXXXXXXXX XXX XXXXXXXXXXXX - """ - - - # Encoded icons - class Boat(BlackScreen): - encoded = "QIAAABACAGIEMEOEPHAEAGACABABAAAAAA" - - - # Default weather icons for mark1 - class SunnyIcon(FaceplateGrid): - encoded = "IICEIBMDNLMDIBCEAA" - - - class PartlyCloudyIcon(FaceplateGrid): - encoded = "IIEEGBGDHLHDHBGEEA" - - - class CloudyIcon(FaceplateGrid): - encoded = "IIIBMDMDODODODMDIB" - - - class LightRainIcon(FaceplateGrid): - encoded = "IIMAOJOFPBPJPFOBMA" - - - class RainIcon(FaceplateGrid): - encoded = "IIMIOFOBPFPDPJOFMA" - - - class StormIcon(FaceplateGrid): - encoded = "IIAAIIMEODLBJAAAAA" - - - class SnowIcon(FaceplateGrid): - encoded = "IIJEKCMBPHMBKCJEAA" - - - class WindIcon(FaceplateGrid): - encoded = "IIABIBIBIJIJJGJAGA" diff --git a/ovos_utils/events.py b/ovos_utils/events.py index 8c603bf7..cef7c169 100644 --- a/ovos_utils/events.py +++ b/ovos_utils/events.py @@ -5,7 +5,7 @@ from ovos_utils.fakebus import Message, FakeBus, dig_for_message from ovos_utils.file_utils import to_alnum -from ovos_utils.log import LOG, log_deprecation, deprecated +from ovos_utils.log import LOG def unmunge_message(message, skill_id: str): @@ -206,17 +206,8 @@ def clear(self): class EventSchedulerInterface: """Interface for accessing the event scheduler over the message bus.""" - def __init__(self, name=None, sched_id=None, bus=None, skill_id=None): - # NOTE: can not rename or move sched_id/name arguments to keep api - # compatibility - if name: - log_deprecation("name argument has been deprecated! " - "use skill_id instead", "0.1.0") - if sched_id: - log_deprecation("sched_id argument has been deprecated! " - "use skill_id instead", "0.1.0") - - self.skill_id = skill_id or sched_id or name or self.__class__.__name__ + def __init__(self, bus=None, skill_id=None): + self.skill_id = skill_id or self.__class__.__name__.lower() self.bus = bus self.events = EventContainer(bus) self.scheduled_repeats = [] @@ -230,15 +221,14 @@ def set_bus(self, bus): self.bus = bus self.events.set_bus(bus) - def set_id(self, sched_id: str): + def set_id(self, skill_id: str): """ Attach the skill_id of the parent skill Args: - sched_id (str): skill_id of the parent skill + skill_id (str): skill_id of the parent skill """ - # NOTE: can not rename sched_id kwarg to keep api compatibility - self.skill_id = sched_id + self.skill_id = skill_id def _get_source_message(self): message = dig_for_message() or Message("") @@ -427,39 +417,3 @@ def shutdown(self): """ self.cancel_all_repeating_events() self.events.clear() - - @property - @deprecated("self.sched_id has been deprecated! use self.skill_id instead", - "0.1.0") - def sched_id(self): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and returns self.skill_id - """ - return self.skill_id - - @sched_id.setter - @deprecated("self.sched_id has been deprecated! use self.skill_id instead", - "0.1.0") - def sched_id(self, skill_id): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and sets self.skill_id - """ - self.skill_id = skill_id - - @property - @deprecated("self.name has been deprecated! use self.skill_id instead", - "0.1.0") - def name(self): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and returns self.skill_id - """ - return self.skill_id - - @name.setter - @deprecated("self.name has been deprecated! use self.skill_id instead", - "0.1.0") - def name(self, skill_id): - """DEPRECATED: do not use, method only for api backwards compatibility - Logs a warning and sets self.skill_id - """ - self.skill_id = skill_id diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py index 1ce41efd..ace07c7a 100644 --- a/ovos_utils/file_utils.py +++ b/ovos_utils/file_utils.py @@ -3,19 +3,16 @@ import os import re import tempfile +from os import walk +from os.path import dirname, splitext, join from threading import RLock from typing import Optional, List -from os import walk -from os.path import dirname -from os.path import splitext, join - from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer from ovos_utils.bracket_expansion import expand_options from ovos_utils.log import LOG, log_deprecation -from ovos_utils.system import search_mycroft_core_location def to_alnum(skill_id: str) -> str: diff --git a/ovos_utils/fingerprinting.py b/ovos_utils/fingerprinting.py deleted file mode 100644 index 073e81b3..00000000 --- a/ovos_utils/fingerprinting.py +++ /dev/null @@ -1,471 +0,0 @@ -import platform -import socket -from enum import Enum -from os.path import join, isfile -from ovos_utils.log import LOG, deprecated -from ovos_utils.system import is_installed, is_running_from_module, has_screen, \ - get_desktop_environment, search_mycroft_core_location, is_process_running - - -class MycroftPlatform(str, Enum): - PICROFT = "picroft" - BIGSCREEN = "kde" - OVOS = "OpenVoiceOS" - HIVEMIND = "HiveMind" - MARK1 = "mycroft_mark_1" - MARK2 = "mycroft_mark_2" - HOLMESV = "HolmesV" - OLD_HOLMES = "mycroft-lib" - NEON = "neon_core" - CHATTERBOX = "chatterbox" - OTHER = "unknown" - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def detect_platform(): - return max(((k, v) for k, v in classify_fingerprint().items()), - key=lambda k: k[1])[0] - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def get_config_fingerprint(config=None): - if not config: - try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config() - except ImportError: - LOG.warning("Config not provided and ovos_config not available") - config = dict() - conf = config - listener_conf = conf.get("listener", {}) - skills_conf = conf.get("skills", {}) - return { - "enclosure": conf.get("enclosure", {}).get("platform"), - "data_dir": conf.get("data_dir"), - "msm_skills_dir": skills_conf.get("msm", {}).get("directory"), - "ipc_path": conf.get("ipc_path"), - "input_device_name": listener_conf.get("device_name"), - "input_device_index": listener_conf.get("device_index"), - "default_audio_backend": conf.get("Audio", {}).get("default-backend"), - "priority_skills": skills_conf.get("priority_skills"), - "backend_url": conf.get("server", {}).get("url") - } - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def get_platform_fingerprint(): - return { - "hostname": socket.gethostname(), - "platform": platform.platform(), - "python_version": platform.python_version(), - "system": platform.system(), - "version": platform.version(), - "arch": platform.machine(), - "release": platform.release(), - "desktop_env": get_desktop_environment(), - "mycroft_core_location": search_mycroft_core_location(), - "can_display": has_screen(), - "is_gui_installed": is_installed("mycroft-gui-app"), - "is_vlc_installed": is_installed("vlc"), - "pulseaudio_running": is_process_running("pulseaudio"), - "core_supports_xdg": core_supports_xdg(), - "core_version": { - "version_str": get_mycroft_version(), - "is_chatterbox_core": is_chatterbox_core(), - "is_neon_core": is_neon_core(), - "is_holmes": is_holmes(), - "is_ovos": is_ovos(), - "is_mycroft_core": is_mycroft_core() - } - } - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def get_fingerprint(): - finger = get_platform_fingerprint() - finger["configuration"] = get_config_fingerprint() - return finger - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def core_supports_xdg(): - return True # no longer optional - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def get_mycroft_version(): - try: # ovos - from mycroft.version import OVOS_VERSION_STR - return OVOS_VERSION_STR - except ImportError: - pass - try: # mycroft - from mycroft.version import CORE_VERSION_STR - return CORE_VERSION_STR - except ImportError: - pass - - root = search_mycroft_core_location() - if root: - version_file = join(root, "version", "__init__.py") - if not isfile(version_file): - version_file = join(root, "mycroft", "version", "__init__.py") - if isfile(version_file): - version = [] - with open(version_file) as f: - text = f.read() - version.append( - text.split("CORE_VERSION_MAJOR =")[-1].split("\n")[ - 0].strip()) - version.append( - text.split("CORE_VERSION_MINOR =")[-1].split("\n")[ - 0].strip()) - version.append( - text.split("CORE_VERSION_BUILD =")[-1].split("\n")[ - 0].strip()) - version = ".".join(version) - if "CORE_VERSION_STR = '.'.join(map(str, " \ - "CORE_VERSION_TUPLE)) + " in text: - version += text.split( - "CORE_VERSION_STR = '.'.join(map(str, " - "CORE_VERSION_TUPLE)) + ")[-1].split("\n")[0][1:-1] - return version - return None - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_chatterbox_core(): - try: - import chatterbox - return True - except ImportError: - return False - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_neon_core(): - try: - import neon_core - return True - except ImportError: - return False - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_mycroft_core(): - try: - import mycroft - return True - except ImportError: - return False - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_vanilla_mycroft_core(): - return is_mycroft_core() and not is_ovos() - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_holmes(): - return "HolmesV" in (get_mycroft_version() or "") or is_mycroft_lib() - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_mycroft_lib(): - return "mycroft-lib" in (get_mycroft_version() or "") - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def is_ovos(): - return is_running_from_module("ovos-core") - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def classify_platform_print(fingerprint=None): - fingerprint = fingerprint or get_platform_fingerprint() - # key, val pairs that indicate a certain platform - fingerprints = { - MycroftPlatform.PICROFT: { - "core_supports_xdg": False, - "core_version": {'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': False, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.BIGSCREEN: { - "core_supports_xdg": False, - "core_version": {'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': False, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.OVOS: { - "core_supports_xdg": True, - "core_version": {'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': False, - 'is_ovos': True, - 'is_mycroft_core': True} - }, - MycroftPlatform.MARK1: { - "core_supports_xdg": False, - "core_version": {'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': False, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.MARK2: { - "core_supports_xdg": False, - "core_version": {'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': False, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.HOLMESV: { - "core_supports_xdg": True, - "core_version": {'version_str': '20.8.1(HolmesV)', - 'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': True, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.OLD_HOLMES: { - "core_supports_xdg": False, - "core_version": {'version_str': '20.8.1(mycroft-lib)', - 'is_chatterbox_core': False, - 'is_neon_core': False, - 'is_holmes': True, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.CHATTERBOX: { - "core_supports_xdg": True, - "core_version": {'is_chatterbox_core': True, - 'is_neon_core': False, - 'is_holmes': True, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.NEON: { - "core_supports_xdg": True, - "core_version": {'is_chatterbox_core': False, - 'is_neon_core': True, - 'is_holmes': True, - 'is_ovos': False, - 'is_mycroft_core': True} - }, - MycroftPlatform.OTHER: { - "core_supports_xdg": True - } - } - - # score += score * weight (if key matches) - weights = { - MycroftPlatform.PICROFT: {}, - MycroftPlatform.BIGSCREEN: {}, - MycroftPlatform.OVOS: { - "core_supports_xdg": 1.0, - "core_version": 3.0 - }, - MycroftPlatform.CHATTERBOX: { - "core_supports_xdg": 1.0, - "core_version": 3.0 - }, - MycroftPlatform.NEON: { - "core_supports_xdg": 1.0, - "core_version": 3.0 - }, - MycroftPlatform.MARK1: {}, - MycroftPlatform.MARK2: {}, - MycroftPlatform.HOLMESV: { - "core_supports_xdg": 1.0, - "core_version": 3.0 - }, - MycroftPlatform.OLD_HOLMES: { - "core_supports_xdg": 1.0, - "core_version": 3.0 - }, - MycroftPlatform.OTHER: {"core_supports_xdg": 0.01} - } - - # score -= score * weight (if key does not match) - negative_weights = { - MycroftPlatform.PICROFT: { - "core_supports_xdg": 0.1 # likely to change soon once - # mycroft-core merges the open PR - }, - MycroftPlatform.BIGSCREEN: {}, - MycroftPlatform.OVOS: {}, - MycroftPlatform.MARK1: { - "core_supports_xdg": 0.1 # likely to change soon once - # mycroft-core merges the open PR - }, - MycroftPlatform.MARK2: { - "core_supports_xdg": 0.1 # likely to change soon once - # mycroft-core merges the open PR - }, - MycroftPlatform.HOLMESV: {}, - MycroftPlatform.OLD_HOLMES: {}, - MycroftPlatform.OTHER: {} - } - - key_counts = {e: 0 for e in MycroftPlatform} - - for k, v in fingerprint.items(): - # compare this fingerprint value to known platform features - for enclosure in MycroftPlatform: - if enclosure not in fingerprints: - continue - count = key_counts[enclosure] or 1 - # key is in fingerprint - if fingerprints[enclosure].get(k): - # key matches - if fingerprints[enclosure].get(k) == v: - if k in weights.get(enclosure, []): - key_counts[enclosure] += count * weights[enclosure][k] - # key does not match - elif k in negative_weights.get(enclosure, []): - key_counts[enclosure] -= abs(count) * \ - negative_weights[enclosure][k] - - # score platforms - m = max(v for v in key_counts.values()) - if not m: - return {k: 0 for k in key_counts} - return {k: v / m for k, v in key_counts.items()} - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def classify_config_print(fingerprint=None): - fingerprint = fingerprint or get_config_fingerprint() - - # key, val pairs that indicate a certain platform - fingerprints = { - MycroftPlatform.PICROFT: { - 'backend_url': 'https://api.mycroft.ai', - 'enclosure': 'picroft' - }, - MycroftPlatform.BIGSCREEN: { - 'backend_url': 'https://api.mycroft.ai' - }, - MycroftPlatform.OVOS: { - 'enclosure': 'OpenVoiceOS', - 'data_dir': '/opt/ovos' - }, - MycroftPlatform.MARK1: { - 'backend_url': 'https://api.mycroft.ai', - 'enclosure': 'mycroft_mark_1', - "data_dir": "/opt/mycroft" - }, - MycroftPlatform.MARK2: { - 'backend_url': 'https://api.mycroft.ai', - 'enclosure': 'mycroft_mark_2', - "data_dir": "/opt/mycroft" - }, - MycroftPlatform.HOLMESV: { - "enclosure": "HolmesV" - }, - MycroftPlatform.OLD_HOLMES: { - "enclosure": "mycroft-lib" - }, - MycroftPlatform.CHATTERBOX: { - 'enclosure': 'chatterhat', # TODO list comparison - "data_dir": "~/chatterbox", - }, - MycroftPlatform.NEON: {}, - MycroftPlatform.OTHER: {} - } - - # score += score * weight (if key matches) - weights = { - MycroftPlatform.PICROFT: { - "enclosure": 1.0, - 'backend_url': 0.5 - }, - MycroftPlatform.BIGSCREEN: {}, - MycroftPlatform.OVOS: { - "enclosure": 1.0 - }, - MycroftPlatform.CHATTERBOX: { - "enclosure": 1.0, - 'backend_url': 1.0, - "data_dir": 1.0 - }, - MycroftPlatform.NEON: { - "enclosure": 1.0 - }, - MycroftPlatform.MARK1: { - "enclosure": 1.0, - 'backend_url': 1.0, - "data_dir": 1.0 - }, - MycroftPlatform.MARK2: { - "enclosure": 1.0, - 'backend_url': 1.0, - "data_dir": 1.0 - }, - MycroftPlatform.HOLMESV: {}, - MycroftPlatform.OTHER: {} - } - - # score -= score * weight (if key does not match) - negative_weights = { - MycroftPlatform.PICROFT: { - "enclosure": 0.2, - 'backend_url': 0.2 - }, - MycroftPlatform.BIGSCREEN: {}, - MycroftPlatform.OVOS: {}, - MycroftPlatform.MARK1: { - "enclosure": 3.0, - 'backend_url': 0.5, - 'data_dir': 1.0 - }, - MycroftPlatform.MARK2: { - "enclosure": 3.0, - 'backend_url': 0.5, - 'data_dir': 1.5 # pantacor really needs /opt, users cant change it - }, - MycroftPlatform.HOLMESV: {}, - MycroftPlatform.OLD_HOLMES: {"enclosure": 0.5}, - MycroftPlatform.OTHER: {} - } - - key_counts = {e: 0 for e in MycroftPlatform} - - for k, v in fingerprint.items(): - # compare this fingerprint value to known platform features - for enclosure in MycroftPlatform: - count = key_counts[enclosure] or 1 - # key is in fingerprint - if fingerprints[enclosure].get(k): - - # key matches - if fingerprints[enclosure][k] == v: - if k in weights.get(enclosure, []): - key_counts[enclosure] += count * weights[enclosure][k] - # key does not match - elif k in negative_weights.get(enclosure, []): - key_counts[enclosure] -= abs(count) * \ - negative_weights[enclosure][k] - - # score platforms - m = max(v for v in key_counts.values()) - if not m: - return {k: 0 for k in key_counts} - return {k: v / m for k, v in key_counts.items()} - - -@deprecated("fingerprinting utils are deprecated.", "0.1.0") -def classify_fingerprint(): - plat = classify_platform_print() - conf = classify_config_print() - for k, v in conf.items(): - # high bias for platform fingerprint - plat[k] = (v * 0.5 + plat[k] * 1.5) / 2 - return plat diff --git a/ovos_utils/gui.py b/ovos_utils/gui.py index 766780d7..845cf73a 100644 --- a/ovos_utils/gui.py +++ b/ovos_utils/gui.py @@ -1,13 +1,8 @@ -import time -from collections import namedtuple -from enum import IntEnum -from os import walk -from os.path import join, splitext, isfile, isdir -from typing import List, Union, Optional, Callable +from os.path import join, isdir +from typing import List -from ovos_utils import resolve_ovos_resource_file, resolve_resource_file -from ovos_utils.fakebus import Message -from ovos_utils.log import LOG, log_deprecation, deprecated +from ovos_utils.log import LOG +from ovos_bus_client.util import wait_for_reply from ovos_utils.system import is_installed, has_screen, is_process_running _default_gui_apps = ( @@ -48,10 +43,6 @@ def is_gui_connected(bus=None) -> bool: @param bus: MessageBusClient to use for query @return: True if GUI is connected """ - try: - from ovos_bus_client.util import wait_for_reply - except: - from ovos_utils.messagebus import wait_for_reply response = wait_for_reply("gui.status.request", "gui.status.request.response", bus=bus) if response: @@ -99,1136 +90,3 @@ def get_ui_directories(root_dir: str) -> dict: LOG.debug("Handling `ui` directory as `qt5`") ui_directories["qt5"] = join(base_directory, "ui") return ui_directories - - -class GUIPlaybackStatus(IntEnum): - STOPPED = 0 - PLAYING = 1 - PAUSED = 2 - UNDEFINED = 3 - - -class GUITracker: - """ Replicates GUI API from mycroft-core, - does not interact with GUI but exactly mimics status""" - Namespace = namedtuple('Namespace', ['name', 'pages']) - RESERVED_KEYS = ['__from', '__idle'] - IDLE_MESSAGE = "mycroft.mark2.collect_idle" # TODO this will change - - @deprecated("GUITracker has been deprecated without replacement", "0.1.0") - def __init__(self, bus=None, - host='0.0.0.0', port=8181, route='/core', ssl=False): - if not bus: - from ovos_bus_client.util import get_mycroft_bus - bus = get_mycroft_bus(host, port, route, ssl) - self.bus = bus - self._active_skill = None - self._is_idle = False - self.idle_ts = 0 - # This datastore holds the data associated with the GUI provider. Data - # is stored in Namespaces, so you can have: - # self.datastore["namespace"]["name"] = value - # Typically the namespace is a meaningless identifier, but there is a - # special "SYSTEM" namespace. - self._datastore = {} - - # self.loaded is a list, each element consists of a namespace named - # tuple. - # The namespace namedtuple has the properties "name" and "pages" - # The name contains the namespace name as a string and pages is a - # mutable list of loaded pages. - # - # [Namespace name, [List of loaded qml pages]] - # [ - # ["SKILL_NAME", ["page1.qml, "page2.qml", ... , "pageN.qml"] - # [...] - # ] - self._loaded = [] # list of lists in order. - - # Listen for new GUI clients to announce themselves on the main bus - self._active_namespaces = [] - - # GUI handlers - self.bus.on("gui.value.set", self._on_gui_set_value) - self.bus.on("gui.page.show", self._on_gui_show_page) - self.bus.on("gui.page.delete", self._on_gui_delete_page) - self.bus.on("gui.clear.namespace", self._on_gui_delete_namespace) - - # Idle screen handlers TODO message cleanup... - self._idle_screens = {} - self.bus.on("mycroft.device.show.idle", self._on_show_idle) # legacy - self.bus.on(self.IDLE_MESSAGE, self._on_show_idle) - self.bus.on("mycroft.mark2.register_idle", self._on_register_idle) - - self.bus.emit(Message("mycroft.mark2.collect_idle")) - - @staticmethod - def is_gui_installed(): - return is_gui_installed() - - @staticmethod - def is_gui_running(): - return is_gui_running() - - def is_gui_connected(self): - return is_gui_connected(self.bus) - - @staticmethod - def can_display(): - return can_display() - - def is_displaying(self): - return self.active_skill is not None - - def is_idle(self): - return self._is_idle - - @property - def active_skill(self): - return self._active_skill - - @property - def gui_values(self): - return self._datastore - - @property - def idle_screens(self): - return self._idle_screens - - @property - def active_namespaces(self): - return self._active_namespaces - - @property - def gui_pages(self): - return self._loaded - - # GUI event handlers - # user can/should subclass this - def on_idle(self, namespace): - pass - - def on_active(self, namespace): - pass - - def on_new_page(self, namespace, page, index): - pass - - def on_delete_page(self, namespace, index): - pass - - def on_gui_value(self, namespace, key, value): - pass - - def on_new_namespace(self, namespace): - pass - - def on_move_namespace(self, namespace, from_index, to_index): - pass - - def on_remove_namespace(self, namespace, index): - pass - - ###################################################################### - # GUI client API - # TODO see how much of this can be removed - @staticmethod - def _get_page_data(message): - """ Extract page related data from a message. - - Args: - message: messagebus message object - Returns: - tuple (page, namespace, index) - Raises: - ValueError if value is missing. - """ - data = message.data - # Note: 'page' can be either a string or a list of strings - if 'page' not in data: - raise ValueError("Page missing in data") - if 'index' in data: - index = data['index'] - else: - index = 0 - page = data.get("page", "") - namespace = data.get("__from", "") - return page, namespace, index - - def _set(self, namespace, name, value): - """ Perform the send of the values to the connected GUIs. """ - if namespace not in self._datastore: - self._datastore[namespace] = {} - if self._datastore[namespace].get(name) != value: - self._datastore[namespace][name] = value - - def __find_namespace(self, namespace): - for i, skill in enumerate(self._loaded): - if skill[0] == namespace: - return i - return None - - def __insert_pages(self, namespace: str, pages: List[str]): - """ Insert pages into the namespace - - Args: - namespace (str): Namespace to add to - pages (list): Pages (str) to insert - """ - LOG.debug("Inserting new pages") - # Insert the pages into local reprensentation as well. - updated = self.Namespace(self._loaded[0].name, - self._loaded[0].pages + pages) - self._loaded[0] = updated - - def __remove_page(self, namespace, pos): - """ Delete page. - - Args: - namespace (str): Namespace to remove from - pos (int): Page position to remove - """ - LOG.debug("Deleting {} from {}".format(pos, namespace)) - self.on_delete_page(namespace, pos) - # Remove the page from the local reprensentation as well. - self._loaded[0].pages.pop(pos) - - def __insert_new_namespace(self, namespace: str, pages: List[str]): - """ Insert new namespace and pages. - - This first sends a message adding a new namespace at the - highest priority (position 0 in the namespace stack) - - Args: - namespace (str): The skill namespace to create - pages (str): Pages to insert (name matches QML) - """ - LOG.debug("Inserting new namespace") - self.on_new_namespace(namespace) - # Make sure the local copy is updated - self._loaded.insert(0, self.Namespace(namespace, pages)) - if time.time() - self.idle_ts > 1: - # we cant know if this page is idle or not, but when it is we - # received a idle event within the same second - self._is_idle = False - self.on_active(namespace) - else: - self.on_idle(namespace) - - def __move_namespace(self, from_pos, to_pos): - """ Move an existing namespace to a new position in the stack. - - Args: - from_pos (int): Position in the stack to move from - to_pos (int): Position to move to - """ - LOG.debug("Activating existing namespace") - # Move the local representation of the skill from current - # position to position 0. - namespace = self._loaded[from_pos].name - self.on_move_namespace(namespace, from_pos, to_pos) - self._loaded.insert(to_pos, self._loaded.pop(from_pos)) - - def _show(self, namespace, page, index): - """ Show a page and load it as needed. - - Args: - page (str or list): page(s) to show - namespace (str): skill namespace - index (int): ??? TODO: Unused in code ??? - - TODO: - Update sync to match. - - Separate into multiple functions/methods - """ - - LOG.debug("GUIConnection activating: " + namespace) - self._active_skill = namespace - pages = page if isinstance(page, list) else [page] - - # find namespace among loaded namespaces - try: - index = self.__find_namespace(namespace) - if index is None: - # This namespace doesn't exist, insert them first so they're - # shown. - self.__insert_new_namespace(namespace, pages) - return - else: # Namespace exists - if index > 0: - # Namespace is inactive, activate it by moving it to - # position 0 - self.__move_namespace(index, 0) - - # Find if any new pages needs to be inserted - new_pages = [p for p in pages if - p not in self._loaded[0].pages] - if new_pages: - self.__insert_pages(namespace, new_pages) - except Exception as e: - LOG.exception(repr(e)) - - ###################################################################### - # Internal GUI events - def _on_gui_set_value(self, message): - data = message.data - namespace = data.get("__from", "") - - # Pass these values on to the GUI renderers - for key in data: - if key not in self.RESERVED_KEYS: - try: - self._set(namespace, key, data[key]) - self.on_gui_value(namespace, key, data[key]) - except Exception as e: - LOG.exception(repr(e)) - - def _on_gui_delete_page(self, message): - """ Bus handler for removing pages. """ - page, namespace, _ = self._get_page_data(message) - try: - self._remove_pages(namespace, page) - except Exception as e: - LOG.exception(repr(e)) - - def _on_gui_delete_namespace(self, message): - """ Bus handler for removing namespace. """ - try: - namespace = message.data['__from'] - self._remove_namespace(namespace) - except Exception as e: - LOG.exception(repr(e)) - - def _on_gui_show_page(self, message): - try: - page, namespace, index = self._get_page_data(message) - # Pass the request to the GUI(s) to pull up a page template - self._show(namespace, page, index) - self.on_new_page(namespace, page, index) - except Exception as e: - LOG.exception(repr(e)) - - def _remove_namespace(self, namespace): - """ Remove namespace. - - Args: - namespace (str): namespace to remove - """ - index = self.__find_namespace(namespace) - if index is None: - return - else: - LOG.debug("Removing namespace {} at {}".format(namespace, index)) - self.on_remove_namespace(namespace, index) - # Remove namespace from loaded namespaces - self._loaded.pop(index) - - def _remove_pages(self, namespace, pages): - """ Remove the listed pages from the provided namespace. - - Args: - namespace (str): The namespace to modify - pages (list): List of page names (str) to delete - """ - try: - index = self.__find_namespace(namespace) - if index is None: - return - else: - # Remove any pages that doesn't exist in the namespace - pages = [p for p in pages if p in self._loaded[index].pages] - # Make sure to remove pages from the back - indexes = [self._loaded[index].pages.index(p) for p in pages] - indexes = sorted(indexes) - indexes.reverse() - for page_index in indexes: - self.__remove_page(namespace, page_index) - except Exception as e: - LOG.exception(repr(e)) - - def _on_register_idle(self, message): - """Handler for catching incoming idle screens.""" - if "name" in message.data and "id" in message.data: - screen = message.data["name"] - if screen not in self._idle_screens: - self.bus.on("{}.idle".format(message.data["id"]), - self._on_show_idle) - self._idle_screens[screen] = message.data["id"] - LOG.info("Registered {}".format(message.data["name"])) - else: - LOG.error("Malformed idle screen registration received") - - def _on_show_idle(self, message): - self.idle_ts = time.time() - self._is_idle = True - - -try: - from ovos_bus_client.apis.gui import extend_about_data as _ead, GUIInterface as _GI, GUIWidgets as _GW - - - def extend_about_data(about_data: Union[list, dict], - bus=None): - return _ead(about_data, bus) - - - class GUIWidgets(_GW): - def __new__(cls, *args, **kwargs): - log_deprecation("GUIWidgets moved to ovos_bus_client.apis.gui", "0.1.0") - return _GW(*args, **kwargs) - - - class GUIInterface(_GI): - def __new__(cls, *args, **kwargs): - log_deprecation("GUIInterface moved to ovos_bus_client.apis.gui", "0.1.0") - return _GI(*args, **kwargs) - -except ImportError: - - @deprecated("extend_about_data moved to ovos_bus_client.apis.gui", "0.1.0") - def extend_about_data(about_data: Union[list, dict], - bus=None): - """ - Add more information to the "About" section in the GUI. - @param about_data: list of dict key, val information to add to the GUI - @param bus: MessageBusClient object to emit update on - """ - if not bus: - from ovos_bus_client.util import get_mycroft_bus - bus = get_mycroft_bus() - if isinstance(about_data, list): - bus.emit(Message("smartspeaker.extension.extend.about", - {"display_list": about_data})) - elif isinstance(about_data, dict): - display_list = [about_data] - bus.emit(Message("smartspeaker.extension.extend.about", - {"display_list": display_list})) - else: - LOG.error("about_data is not a list or dictionary") - - - class GUIWidgets: - @deprecated("GUIWidgets moved to ovos_bus_client.apis.gui", "0.1.0") - def __init__(self, bus=None): - if not bus: - from ovos_bus_client.util import get_mycroft_bus - bus = get_mycroft_bus() - self.bus = bus - - def show_widget(self, widget_type, widget_data): - LOG.debug("Showing widget: " + widget_type) - self.bus.emit(Message("ovos.widgets.display", {"type": widget_type, "data": widget_data})) - - def remove_widget(self, widget_type, widget_data): - LOG.debug("Removing widget: " + widget_type) - self.bus.emit(Message("ovos.widgets.remove", {"type": widget_type, "data": widget_data})) - - def update_widget(self, widget_type, widget_data): - LOG.debug("Updating widget: " + widget_type) - self.bus.emit(Message("ovos.widgets.update", {"type": widget_type, "data": widget_data})) - - - class _GUIDict(dict): - """ - This is a helper dictionary subclass. It ensures that values changed - in it are propagated to the GUI service in real time. - """ - - def __init__(self, gui, **kwargs): - self.gui = gui - super().__init__(**kwargs) - - def __setitem__(self, key, value): - old = self.get(key) - if old != value: - super(_GUIDict, self).__setitem__(key, value) - self.gui._sync_data() - - - class GUIInterface: - """ - Interface to the Graphical User Interface, allows interaction with - the mycroft-gui from anywhere - - Values set in this class are synced to the GUI, accessible within QML - via the built-in sessionData mechanism. For example, in Python you can - write in a skill: - self.gui['temp'] = 33 - self.gui.show_page('Weather.qml') - Then in the Weather.qml you'd access the temp via code such as: - text: sessionData.time - """ - - @deprecated("GUIInterface moved to ovos_bus_client.apis.gui", "0.1.0") - def __init__(self, skill_id: str, bus=None, - remote_server: str = None, config: dict = None, - ui_directories: dict = None): - """ - Create an interface to the GUI module. Values set here are exposed to - the GUI client as sessionData - @param skill_id: ID of this interface - @param bus: MessagebusClient object to connect to - @param remote_server: Optional URL of a remote GUI server - @param config: dict gui Configuration - @param ui_directories: dict framework to directory containing resources - `all` key should reference a `gui` directory containing all - specific resource subdirectories - """ - if not config: - log_deprecation(f"Expected a dict config and got None.", "0.1.0") - try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config().get("gui", {}) - except ImportError: - LOG.warning("Config not provided and ovos_config not available") - config = dict() - self.config = config - if remote_server: - self.config["remote-server"] = remote_server - self._bus = bus - self.__session_data = {} # synced to GUI for use by this skill's pages - self._pages = [] - self.current_page_idx = -1 - self._skill_id = skill_id - self.on_gui_changed_callback = None - self._events = [] - self.ui_directories = ui_directories or dict() - if bus: - self.set_bus(bus) - - @property - def remote_url(self) -> Optional[str]: - """Returns configuration value for url of remote-server.""" - return self.config.get('remote-server') - - @remote_url.setter - def remote_url(self, val: str): - self.config["remote-server"] = val - - def set_bus(self, bus=None): - if not bus: - from ovos_bus_client.util import get_mycroft_bus - bus = get_mycroft_bus() - self._bus = bus - self.setup_default_handlers() - - @property - def bus(self): - """ - Return the attached MessageBusClient - """ - return self._bus - - @bus.setter - def bus(self, val): - self.set_bus(val) - - @property - def skill_id(self) -> str: - """ - Return the ID of the module implementing this interface - """ - return self._skill_id - - @skill_id.setter - def skill_id(self, val: str): - self._skill_id = val - - @property - def page(self) -> Optional[str]: - """ - Return the active GUI page name to show - """ - return self._pages[self.current_page_idx] if len(self._pages) else None - - @property - def connected(self) -> bool: - """ - Returns True if at least 1 remote gui is connected or if gui is - installed and running locally, else False - """ - if not self.bus: - return False - return can_use_gui(self.bus) - - @property - def pages(self) -> List[str]: - """ - Get a list of the active page ID's managed by this interface - """ - return self._pages - - def build_message_type(self, event: str) -> str: - """ - Ensure the specified event prepends this interface's `skill_id` - """ - if not event.startswith(f'{self.skill_id}.'): - event = f'{self.skill_id}.' + event - return event - - # events - def setup_default_handlers(self): - """ - Sets the handlers for the default messages. - """ - msg_type = self.build_message_type('set') - self.bus.on(msg_type, self.gui_set) - self._events.append((msg_type, self.gui_set)) - self.bus.on("gui.request_page_upload", self.upload_gui_pages) - if self.ui_directories: - LOG.debug("Volunteering gui page upload") - self.bus.emit(Message("gui.volunteer_page_upload", - {'skill_id': self.skill_id}, - {'source': self.skill_id, "destination": ["gui"]})) - - def upload_gui_pages(self, message: Message): - """ - Emit a response Message with all known GUI files managed by - this interface for the requested infrastructure - @param message: `gui.request_page_upload` Message requesting pages - """ - if not self.ui_directories: - LOG.debug("No UI resources to upload") - return - - requested_skill = message.data.get("skill_id") or self._skill_id - if requested_skill != self._skill_id: - # GUI requesting a specific skill to upload other than this one - return - - request_res_type = message.data.get("framework") or "all" if "all" in \ - self.ui_directories else "qt5" - # Note that ui_directory "all" is a special case that will upload all - # gui files, including all framework subdirectories - if request_res_type not in self.ui_directories: - LOG.warning(f"Requested UI files not available: {request_res_type}") - return - LOG.debug(f"Requested upload resources for: {request_res_type}") - pages = dict() - # `pages` keys are unique identifiers in the scope of this interface; - # if ui_directory is "all", then pages are prefixed with `/` - res_dir = self.ui_directories[request_res_type] - for path, _, files in walk(res_dir): - for file in files: - try: - full_path: str = join(path, file) - page_name = full_path.replace(f"{res_dir}/", "", 1) - with open(full_path, 'rb') as f: - file_bytes = f.read() - pages[page_name] = file_bytes.hex() - except Exception as e: - LOG.exception(f"{file} not uploaded: {e}") - # Note that `pages` in this context include file extensions - self.bus.emit(message.forward("gui.page.upload", - {"__from": self.skill_id, - "framework": request_res_type, - "pages": pages})) - - def register_handler(self, event: str, handler: Callable): - """ - Register a handler for GUI events. - - will be prepended with self.skill_id.XXX if missing in event - - When using the triggerEvent method from Qt - triggerEvent("event", {"data": "cool"}) - - Args: - event (str): event to catch - handler: function to handle the event - """ - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - event = self.build_message_type(event) - self._events.append((event, handler)) - self.bus.on(event, handler) - - def set_on_gui_changed(self, callback: Callable): - """ - Registers a callback function to run when a value is - changed from the GUI. - - Arguments: - callback: Function to call when a value is changed - """ - self.on_gui_changed_callback = callback - - # internals - def gui_set(self, message: Message): - """ - Handler catching variable changes from the GUI. - - Arguments: - message: Messagebus message - """ - for key in message.data: - self[key] = message.data[key] - if self.on_gui_changed_callback: - self.on_gui_changed_callback() - - def _sync_data(self): - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - data = self.__session_data.copy() - data.update({'__from': self.skill_id}) - self.bus.emit(Message("gui.value.set", data)) - - def __setitem__(self, key, value): - """Implements set part of dict-like behaviour with named keys.""" - old = self.__session_data.get(key) - if old == value: # no need to sync - return - - # cast to helper dict subclass that syncs data - if isinstance(value, dict) and not isinstance(value, _GUIDict): - value = _GUIDict(self, **value) - - self.__session_data[key] = value - - # emit notification (but not needed if page has not been shown yet) - if self.page: - self._sync_data() - - def __getitem__(self, key): - """Implements get part of dict-like behaviour with named keys.""" - return self.__session_data[key] - - def get(self, *args, **kwargs): - """Implements the get method for accessing dict keys.""" - return self.__session_data.get(*args, **kwargs) - - def __contains__(self, key): - """ - Implements the "in" operation. - """ - return self.__session_data.__contains__(key) - - def clear(self): - """ - Reset the value dictionary, and remove namespace from GUI. - - This method does not close the GUI for a Skill. For this purpose see - the `release` method. - """ - self.__session_data = {} - self._pages = [] - self.current_page_idx = -1 - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("gui.clear.namespace", - {"__from": self.skill_id})) - - def send_event(self, event_name: str, - params: Union[dict, list, str, int, float, bool] = None): - """ - Trigger a gui event. - - Arguments: - event_name (str): name of event to be triggered - params: json serializable object containing any parameters that - should be sent along with the request. - """ - params = params or {} - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("gui.event.send", - {"__from": self.skill_id, - "event_name": event_name, - "params": params})) - - def _pages2uri(self, page_names: List[str]) -> List[str]: - """ - Get a list of resolved URIs from a list of string page names. - @param page_names: List of GUI resource names (file basenames) to locate - @return: List of resolved paths to the requested pages - """ - # TODO: This method resolves absolute file paths. These will no longer - # be used with the implementation of `ovos-gui` - page_urls = [] - extra_dirs = list(self.ui_directories.values()) or list() - for name in page_names: - # Prefer plugin-specific resources first, then fallback to core - page = resolve_ovos_resource_file(name, extra_dirs) or \ - resolve_ovos_resource_file(join('ui', name), extra_dirs) or \ - resolve_resource_file(name, self.config) or \ - resolve_resource_file(join('ui', name), self.config) - - if page: - if self.remote_url: - page_urls.append(self.remote_url + "/" + page) - elif page.startswith("file://"): - page_urls.append(page) - else: - page_urls.append("file://" + page) - else: - # This is expected; with `ovos-gui`, pages are referenced by ID - # rather than filename in order to support multiple frameworks - LOG.debug(f"Requested page not resolved to a file: {page}") - LOG.debug(f"Resolved pages: {page_urls}") - return page_urls - - @staticmethod - def _normalize_page_name(page_name: str) -> str: - """ - Normalize a requested GUI resource - @param page_name: string name of a GUI resource - @return: normalized string name (`.qml` removed for other GUI support) - """ - if isfile(page_name): - log_deprecation("GUI resources should specify a resource name and " - "not a file path.", "0.1.0") - return page_name - file, ext = splitext(page_name) - if ext == ".qml": - log_deprecation("GUI resources should exclude gui-specific file " - f"extensions. This call should probably pass " - f"`{file}`, instead of `{page_name}`", "0.1.0") - return file - - return page_name - - # base gui interactions - def show_page(self, name: str, override_idle: Union[bool, int] = None, - override_animations: bool = False): - """ - Request to show a page in the GUI. - @param name: page resource requested - @param override_idle: number of seconds to override display for; - if True, override display indefinitely - @param override_animations: if True, disables all GUI animations - """ - self.show_pages([name], 0, override_idle, override_animations) - - def show_pages(self, page_names: List[str], index: int = 0, - override_idle: Union[bool, int] = None, - override_animations: bool = False): - """ - Request to show a list of pages in the GUI. - @param page_names: list of page resources requested - @param index: position to insert pages at (default 0) - @param override_idle: number of seconds to override display for; - if True, override display indefinitely - @param override_animations: if True, disables all GUI animations - """ - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - if isinstance(page_names, str): - page_names = [page_names] - if not isinstance(page_names, list): - raise ValueError('page_names must be a list') - - if index > len(page_names): - LOG.error('Default index is larger than page list length') - index = len(page_names) - 1 - - # TODO: deprecate sending page_urls after ovos_gui implementation - page_urls = self._pages2uri(page_names) - page_names = [self._normalize_page_name(n) for n in page_names] - - self._pages = page_names - self.current_page_idx = index - - # First sync any data... - data = self.__session_data.copy() - data.update({'__from': self.skill_id}) - LOG.debug(f"Updating gui data: {data}") - self.bus.emit(Message("gui.value.set", data)) - - # finally tell gui what to show - self.bus.emit(Message("gui.page.show", - {"page": page_urls, - "page_names": page_names, - "ui_directories": self.ui_directories, - "index": index, - "__from": self.skill_id, - "__idle": override_idle, - "__animations": override_animations})) - - def remove_page(self, page: str): - """ - Remove a single page from the GUI. - @param page: Name of page to remove - """ - self.remove_pages([page]) - - def remove_pages(self, page_names: List[str]): - """ - Request to remove a list of pages from the GUI. - @param page_names: list of page resources requested - """ - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - if isinstance(page_names, str): - page_names = [page_names] - if not isinstance(page_names, list): - raise ValueError('page_names must be a list') - # TODO: deprecate sending page_urls after ovos_gui implementation - page_urls = self._pages2uri(page_names) - page_names = [self._normalize_page_name(n) for n in page_names] - self.bus.emit(Message("gui.page.delete", - {"page": page_urls, - "page_names": page_names, - "__from": self.skill_id})) - - # Utils / Templates - - # backport - PR https://github.com/MycroftAI/mycroft-core/pull/2862 - def show_notification(self, content: str, duration: int = 10, - action: str = None, noticetype: str = "transient", - style: str = "info", - callback_data: Optional[dict] = None): - """Display a Notification on homepage in the GUI. - Arguments: - content (str): Main text content of a notification, Limited - to two visual lines. - duration (int): seconds to display notification for - action (str): Callback to any event registered by the skill - to perform a certain action when notification is clicked. - noticetype (str): - transient: 'Default' displays a notification with a timeout. - sticky: displays a notification that sticks to the screen. - style (str): - info: 'Default' displays a notification with information styling - warning: displays a notification with warning styling - success: displays a notification with success styling - error: displays a notification with error styling - callback_data (dict): data dictionary available to use with action - """ - # TODO: Define enums for style and noticetype - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - # GUI does not accept NONE type, send an empty dict - # Sending NONE will corrupt entries in the model - callback_data = callback_data or dict() - self.bus.emit(Message("ovos.notification.api.set", - data={ - "duration": duration, - "sender": self.skill_id, - "text": content, - "action": action, - "type": noticetype, - "style": style, - "callback_data": callback_data - })) - - def show_controlled_notification(self, content: str, style: str = "info"): - """ - Display a controlled Notification in the GUI. - Arguments: - content (str): Main text content of a notification, Limited - to two visual lines. - style (str): - info: 'Default' displays a notification with information styling - warning: displays a notification with warning styling - success: displays a notification with success styling - error: displays a notification with error styling - """ - # TODO: Define enum for style - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("ovos.notification.api.set.controlled", - data={ - "sender": self.skill_id, - "text": content, - "style": style - })) - - def remove_controlled_notification(self): - """ - Remove a controlled Notification in the GUI. - """ - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("ovos.notification.api.remove.controlled")) - - def show_text(self, text: str, title: Optional[str] = None, - override_idle: Union[int, bool] = None, - override_animations: bool = False): - """ - Display a GUI page for viewing simple text. - - Arguments: - text (str): Main text content. It will auto-paginate - title (str): A title to display above the text content. - override_idle (boolean, int): - True: Takes over the resting page indefinitely - (int): Delays resting page for the specified number of - seconds. - override_animations (boolean): - True: Disables showing all platform skill animations. - False: 'Default' always show animations. - """ - self["text"] = text - self["title"] = title - self.show_page("SYSTEM_TextFrame.qml", override_idle, - override_animations) - - def show_image(self, url: str, caption: Optional[str] = None, - title: Optional[str] = None, - fill: str = None, background_color: str = None, - override_idle: Union[int, bool] = None, - override_animations: bool = False): - """ - Display a GUI page for viewing an image. - - Arguments: - url (str): Pointer to the image - caption (str): A caption to show under the image - title (str): A title to display above the image content - fill (str): Fill type supports 'PreserveAspectFit', - 'PreserveAspectCrop', 'Stretch' - background_color (str): A background color for - the page in hex i.e. #000000 - override_idle (boolean, int): - True: Takes over the resting page indefinitely - (int): Delays resting page for the specified number of - seconds. - override_animations (boolean): - True: Disables showing all platform skill animations. - False: 'Default' always show animations. - """ - self["image"] = url - self["title"] = title - self["caption"] = caption - self["fill"] = fill - self["background_color"] = background_color - self.show_page("SYSTEM_ImageFrame.qml", override_idle, - override_animations) - - def show_animated_image(self, url: str, caption: Optional[str] = None, - title: Optional[str] = None, - fill: str = None, background_color: str = None, - override_idle: Union[int, bool] = None, - override_animations: bool = False): - """ - Display a GUI page for viewing an image. - - Args: - url (str): Pointer to the .gif image - caption (str): A caption to show under the image - title (str): A title to display above the image content - fill (str): Fill type supports 'PreserveAspectFit', - 'PreserveAspectCrop', 'Stretch' - background_color (str): A background color for - the page in hex i.e. #000000 - override_idle (boolean, int): - True: Takes over the resting page indefinitely - (int): Delays resting page for the specified number of - seconds. - override_animations (boolean): - True: Disables showing all platform skill animations. - False: 'Default' always show animations. - """ - self["image"] = url - self["title"] = title - self["caption"] = caption - self["fill"] = fill - self["background_color"] = background_color - self.show_page("SYSTEM_AnimatedImageFrame.qml", override_idle, - override_animations) - - def show_html(self, html: str, resource_url: Optional[str] = None, - override_idle: Union[int, bool] = None, - override_animations: bool = False): - """ - Display an HTML page in the GUI. - - Args: - html (str): HTML text to display - resource_url (str): Pointer to HTML resources - override_idle (boolean, int): - True: Takes over the resting page indefinitely - (int): Delays resting page for the specified number of - seconds. - override_animations (boolean): - True: Disables showing all platform skill animations. - False: 'Default' always show animations. - """ - self["html"] = html - self["resourceLocation"] = resource_url - self.show_page("SYSTEM_HtmlFrame.qml", override_idle, - override_animations) - - def show_url(self, url: str, override_idle: Union[int, bool] = None, - override_animations: bool = False): - """ - Display an HTML page in the GUI. - - Args: - url (str): URL to render - override_idle (boolean, int): - True: Takes over the resting page indefinitely - (int): Delays resting page for the specified number of - seconds. - override_animations (boolean): - True: Disables showing all platform skill animations. - False: 'Default' always show animations. - """ - self["url"] = url - self.show_page("SYSTEM_UrlFrame.qml", override_idle, - override_animations) - - def show_input_box(self, title: Optional[str] = None, - placeholder: Optional[str] = None, - confirm_text: Optional[str] = None, - exit_text: Optional[str] = None, - override_idle: Union[int, bool] = None, - override_animations: bool = False): - """ - Display a fullscreen UI for a user to enter text and confirm or cancel - @param title: title of input UI should describe what the input is - @param placeholder: default text hint to show in an empty entry box - @param confirm_text: text to display on the submit/confirm button - @param exit_text: text to display on the cancel/exit button - @param override_idle: if True, takes over the resting page indefinitely - else Delays resting page for the specified number of seconds. - @param override_animations: disable showing all platform animations - """ - self["title"] = title - self["placeholder"] = placeholder - self["skill_id_handler"] = self.skill_id - if not confirm_text: - self["confirm_text"] = "Confirm" - else: - self["confirm_text"] = confirm_text - - if not exit_text: - self["exit_text"] = "Exit" - else: - self["exit_text"] = exit_text - - self.show_page("SYSTEM_InputBox.qml", override_idle, - override_animations) - - def remove_input_box(self): - """ - Remove an input box shown by `show_input_box` - """ - LOG.info(f"GUI pages length {len(self._pages)}") - if len(self._pages) > 1: - self.remove_page("SYSTEM_InputBox.qml") - else: - self.release() - - def release(self): - """ - Signal that this skill is no longer using the GUI, - allow different platforms to properly handle this event. - Also calls self.clear() to reset the state variables - Platforms can close the window or go back to previous page - """ - if not self.bus: - raise RuntimeError("bus not set, did you call self.bind() ?") - self.clear() - self.bus.emit(Message("mycroft.gui.screen.close", - {"skill_id": self.skill_id})) - - def shutdown(self): - """ - Shutdown gui interface. - - Clear pages loaded through this interface and remove the bus events - """ - if self.bus: - self.release() - for event, handler in self._events: - self.bus.remove(event, handler) diff --git a/ovos_utils/intents/__init__.py b/ovos_utils/intents/__init__.py deleted file mode 100644 index 2d329971..00000000 --- a/ovos_utils/intents/__init__.py +++ /dev/null @@ -1,143 +0,0 @@ -from ovos_utils.intents.intent_service_interface import IntentQueryApi, \ - IntentServiceInterface -from ovos_utils.intents.converse import ConverseTracker -from ovos_utils.intents.layers import IntentLayers -from ovos_utils.log import log_deprecation - -log_deprecation("ovos_utils.intents moved to ovos_workshop.intents", "0.1.0") - -try: - from ovos_workshop.intents import * - -except ImportError: - - class Intent: - def __init__(self, name, requires, at_least_one, optional): - """Create Intent object - Args: - name(str): Name for Intent - requires(list): Entities that are required - at_least_one(list): One of these Entities are required - optional(list): Optional Entities used by the intent - """ - self.name = name - self.requires = requires - self.at_least_one = at_least_one - self.optional = optional - - def validate(self, tags, confidence): - """Using this method removes tags from the result of validate_with_tags - Returns: - intent(intent): Results from validate_with_tags - """ - raise NotImplementedError("please install adapt-parser") - - def validate_with_tags(self, tags, confidence): - """Validate whether tags has required entites for this intent to fire - Args: - tags(list): Tags and Entities used for validation - confidence(float): The weight associate to the parse result, - as indicated by the parser. This is influenced by a parser - that uses edit distance or context. - Returns: - intent, tags: Returns intent and tags used by the intent on - failure to meat required entities then returns intent with - confidence - of 0.0 and an empty list for tags. - """ - raise NotImplementedError("please install adapt-parser") - - - class IntentBuilder: - """ - IntentBuilder, used to construct intent parsers. - Attributes: - at_least_one(list): A list of Entities where one is required. - These are separated into lists so you can have one of (A or B) and - then require one of (D or F). - requires(list): A list of Required Entities - optional(list): A list of optional Entities - name(str): Name of intent - Notes: - This is designed to allow construction of intents in one line. - Example: - IntentBuilder("Intent")\ - .requires("A")\ - .one_of("C","D")\ - .optional("G").build() - """ - - def __init__(self, intent_name): - """ - Constructor - Args: - intent_name(str): the name of the intents that this parser - parses/validates - """ - self.at_least_one = [] - self.requires = [] - self.optional = [] - self.name = intent_name - - def one_of(self, *args): - """ - The intent parser should require one of the provided entity types to - validate this clause. - Args: - args(args): *args notation list of entity names - Returns: - self: to continue modifications. - """ - self.at_least_one.append(args) - return self - - def require(self, entity_type, attribute_name=None): - """ - The intent parser should require an entity of the provided type. - Args: - entity_type(str): an entity type - attribute_name(str): the name of the attribute on the parsed intent. - Defaults to match entity_type. - Returns: - self: to continue modifications. - """ - if not attribute_name: - attribute_name = entity_type - self.requires += [(entity_type, attribute_name)] - return self - - def optionally(self, entity_type, attribute_name=None): - """ - Parsed intents from this parser can optionally include an entity of the - provided type. - Args: - entity_type(str): an entity type - attribute_name(str): the name of the attribute on the parsed intent. - Defaults to match entity_type. - Returns: - self: to continue modifications. - """ - if not attribute_name: - attribute_name = entity_type - self.optional += [(entity_type, attribute_name)] - return self - - def build(self): - """ - Constructs an intent from the builder's specifications. - :return: an Intent instance. - """ - return Intent(self.name, self.requires, - self.at_least_one, self.optional) - - -class AdaptIntent(IntentBuilder): - """Wrapper for IntentBuilder setting a blank name. - - Args: - name (str): Optional name of intent - """ - - def __init__(self, name=''): - super().__init__(name) - diff --git a/ovos_utils/intents/converse.py b/ovos_utils/intents/converse.py deleted file mode 100644 index 8f8c6e6e..00000000 --- a/ovos_utils/intents/converse.py +++ /dev/null @@ -1,205 +0,0 @@ -import time - -import ovos_utils.messagebus -from ovos_utils.intents.intent_service_interface import IntentQueryApi -from ovos_utils.log import LOG, deprecated - - -class ConverseTracker: - """ Using the messagebus this class recreates/keeps track of the state - of the converse system, it uses both passive listening and active - queries to sync it's state, it also emits 2 new bus events - - Implements https://github.com/MycroftAI/mycroft-core/pull/1468 - """ - bus = None - active_skills = [] - converse_timeout = 5 # MAGIC NUMBER hard coded in mycroft-core - last_conversed = None - intent_api = None - - @deprecated("ConverseTracker has been deprecated without replacement", "0.1.0") - def __init__(self): - pass - - @classmethod - def connect_bus(cls, mycroft_bus): - """Registers the bus object to use.""" - # PATCH - in mycroft-core this would be handled in intent_service - # in here it is done in MycroftSkill.bind so i added this - # conditional check - if cls.bus is None and mycroft_bus is not None: - cls.bus = mycroft_bus - cls.intent_api = IntentQueryApi(cls.bus) - cls.register_bus_events() - - @classmethod - def register_bus_events(cls): - cls.bus.on('active_skill_request', cls.handle_activate_request) - cls.bus.on('skill.converse.response', cls.handle_converse_response) - cls.bus.on("mycroft.skill.handler.start", cls.handle_intent_start) - cls.bus.on("recognizer_loop:utterance", cls.handle_utterance) - - # public methods - @classmethod - def check_skill(cls, skill_id): - """ Check if a skill is active """ - cls.filter_active_skills() - for skill in list(cls.active_skills): - if skill[0] == skill_id: - return True - return False - - @classmethod - def filter_active_skills(cls): - """ Removes expired skills from active skill list """ - # filter timestamps - for skill in list(cls.active_skills): - if time.time() - skill[1] <= cls.converse_timeout * 60: - cls.remove_active_skill(skill[0]) - - @classmethod - def sync_with_intent_service(cls): - """sync active skill list using intent api - - WARNING - we don't have the timestamps so order might be messed up!! - avoid calling this until - """ - skill_ids = cls.intent_api.get_active_skills(include_timestamps=True) - if skill_ids: - if len(skill_ids[0]) == 2: - # PR was merged! hurray! - cls.active_skills = skill_ids - else: - # hoping they come sorted by timestamp.... - # older to newer (most recently used) - for skill_id in reversed(skill_ids): - # are we tracking this skill ? - if not cls.check_skill(skill_id): - # we missed adding this skill in our tracking - cls.add_active_skill(skill_id) - for skill in cls.active_skills: - if skill[0] not in skill_ids: - # we missed removing this skill in our tracking - cls.remove_active_skill(skill[0]) - - # https://github.com/MycroftAI/mycroft-core/pull/1468 - @classmethod - def remove_active_skill(cls, skill_id, silent=False): - """ - Emits "converse.skill.deactivated" event, improvement of #1468 - """ - for skill in list(cls.active_skills): - if skill[0] == skill_id: - cls.active_skills.remove(skill) - if not silent: - cls.bus.emit(ovos_utils.messagebus.Message("converse.skill.deactivated", - {"skill_id": skill[0]})) - - @classmethod - def add_active_skill(cls, skill_id): - """ - Emits "converse.skill.activated" event, improvement of #1468 - """ - # search the list for an existing entry that already contains it - # and remove that reference - if skill_id != '': - cls.remove_active_skill(skill_id, silent=True) - # add skill with timestamp to start of skill_list - cls.active_skills.insert(0, [skill_id, time.time()]) - # this might be sent more than once and it's perfectly fine - # it's just a new info message not consumed anywhere by default - cls.bus.emit(ovos_utils.messagebus.Message("converse.skill.activated", - {"skill_id": skill_id})) - else: - LOG.warning('Skill ID was empty, won\'t add to list of ' - 'active skills.') - - # status tracking - @classmethod - def handle_activate_request(cls, message): - """ - a skill bumped itself to the top of active skills list - duplicate functionality from mycroft-core, keeping list in sync - """ - skill_id = message.data["skill_id"] - cls.add_active_skill(skill_id) - - @classmethod - def handle_converse_error(cls, message): - """ - a skill was removed from active skill list due to converse error - duplicate functionality from mycroft-core, keeping list in sync - """ - skill_id = message.data["skill_id"] - if message.data["error"] == "skill id does not exist": - cls.remove_active_skill(skill_id) - - @classmethod - def handle_intent_start(cls, message): - """ - duplicate functionality from mycroft-core, keeping list in sync - - TODO skill_id from message, core is not passing it along... it used - to be possible to retrieve it from munged message but that changed. - send a PR (if those got merged this code wouldn't exist) - - handle_utterance will take over this functionality for now - handle_converse_response will take corrective action - """ - # skill_id = message.data["skill_id"] - # bump skill to top of active list - # cls.add_active_skill(skill_id) - - @classmethod - def handle_utterance(cls, message): - """ - duplicate functionality from mycroft-core, keeping list in sync - - WORKAROUND - skill_id missing in handle_intent_start, will keep list - in sync by using the IntentAPI to check what skill the utterance - should trigger - - handle_converse_response will take corrective action - """ - # NOTE borked in mycroft-core - # needs https://github.com/MycroftAI/mycroft-core/pull/2786 - skill_id = cls.intent_api.get_skill(message.data["utterances"][0]) - if skill_id: - # this skill will trigger and therefore is the last active skill - cls.add_active_skill(skill_id) - # will remove expired intents from list - cls.filter_active_skills() - - @classmethod - def handle_converse_response(cls, message): - """ - tracks last_conversed skill - - FAILSAFE - additional checks to correct active skills list, - but that should never happen, accounts for mistakes in - handle_utterance / intent_api - - accounts for https://github.com/MycroftAI/mycroft-core/pull/2786 - not yet being merged - """ - skill_id = message.data["skill_id"] - if 'error' in message.data: - cls.handle_converse_error(message) - elif message.data.get('result') is True: - cls.last_conversed = skill_id - if not cls.check_skill(skill_id): - # seems like we missed adding this skill to active list - # NOTE this is a failsafe and should never trigger - # since this answered True we have the real timestamp - cls.add_active_skill(skill_id) - elif not cls.check_skill(skill_id): - # seems like we missed adding this skill to active list - # NOTE this is a failsafe and should never trigger - # since this answered false and we don't have the real timestamp - # let's add it to the end of the active_skills list - ts = time.time() - if len(cls.active_skills): - ts = cls.active_skills[-1][1] - cls.active_skills.append([skill_id, ts]) diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py deleted file mode 100644 index cddadff5..00000000 --- a/ovos_utils/intents/intent_service_interface.py +++ /dev/null @@ -1,585 +0,0 @@ -from os.path import exists, isfile -from threading import RLock -from typing import List, Tuple, Optional - -import ovos_utils.messagebus -from ovos_utils.log import LOG, log_deprecation, deprecated - -from ovos_utils.file_utils import to_alnum # backwards compat import - -log_deprecation("ovos_utils.intents moved to ovos_workshop.intents", "0.1.0") - - -try: - from ovos_workshop.intents import * -except: - - def munge_regex(regex: str, skill_id: str) -> str: - """ - Insert skill id as letters into match groups. - - Args: - regex (str): regex string - skill_id (str): skill identifier - Returns: - (str) munged regex - """ - base = '(?P<' + to_alnum(skill_id) - return base.join(regex.split('(?P<')) - - - def munge_intent_parser(intent_parser, name, skill_id): - """ - Rename intent keywords to make them skill exclusive - This gives the intent parser an exclusive name in the - format :. The keywords are given unique - names in the format . - - The function will not munge instances that's already been - munged - - Args: - intent_parser: (IntentParser) object to update - name: (str) Skill name - skill_id: (int) skill identifier - """ - # Munge parser name - if not name.startswith(str(skill_id) + ':'): - intent_parser.name = str(skill_id) + ':' + name - else: - intent_parser.name = name - - # Munge keywords - skill_id = to_alnum(skill_id) - # Munge required keyword - reqs = [] - for i in intent_parser.requires: - if not i[0].startswith(skill_id): - kw = (skill_id + i[0], skill_id + i[0]) - reqs.append(kw) - else: - reqs.append(i) - intent_parser.requires = reqs - - # Munge optional keywords - opts = [] - for i in intent_parser.optional: - if not i[0].startswith(skill_id): - kw = (skill_id + i[0], skill_id + i[0]) - opts.append(kw) - else: - opts.append(i) - intent_parser.optional = opts - - # Munge at_least_one keywords - at_least_one = [] - for i in intent_parser.at_least_one: - element = [skill_id + e.replace(skill_id, '') for e in i] - at_least_one.append(tuple(element)) - intent_parser.at_least_one = at_least_one - - - class IntentServiceInterface: - """ - Interface to communicate with the Mycroft intent service. - - This class wraps the messagebus interface of the intent service allowing - for easier interaction with the service. It wraps both the Adapt and - Padatious parts of the intent services. - """ - - def __init__(self, bus=None): - self._bus = bus - self.skill_id = self.__class__.__name__ - # TODO: Consider using properties with setters to prevent duplicates - self.registered_intents: List[Tuple[str, object]] = [] - self.detached_intents: List[Tuple[str, object]] = [] - self._iterator_lock = RLock() - - @property - def intent_names(self) -> List[str]: - """ - Get a list of intent names (both registered and disabled). - """ - return [a[0] for a in self.registered_intents + self.detached_intents] - - @property - def bus(self): - if not self._bus: - raise RuntimeError("bus not set. call `set_bus()` before trying to" - "interact with the Messagebus") - return self._bus - - @bus.setter - def bus(self, val): - self.set_bus(val) - - def set_bus(self, bus=None): - self._bus = bus or ovos_utils.messagebus.get_mycroft_bus() - - def set_id(self, skill_id: str): - self.skill_id = skill_id - - def register_adapt_keyword(self, vocab_type: str, entity: str, - aliases: Optional[List[str]] = None, - lang: str = None): - """ - Send a message to the intent service to add an Adapt keyword. - @param vocab_type: Keyword reference (file basename) - @param entity: Primary keyword value - @param aliases: List of alternative keyword values - @param lang: BCP-47 language code of entity and aliases - """ - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - - # TODO 22.02: Remove compatibility data - aliases = aliases or [] - entity_data = {'entity_value': entity, - 'entity_type': vocab_type, - 'lang': lang} - compatibility_data = {'start': entity, 'end': vocab_type} - - self.bus.emit(msg.forward("register_vocab", - {**entity_data, **compatibility_data})) - for alias in aliases: - alias_data = { - 'entity_value': alias, - 'entity_type': vocab_type, - 'alias_of': entity, - 'lang': lang} - compatibility_data = {'start': alias, 'end': vocab_type} - self.bus.emit(msg.forward("register_vocab", - {**alias_data, **compatibility_data})) - - def register_adapt_regex(self, regex: str, lang: str = None): - """ - Register a regex string with the intent service. - @param regex: Regex to be registered; Adapt extracts keyword references - from named match group. - @param lang: BCP-47 language code of regex - """ - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("register_vocab", - {'regex': regex, 'lang': lang})) - - def register_adapt_intent(self, name: str, intent_parser: object): - """ - Register an Adapt intent parser object. Serializes the intent_parser - and sends it over the messagebus to registered. - @param name: string intent name (without skill_id prefix) - @param intent_parser: Adapt Intent object - """ - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("register_intent", intent_parser.__dict__)) - self.registered_intents.append((name, intent_parser)) - self.detached_intents = [detached for detached in self.detached_intents - if detached[0] != name] - - def detach_intent(self, intent_name: str): - """ - DEPRECATED: Use `remove_intent` instead, all other methods from this - class expect intent_name; this was the weird one expecting the internal - munged intent_name with skill_id. - """ - name = intent_name.split(':')[1] - log_deprecation(f"Update to `self.remove_intent({name})", - "0.1.0") - self.remove_intent(name) - - def remove_intent(self, intent_name: str): - """ - Remove an intent from the intent service. The intent is saved in the - list of detached intents for use when re-enabling an intent. A - `detach_intent` Message is emitted for the intent service to handle. - @param intent_name: Registered intent to remove/detach (no skill_id) - """ - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - if intent_name in self.intent_names: - # TODO: This will create duplicates of already detached intents - LOG.info(f"Detaching intent: {intent_name}") - self.detached_intents.append((intent_name, - self.get_intent(intent_name))) - self.registered_intents = [pair for pair in self.registered_intents - if pair[0] != intent_name] - self.bus.emit(msg.forward("detach_intent", - {"intent_name": - f"{self.skill_id}:{intent_name}"})) - - def intent_is_detached(self, intent_name: str) -> bool: - """ - Determine if an intent is detached. - @param intent_name: String intent reference to check (without skill_id) - @return: True if intent is in detached_intents, else False. - """ - is_detached = False - with self._iterator_lock: - for (name, _) in self.detached_intents: - if name == intent_name: - is_detached = True - break - return is_detached - - def set_adapt_context(self, context: str, word: str, origin: str): - """ - Set an Adapt context. - @param context: context keyword name to add/update - @param word: word to register (context keyword value) - @param origin: original origin of the context (for cross context) - """ - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward('add_context', - {'context': context, 'word': word, - 'origin': origin})) - - def remove_adapt_context(self, context: str): - """ - Remove an Adapt context. - @param context: context keyword name to remove - """ - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward('remove_context', {'context': context})) - - def register_padatious_intent(self, intent_name: str, filename: str, - lang: str): - """ - Register a Padatious intent file with the intent service. - @param intent_name: Unique intent identifier - (usually `skill_id`:`filename`) - @param filename: Absolute file path to entity file - @param lang: BCP-47 language code of registered intent - """ - if not isinstance(filename, str): - raise ValueError('Filename path must be a string') - if not exists(filename): - raise FileNotFoundError(f'Unable to find "{filename}"') - with open(filename) as f: - samples = [_ for _ in f.read().split("\n") if _ - and not _.startswith("#")] - data = {'file_name': filename, - "samples": samples, - 'name': intent_name, - 'lang': lang} - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("padatious:register_intent", data)) - self.registered_intents.append((intent_name.split(':')[-1], data)) - - def register_padatious_entity(self, entity_name: str, filename: str, - lang: str): - """ - Register a Padatious entity file with the intent service. - @param entity_name: Unique entity identifier - (usually `skill_id`:`filename`) - @param filename: Absolute file path to entity file - @param lang: BCP-47 language code of registered intent - """ - if not isinstance(filename, str): - raise ValueError('Filename path must be a string') - if not exists(filename): - raise FileNotFoundError('Unable to find "{}"'.format(filename)) - with open(filename) as f: - samples = [_ for _ in f.read().split("\n") if _ - and not _.startswith("#")] - msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward('padatious:register_entity', - {'file_name': filename, - "samples": samples, - 'name': entity_name, - 'lang': lang})) - - def get_intent_names(self): - log_deprecation("Reference `intent_names` directly", "0.1.0") - return self.intent_names - - def detach_all(self): - """ - Detach all intents associated with this interface and remove all - internal references to intents and handlers. - """ - for name in self.intent_names: - self.remove_intent(name) - if self.registered_intents: - LOG.error(f"Expected an empty list; got: {self.registered_intents}") - self.registered_intents = [] - self.detached_intents = [] # Explicitly remove all intent references - - def get_intent(self, intent_name: str) -> Optional[object]: - """ - Get an intent object by name. This will find both enabled and disabled - intents. - @param intent_name: name of intent to find (without skill_id) - @return: intent object if found, else None - """ - to_return = None - with self._iterator_lock: - for name, intent in self.registered_intents: - if name == intent_name: - to_return = intent - break - if to_return is None: - with self._iterator_lock: - for name, intent in self.detached_intents: - if name == intent_name: - to_return = intent - break - return to_return - - def __iter__(self): - """Iterator over the registered intents. - - Returns an iterator returning name-handler pairs of the registered - intent handlers. - """ - return iter(self.registered_intents) - - def __contains__(self, val): - """ - Checks if an intent name has been registered. - """ - return val in [i[0] for i in self.registered_intents] - - - def open_intent_envelope(message): - """ - Convert dictionary received over messagebus to Intent. - """ - # TODO can this method be fully removed from ovos_utils ? - from adapt.intent import Intent - - intent_dict = message.data - return Intent(intent_dict.get('name'), - intent_dict.get('requires'), - intent_dict.get('at_least_one'), - intent_dict.get('optional')) - - -class IntentQueryApi: - """ - Query Intent Service at runtime - """ - - @deprecated("IntentQueryApi has been deprecated without replacement", "0.1.0") - def __init__(self, bus=None, timeout=5): - if bus is None: - bus = ovos_utils.messagebus.get_mycroft_bus() - self.bus = bus - self.timeout = timeout - - def get_adapt_intent(self, utterance, lang="en-us"): - """ get best adapt intent for utterance """ - msg = ovos_utils.messagebus.Message("intent.service.adapt.get", - {"utterance": utterance, "lang": lang}, - context={"destination": "intent_service", - "source": "intent_api"}) - - resp = self.bus.wait_for_response(msg, - 'intent.service.adapt.reply', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - return data["intent"] - - def get_padatious_intent(self, utterance, lang="en-us"): - """ get best padatious intent for utterance """ - msg = ovos_utils.messagebus.Message("intent.service.padatious.get", - {"utterance": utterance, "lang": lang}, - context={"destination": "intent_service", - "source": "intent_api"}) - resp = self.bus.wait_for_response(msg, - 'intent.service.padatious.reply', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - return data["intent"] - - def get_intent(self, utterance, lang="en-us"): - """ get best intent for utterance """ - msg = ovos_utils.messagebus.Message("intent.service.intent.get", - {"utterance": utterance, "lang": lang}, - context={"destination": "intent_service", - "source": "intent_api"}) - resp = self.bus.wait_for_response(msg, - 'intent.service.intent.reply', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - return data["intent"] - - def get_skill(self, utterance, lang="en-us"): - """ get skill that utterance will trigger """ - intent = self.get_intent(utterance, lang) - if not intent: - return None - # theoretically skill_id might be missing - if intent.get("skill_id"): - return intent["skill_id"] - # retrieve skill from munged intent name - if intent.get("intent_name"): # padatious + adapt - return intent["name"].split(":")[0] - if intent.get("intent_type"): # adapt - return intent["intent_type"].split(":")[0] - return None # raise some error here maybe? this should never happen - - def get_skills_manifest(self): - msg = ovos_utils.messagebus.Message("intent.service.skills.get", - context={"destination": "intent_service", - "source": "intent_api"}) - resp = self.bus.wait_for_response(msg, - 'intent.service.skills.reply', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - return data["skills"] - - def get_active_skills(self, include_timestamps=False): - msg = ovos_utils.messagebus.Message("intent.service.active_skills.get", - context={"destination": "intent_service", - "source": "intent_api"}) - resp = self.bus.wait_for_response(msg, - 'intent.service.active_skills.reply', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - if include_timestamps: - return data["skills"] - return [s[0] for s in data["skills"]] - - def get_adapt_manifest(self): - msg = ovos_utils.messagebus.Message("intent.service.adapt.manifest.get", - context={"destination": "intent_service", - "source": "intent_api"}) - resp = self.bus.wait_for_response(msg, - 'intent.service.adapt.manifest', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - return data["intents"] - - def get_padatious_manifest(self): - msg = ovos_utils.messagebus.Message("intent.service.padatious.manifest.get", - context={"destination": "intent_service", - "source": "intent_api"}) - resp = self.bus.wait_for_response(msg, - 'intent.service.padatious.manifest', - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - return data["intents"] - - def get_intent_manifest(self): - padatious = self.get_padatious_manifest() - adapt = self.get_adapt_manifest() - return {"adapt": adapt, - "padatious": padatious} - - def get_vocab_manifest(self): - msg = ovos_utils.messagebus.Message("intent.service.adapt.vocab.manifest.get", - context={"destination": "intent_service", - "source": "intent_api"}) - reply_msg_type = 'intent.service.adapt.vocab.manifest' - resp = self.bus.wait_for_response(msg, - reply_msg_type, - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - - vocab = {} - for voc in data["vocab"]: - if voc.get("regex"): - continue - if voc["end"] not in vocab: - vocab[voc["end"]] = {"samples": []} - vocab[voc["end"]]["samples"].append(voc["start"]) - return [{"name": voc, "samples": vocab[voc]["samples"]} - for voc in vocab] - - def get_regex_manifest(self): - msg = ovos_utils.messagebus.Message("intent.service.adapt.vocab.manifest.get", - context={"destination": "intent_service", - "source": "intent_api"}) - reply_msg_type = 'intent.service.adapt.vocab.manifest' - resp = self.bus.wait_for_response(msg, - reply_msg_type, - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - - vocab = {} - for voc in data["vocab"]: - if not voc.get("regex"): - continue - name = voc["regex"].split("(?P<")[-1].split(">")[0] - if name not in vocab: - vocab[name] = {"samples": []} - vocab[name]["samples"].append(voc["regex"]) - return [{"name": voc, "regexes": vocab[voc]["samples"]} - for voc in vocab] - - def get_entities_manifest(self): - msg = ovos_utils.messagebus.Message("intent.service.padatious.entities.manifest.get", - context={"destination": "intent_service", - "source": "intent_api"}) - reply_msg_type = 'intent.service.padatious.entities.manifest' - resp = self.bus.wait_for_response(msg, - reply_msg_type, - timeout=self.timeout) - data = resp.data if resp is not None else {} - if not data: - LOG.error("Intent Service timed out!") - return None - - entities = [] - # read files - for ent in data["entities"]: - if isfile(ent["file_name"]): - with open(ent["file_name"]) as f: - lines = f.read().replace("(", "").replace(")", "").split( - "\n") - samples = [] - for l in lines: - samples += [a.strip() for a in l.split("|") if a.strip()] - entities.append({"name": ent["name"], "samples": samples}) - return entities - - def get_keywords_manifest(self): - padatious = self.get_entities_manifest() - adapt = self.get_vocab_manifest() - regex = self.get_regex_manifest() - return {"adapt": adapt, - "padatious": padatious, - "regex": regex} - - diff --git a/ovos_utils/intents/layers.py b/ovos_utils/intents/layers.py deleted file mode 100644 index e1c62ede..00000000 --- a/ovos_utils/intents/layers.py +++ /dev/null @@ -1,161 +0,0 @@ -from ovos_utils.log import LOG, deprecated - - -try: - from ovos_workshop.decorators.layers import IntentLayers -except ImportError: - import ovos_utils.messagebus - - from time import sleep - - class IntentLayers: - - @deprecated("IntentLayers moved to ovos_workshop.decorators.layers", "0.1.0") - def __init__(self, bus=None, layers=None): - layers = layers or [] - self.bus = bus or ovos_utils.messagebus.get_mycroft_bus() - # make intent levels for N layers - self.layers = layers - self.current_layer = 0 - self.activate_layer(0) - self.named_layers = {} - - def disable_intent(self, intent_name): - """Disable a registered intent""" - self.bus.emit(ovos_utils.messagebus.Message("mycroft.skill.disable_intent", - {"intent_name": intent_name})) - - def enable_intent(self, intent_name): - """Reenable a registered self intent""" - self.bus.emit(ovos_utils.messagebus.Message("mycroft.skill.enable_intent", - {"intent_name": intent_name})) - - def reset(self): - LOG.info("Reseting Intent Layers") - self.activate_layer(0) - - def next(self): - LOG.info("Going to next Intent Layer") - self.current_layer += 1 - if self.current_layer > len(self.layers): - LOG.info("Already in last layer, going to layer 0") - self.current_layer = 0 - self.activate_layer(self.current_layer) - - def previous(self): - LOG.info("Going to previous Intent Layer") - self.current_layer -= 1 - if self.current_layer < 0: - self.current_layer = 0 - LOG.error("Already in layer 0") - else: - self.activate_layer(self.current_layer) - - def add_layer(self, intent_list=None): - intent_list = intent_list or [] - self.layers.append(intent_list) - LOG.info("Adding intent layer: " + str(intent_list)) - - def add_named_layer(self, name, intent_list=None): - intent_list = intent_list or [] - self.named_layers[name] = len(self.layers) - self.layers.append(intent_list) - LOG.info("Setting layer " + name + " to: " + str(intent_list)) - - def activate_named_layer(self, name): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("activating layer named: " + name) - self.activate_layer(i) - else: - LOG.error("no layer named: " + name) - - def deactivate_named_layer(self, name): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("deactivating layer named: " + name) - self.deactivate_layer(i) - else: - LOG.error("no layer named: " + name) - - def remove_named_layer(self, name): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("removing layer named: " + name) - self.remove_layer(i) - else: - LOG.error("no layer named: " + name) - - def replace_named_layer(self, name, intent_list=None): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("replacing layer named: " + name) - self.replace_layer(i, intent_list) - else: - LOG.error("no layer named: " + name) - - def replace_layer(self, layer_num, intent_list=None): - intent_list = intent_list or [] - if self.current_layer == layer_num: - self.deactivate_layer(layer_num) - LOG.info("Adding layer" + str(intent_list) + " in position " + str( - layer_num)) - self.layers[layer_num] = intent_list - if self.current_layer == layer_num: - self.activate_layer(layer_num) - - def remove_layer(self, layer_num): - if layer_num >= len(self.layers): - return False - if self.current_layer == layer_num: - self.deactivate_layer(layer_num) - self.layers.pop(layer_num) - LOG.info("Removing layer number " + str(layer_num)) - return True - - def find_layer(self, intent_name): - layer_list = [] - for i in range(0, len(self.layers)): - if intent_name in self.layers[i]: - layer_list.append(i) - return layer_list - - def disable(self): - LOG.info("Disabling layers") - # disable all layers - for i in range(0, len(self.layers)): - self.deactivate_layer(i) - - def activate_layer(self, layer_num): - # error check - if layer_num < 0 or layer_num > len(self.layers): - LOG.error("invalid layer number") - return False - - self.current_layer = layer_num - - # disable other layers - self.disable() - - # TODO in here we should wait for all intents to be detached - # sometimes detach intent from this step comes after register from next - # is there some bus signal we can track? - sleep(0.3) - - # enable layer - LOG.info("Activating Layer " + str(layer_num)) - if layer_num < len(self.layers) and len(self.layers): - for intent_name in self.layers[layer_num]: - self.enable_intent(intent_name) - return True - return False - - def deactivate_layer(self, layer_num): - # error check - if layer_num < 0 or layer_num > len(self.layers): - LOG.error("invalid layer number") - return False - LOG.info("Deactivating Layer " + str(layer_num)) - for intent_name in self.layers[layer_num]: - self.disable_intent(intent_name) - return True diff --git a/ovos_utils/json_helper.py b/ovos_utils/json_helper.py index c54ba9b5..ed356450 100644 --- a/ovos_utils/json_helper.py +++ b/ovos_utils/json_helper.py @@ -1,9 +1,5 @@ import json from copy import copy -# TODO: Deprecate unused imports -from json_database.utils import is_jsonifiable, get_key_recursively, \ - get_key_recursively_fuzzy, get_value_recursively_fuzzy, \ - get_value_recursively, jsonify_recursively def nested_get(base, key_list): diff --git a/ovos_utils/log.py b/ovos_utils/log.py index d054053b..5f8fe83d 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -19,6 +19,7 @@ from os.path import join from typing import List + class LOG: """ Custom logger class that acts like logging.Logger @@ -83,12 +84,12 @@ def init(cls, config=None): default_base = get_xdg_base() except ImportError: default_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER") or \ - "mycroft" + "mycroft" from ovos_utils.xdg_utils import xdg_state_home config = config or {} cls.base_path = config.get("path") or \ - f"{xdg_state_home()}/{default_base}" + f"{xdg_state_home()}/{default_base}" cls.max_bytes = config.get("max_bytes", 50000000) cls.backup_count = config.get("backup_count", 3) cls.level = config.get("level") or LOG.level @@ -190,7 +191,6 @@ def init_service_logger(service_name): LOG.warning("ovos_config not available. Falling back to defaults") _cfg = dict() - # First try and get the "logging" section log_config = _cfg.get("logging") # For compatibility we try to get the "logs" from the root level @@ -265,6 +265,7 @@ def deprecated(log_message: str, deprecation_version: str): @param log_message: Deprecation log message @param deprecation_version: package version in which deprecation will occur """ + def wrapped(func): @functools.wraps(func) def log_wrapper(*args, **kwargs): @@ -273,6 +274,7 @@ def log_wrapper(*args, **kwargs): func_module=func.__module__, deprecation_version=deprecation_version) return func(*args, **kwargs) + return log_wrapper return wrapped diff --git a/ovos_utils/messagebus.py b/ovos_utils/messagebus.py index 924ab3c4..698685fa 100644 --- a/ovos_utils/messagebus.py +++ b/ovos_utils/messagebus.py @@ -1,529 +1 @@ -import json -import time - -from ovos_utils import create_loop from ovos_utils.fakebus import dig_for_message, FakeMessage, Message, FakeBus -from ovos_utils.log import LOG, log_deprecation, deprecated -from ovos_utils.events import EventContainer as _EC - -log_deprecation("decode_binary_message, send_binary_file_message, send_binary_data_message, \ - send_message, wait_for_reply, listen_once_for_message, get_message_lang, get_websocket, get_mycroft_bus, \ - listen_for_message have moved to ovos_bus_client.util", "0.1.0") -log_deprecation("dig_for_message, FakeMessage, FakeBus moved to ovos_utils.fakebus", "0.1.0") - - -class EventContainer(_EC): - def __init__(self, bus=None): - log_deprecation("Import from `ovos_utils.events`", "0.1.0") - _EC.__init__(self, bus=bus) - - -def create_wrapper(*args, **kwargs): - log_deprecation("Import from `ovos_utils.events`", "0.1.0") - from ovos_utils.events import create_wrapper - return create_wrapper(*args, **kwargs) - - -def get_handler_name(*args, **kwargs): - log_deprecation("Import from `ovos_utils.events`", "0.1.0") - from ovos_utils.events import get_handler_name - return get_handler_name(*args, **kwargs) - - -def merge_dict(*args, **kwargs): - log_deprecation("Import from `ovos_utils.json_helper`", "0.1.0") - from ovos_utils.json_helper import merge_dict - return merge_dict(*args, **kwargs) - - -try: - from ovos_bus_client.util import decode_binary_message, send_binary_file_message, send_binary_data_message, \ - send_message, wait_for_reply, listen_once_for_message, get_message_lang, get_websocket, get_mycroft_bus, \ - listen_for_message - -except: - _DEFAULT_WS_CONFIG = {"host": "0.0.0.0", - "port": 8181, - "route": "/core", - "ssl": False} - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def get_message_lang(message=None): - """Get the language from the message or the default language. - Args: - message: message to check for language code. - Returns: - The language code from the message or the default language. - """ - try: - from ovos_config.locale import get_default_lang - default_lang = get_default_lang() - except ImportError: - LOG.warning("ovos_config not available. Using default lang en-us") - default_lang = "en-us" - message = message or dig_for_message() - if not message: - return default_lang - lang = message.data.get("lang") or message.context.get("lang") or default_lang - return lang.lower() - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def get_websocket(host, port, route='/', ssl=False, threaded=True): - """ - Returns a connection to a websocket - """ - from ovos_bus_client import MessageBusClient - - client = MessageBusClient(host, port, route, ssl) - if threaded: - client.run_in_thread() - return client - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def get_mycroft_bus(host: str = None, port: int = None, route: str = None, - ssl: bool = None): - """ - Returns a connection to the mycroft messagebus - """ - try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config().get('websocket') or dict() - except ImportError: - LOG.warning("ovos_config not available. Falling back to default WS") - config = dict() - host = host or config.get('host') or _DEFAULT_WS_CONFIG['host'] - port = port or config.get('port') or _DEFAULT_WS_CONFIG['port'] - route = route or config.get('route') or _DEFAULT_WS_CONFIG['route'] - if ssl is None: - ssl = config.get('ssl') if 'ssl' in config else \ - _DEFAULT_WS_CONFIG['ssl'] - return get_websocket(host, port, route, ssl) - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def listen_for_message(msg_type, handler, bus=None): - """ - Continuously listens and reacts to a specific messagetype on the mycroft messagebus - - NOTE: when finished you should call bus.remove(msg_type, handler) - """ - bus = bus or get_mycroft_bus() - bus.on(msg_type, handler) - return bus - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def listen_once_for_message(msg_type, handler, bus=None): - """ - listens and reacts once to a specific messagetype on the mycroft messagebus - """ - auto_close = bus is None - bus = bus or get_mycroft_bus() - - def _handler(message): - handler(message) - if auto_close: - bus.close() - - bus.once(msg_type, _handler) - return bus - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def wait_for_reply(message: Message, reply_type=None, timeout=3.0, bus=None): - """Send a message and wait for a response. - - Args: - message (Message or str or dict): message object or type to send - reply_type (str): the message type of the expected reply. - Defaults to ".response". - timeout: seconds to wait before timeout, defaults to 3 - Returns: - The received message or None if the response timed out - """ - auto_close = bus is None - bus = bus or get_mycroft_bus() - if isinstance(message, str): - try: - message = json.loads(message) - except: - pass - if isinstance(message, str): - message = Message(message) - elif isinstance(message, dict): - message = Message(message["type"], - message.get("data"), - message.get("context")) - elif not isinstance(message, Message): - raise ValueError - response = bus.wait_for_response(message, reply_type, timeout) - if auto_close: - bus.close() - return response - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def send_message(message, data=None, context=None, bus=None): - auto_close = bus is None - bus = bus or get_mycroft_bus() - if isinstance(message, str): - if isinstance(data, dict) or isinstance(context, dict): - message = Message(message, data, context) - else: - try: - message = json.loads(message) - except: - message = Message(message) - if isinstance(message, dict): - message = Message(message["type"], - message.get("data"), - message.get("context")) - if not isinstance(message, Message): - raise ValueError - bus.emit(message) - if auto_close: - bus.close() - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def send_binary_data_message(binary_data, msg_type="mycroft.binary.data", - msg_data=None, msg_context=None, bus=None): - msg_data = msg_data or {} - msg = { - "type": msg_type, - "data": merge_dict(msg_data, {"binary": binary_data.hex()}), - "context": msg_context or None - } - send_message(msg, bus=bus) - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def send_binary_file_message(filepath, msg_type="mycroft.binary.file", - msg_context=None, bus=None): - with open(filepath, 'rb') as f: - binary_data = f.read() - msg_data = {"path": filepath} - send_binary_data_message(binary_data, msg_type=msg_type, msg_data=msg_data, - msg_context=msg_context, bus=bus) - - - @deprecated("moved to ovos_bus_client.util", "0.1.0") - def decode_binary_message(message): - if isinstance(message, str): - try: # json string - message = json.loads(message) - binary_data = message.get("binary") or message["data"]["binary"] - except: # hex string - binary_data = message - elif isinstance(message, dict): - # data field or serialized message - binary_data = message.get("binary") or message["data"]["binary"] - else: - # message object - binary_data = message.data["binary"] - # decode hex string - return bytearray.fromhex(binary_data) - - -class BusService: - """ - Provide some service over the messagebus for other components - - response = Message("face.recognition.reply") - service = BusService(response) - service.listen("face.recognition") - - while True: - data = do_computation() - service.update_response(data) # replaces response.data - - """ - - @deprecated("deprecated without replacement", "0.1.0") - def __init__(self, message, trigger_messages=None, bus=None): - self.bus = bus or get_mycroft_bus() - self.response = message - trigger_messages = trigger_messages or [] - self.events = [] - for message_type in trigger_messages: - self.listen(message_type) - - def listen(self, message_type, callback=None): - if callback is None: - callback = self._respond - self.bus.on(message_type, callback) - self.events.append((message_type, callback)) - - def update_response(self, data=None): - if data is not None: - self.response.data = data - - def _respond(self, message): - self.bus.emit(message.reply(self.response.type, self.response.data)) - - def shutdown(self): - """ remove all listeners """ - for event, callback in self.events: - self.bus.remove(event, callback) - - -class BusFeedProvider: - """ - - Meant to be subclassed - - - class ClockService(BusFeedProvider): - def __init__(self, name="clock_transmitter", bus=None): - trigger_message = Message("time.request") - super().__init__(trigger_message, name, bus) - self.set_data_gatherer(self.handle_get_time) - - def handle_get_time(self, message): - self.update({"date": datetime.now()}) - - - clock_service = ClockService() - - """ - - @deprecated("deprecated without replacement", "0.1.0") - def __init__(self, trigger_message, name=None, bus=None, config=None): - """ - initialize responder - - args: - name(str): name identifier for .conf settings - bus (WebsocketClient): mycroft messagebus websocket - """ - if not config: - log_deprecation(f"Expected a dict config and got None.", "0.1.0") - try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config() - except ImportError: - LOG.warning("ovos_config not available. Falling back to " - "default configuration") - config = dict() - self.trigger_message = trigger_message - self.name = name or self.__class__.__name__ - self.bus = bus or get_mycroft_bus() - self.callback = None - self.service = None - self._daemon = None - self.config = config.get(self.name, {}) - - def update(self, data): - """ - change the data of the response to be sent when queried - """ - if self.service is not None: - self.service.update_response(data) - - def set_data_gatherer(self, callback, default_data=None, daemonic=False, interval=90): - """ - prepare responder for sending, register answers - """ - - self.bus.remove_all_listeners(self.trigger_message) - if ".request" in self.trigger_message: - response_type = self.trigger_message.replace(".request", ".reply") - else: - response_type = self.trigger_message + ".reply" - - response = Message(response_type, default_data) - self.service = BusService(response, bus=self.bus) - self.callback = callback - self.bus.on(self.trigger_message, self._respond) - if daemonic: - self._daemon = create_loop(self._data_daemon, interval) - - def _data_daemon(self): - if self.callback is not None: - self.callback(self.trigger_message) - - def _respond(self, message): - """ - gather data and emit to bus - """ - try: - if self.callback: - self.callback(message) - except Exception as e: - LOG.error(e) - self.service.respond(message) - - def shutdown(self): - self.bus.remove_all_listeners(self.trigger_message) - if self._daemon: - self._daemon.join(0) - self._daemon = None - if self.service: - self.service.shutdown() - self.service = None - - -class BusQuery: - """ - retrieve data from some other component over the messagebus at any time - - message = Message("request.msg", {...}, {...}) - query = BusQuery(message) - response = query.send() - # do some more stuff - response = query.send() # reutilize the object - - """ - - @deprecated("deprecated without replacement", "0.1.0") - def __init__(self, message, bus=None): - self.bus = bus or get_mycroft_bus() - self._waiting = False - self.response = Message(None, None, None) - self.query = message - self.valid_response_types = [] - - def add_response_type(self, response_type): - """ listen to a new response_type """ - if response_type not in self.valid_response_types: - self.valid_response_types.append(response_type) - self.bus.on(response_type, self._end_wait) - - def _end_wait(self, message): - self.response = message - self._waiting = False - - def _wait_response(self, timeout): - start = time.time() - elapsed = 0 - self._waiting = True - while self._waiting and elapsed < timeout: - elapsed = time.time() - start - time.sleep(0.1) - self._waiting = False - - def send(self, response_type=None, timeout=10): - self.response = Message(None, None, None) - if response_type is None: - response_type = self.query.type + ".reply" - self.add_response_type(response_type) - self.bus.emit(self.query) - self._wait_response(timeout) - return self.response - - def remove_listeners(self): - for event in self.valid_response_types: - self.bus.remove(event, self._end_wait) - - def shutdown(self): - """ remove all listeners """ - self.remove_listeners() - - -class BusFeedConsumer: - """ - this is meant to be subclassed - - class Clock(BusFeedConsumer): - def __init__(self, name="clock_receiver", timeout=3, bus=None): - request_message = Message("time.request") - super().__init__(request_message, name, timeout, bus) - - # blocking - clock = Clock() - date = clock.request()["date"] - - # async - clock = Clock(timeout=0) # non - blocking - clock.request(daemonic=True, # loop on background - frequency=1) # update result every second - - date = clock.result["date"] - - """ - - @deprecated("deprecated without replacement", "0.1.0") - def __init__(self, query_message, name=None, timeout=5, bus=None, - config=None): - self.query_message = query_message - self.query_message.context["source"] = self.name - self.name = name or self.__class__.__name__ - self.bus = bus or get_mycroft_bus() - if not config: - log_deprecation(f"Expected a dict config and got None.", "0.1.0") - try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config() - except ImportError: - LOG.warning("Config not provided and ovos_config not available") - config = dict() - self.config = config.get(self.name, {}) - self.timeout = timeout - self.query = None - self.valid_responses = [] - self._daemon = None - - def request(self, response_messages=None, daemonic=False, interval=90): - """ - prepare query for sending, add several possible kinds of - response message automatically - "message_type.reply" , - "message_type.response", - "message_type.result" - """ - response_messages = response_messages or [] - - # generate valid reply message types - self.valid_responses = response_messages - if ".request" in self.query_message.type: - response = self.query_message.type.replace(".request", ".reply") - if response not in self.valid_responses: - self.valid_responses.append(response) - response = self.query_message.type.replace(".request", ".response") - if response not in self.valid_responses: - self.valid_responses.append(response) - response = self.query_message.type.replace(".request", ".result") - if response not in self.valid_responses: - self.valid_responses.append(response) - else: - response = self.query_message.type + ".reply" - if response not in self.valid_responses: - self.valid_responses.append(response) - response = self.query_message.type + ".response" - if response not in self.valid_responses: - self.valid_responses.append(response) - response = self.query_message.type + ".result" - if response not in self.valid_responses: - self.valid_responses.append(response) - - # update message context - self.query_message.context["valid_responses"] = self.valid_responses - - self._query() - if daemonic: - self._daemon = create_loop(self._request_daemon, interval) - return self.result - - def _request_daemon(self): - self.query.send(self.valid_responses[0], self.timeout) - - def _query(self): - self.query = BusQuery(self.query_message) - for message in self.valid_responses[1:]: - self.query.add_response_type(message) - self.query.send(self.valid_responses[0], self.timeout) - - @property - def result(self): - return self.query.response.data - - def shutdown(self): - """ remove all listeners """ - if self._daemon: - self._daemon.join(0) - self._daemon = None - if self.query: - self.query.shutdown() diff --git a/ovos_utils/metrics.py b/ovos_utils/metrics.py index 60af232f..28a32f74 100644 --- a/ovos_utils/metrics.py +++ b/ovos_utils/metrics.py @@ -5,6 +5,7 @@ class Stopwatch: """ Simple time measuring class. """ + def __init__(self): self.timestamp = None self.time = None @@ -57,4 +58,3 @@ def __str__(self): return str(self.time or cur_time - self.timestamp) else: return 'Not started' - diff --git a/ovos_utils/network_utils.py b/ovos_utils/network_utils.py index 316b2581..40d5008b 100644 --- a/ovos_utils/network_utils.py +++ b/ovos_utils/network_utils.py @@ -3,10 +3,8 @@ import requests - from ovos_utils.log import LOG - _DEFAULT_TEST_CONFIG = { "ip_url": 'https://api.ipify.org', "dns_primary": "1.1.1.1", @@ -15,7 +13,7 @@ "web_url_secondary": "https://checkonline.home-assistant.io/online.txt", "captive_portal_url": "http://nmcheck.gnome.org/check_network_status.txt", "captive_portal_text": "NetworkManager is online" - } +} def get_network_tests_config(): @@ -140,4 +138,3 @@ def check_captive_portal(host: Optional[str] = None, LOG.exception("Error checking for captive portal") return captive_portal - diff --git a/ovos_utils/ovos_service_api.py b/ovos_utils/ovos_service_api.py deleted file mode 100644 index 19df7757..00000000 --- a/ovos_utils/ovos_service_api.py +++ /dev/null @@ -1,227 +0,0 @@ -import requests -from json_database import JsonStorageXDG -from ovos_utils.log import deprecated - - -# TODO: This will be deprecated in v0.1 - - -class OVOSApiService: - @deprecated("Import from ovos-backend-client.api.BaseApi", "0.1.0") - def __init__(self) -> None: - self.uuid_storage = JsonStorageXDG("ovos_api_uuid") - self.token_storage = JsonStorageXDG("ovos_api_token") - - def register_device(self): - if self.check_if_uuid_exists(): - return - else: - created_challenge = requests.get('https://api.openvoiceos.com/create_challenge') - challenge_response = created_challenge.json() - register_device = requests.get( - 'https://api.openvoiceos.com/register_device/' + challenge_response['challenge'] + '/' + - challenge_response['secret']) - register_device_uuid = challenge_response['challenge'] - self.uuid_storage['uuid'] = register_device_uuid - self.uuid_storage.store() - - def check_if_uuid_exists(self): - if "uuid" in self.uuid_storage: - return True - return False - - def get_session_challenge(self): - session_challenge_request = requests.get('https://api.openvoiceos.com/get_session_challenge') - session_challenge_response = session_challenge_request.json() - self.token_storage["challenge"] = session_challenge_response['challenge'] - self.token_storage.store() - - def get_uuid(self): - return self.uuid_storage.get("uuid", "") - - def get_session_token(self): - return self.token_storage.get("challenge", "") - - -class OvosWeather: - @deprecated("Import ovos-backend-client.api.OpenWeatherMapApi", "0.1.0") - def __init__(self): - self.api = OVOSApiService() - - @property - def uuid(self): - return self.api.get_uuid() - - @property - def headers(self): - self.api.get_session_challenge() - return {'session_challenge': self.api.get_session_token(), 'backend': 'OWM'} - - def get_current(self, query): - reqdata = {"lat": query.get("lat"), - "lon": query.get("lon"), - "units": query.get("units"), - "lang": query.get("lang")} - url = f"https://api.openvoiceos.com/weather/generate_current_weather_report/{self.uuid}" - r = requests.post(url, data=reqdata, headers=self.headers) - return r.json() - - def get_hourly(self, query): - reqdata = {"lat": query.get("lat"), - "lon": query.get("lon"), - "units": query.get("units"), - "lang": query.get("lang")} - url = f"https://api.openvoiceos.com/weather/generate_hourly_weather_report/{self.uuid}" - r = requests.post(url, data=reqdata, headers=self.headers) - return r.json() - - def get_forecast(self, query): - # Requires Paid API - reqdata = {"lat": query.get("lat"), - "lon": query.get("lon"), - "units": query.get("units"), - "lang": query.get("lang")} - url = f"https://api.openvoiceos.com/weather/generate_forecast_weather_report/{self.uuid}" - r = requests.post(url, data=reqdata, headers=self.headers) - return r.json() - - def get_weather_onecall(self, query): - reqdata = {"lat": query.get("lat"), - "lon": query.get("lon"), - "units": query.get("units"), - "lang": query.get("lang")} - url = f'https://api.openvoiceos.com/weather/onecall_weather_report/{self.uuid}' - r = requests.post(url, data=reqdata, headers=self.headers) - return r.json() - - -class OvosWolframAlpha: - @deprecated("Import from ovos-backend-client.api.WolframAlphaApi", "0.1.0") - def __init__(self): - self.api = OVOSApiService() - - @property - def uuid(self): - return self.api.get_uuid() - - @property - def headers(self): - self.api.get_session_challenge() - return {'session_challenge': self.api.get_session_token()} - - def get_wolfram_spoken(self, query): - reqdata = {"input": query.get("input"), - "units": query.get("units")} - url = f'https://api.openvoiceos.com/wolframalpha/spoken/{self.uuid}' - r = requests.post(url, data=reqdata, headers=self.headers) - return r - - def get_wolfram_simple(self, query): - reqdata = {"input": query.get("input"), - "units": query.get("units")} - url = f'https://api.openvoiceos.com/wolframalpha/simple/{self.uuid}' - r = requests.post(url, data=reqdata, headers=self.headers) - return r - - def get_wolfram_full(self, query): - reqdata = {"input": query.get("input"), - "units": query.get("units"), - "output": query.get("output", "json")} - url = f'https://api.openvoiceos.com/wolframalpha/full/{self.uuid}' - r = requests.post(url, data=reqdata, headers=self.headers) - if reqdata["output"] == "json": - return r.json() - else: - return r - - -class OvosEdamamRecipe: - @deprecated("No direct replacement", "0.1.0") - def __init__(self): - self.api = OVOSApiService() - - @property - def uuid(self): - return self.api.get_uuid() - - @property - def headers(self): - self.api.get_session_challenge() - return {'session_challenge': self.api.get_session_token()} - - def get_recipe(self, query): - reqdata = {"query": query.get("query"), - "count": query.get("count", 5)} - url = f'https://api.openvoiceos.com/recipes/search_recipe/' - r = requests.post(url, data=reqdata, headers=self.headers) - return r.json() - - -class OvosOmdb: - @deprecated("No direct replacement", "0.1.0") - def __init__(self): - self.api = OVOSApiService() - - @property - def uuid(self): - return self.api.get_uuid() - - @property - def headers(self): - self.api.get_session_challenge() - return {'session_challenge': self.api.get_session_token()} - - def search_movie(self, query): - reqdata = {"movie_name": query.get("movie_name"), - "movie_year": query.get("movie_year"), - "movie_id": query.get("movie_id")} - - url = f'https://api.openvoiceos.com/omdb/search_movie/' - r = requests.post(url, data=reqdata, headers=self.headers) - return r.json() - - -class OvosGeolocate: - @deprecated("Import from ovos-backend-client.api.GeolocationApi", "0.1.0") - def __init__(self): - pass - - def geolocate_ip(self, ip): - reqdata = {"address": ip} - url = f'https://api.openvoiceos.com/geolocate/ip/' - r = requests.post(url, data=reqdata) - return r.json() - - def geolocate_address(self, address): - reqdata = {"address": address} - url = f'https://api.openvoiceos.com/geolocate/address/' - r = requests.post(url, data=reqdata) - return r.json() - - def geolocate_location_config(self, address): - reqdata = {"address": address} - url = f'https://api.openvoiceos.com/geolocate/location/config' - r = requests.post(url, data=reqdata) - return r.json() - - -class OvosSendMail: - @deprecated("Import from ovos-backend-client.api.EmailApi", "0.1.0") - def __init__(self): - self.api = OVOSApiService() - - @property - def uuid(self): - return self.api.get_uuid() - - @property - def headers(self): - self.api.get_session_challenge() - return {'session_challenge': self.api.get_session_token()} - - def send_mail(self, recipient, subject, body): - reqdata = {"recipient": recipient, - "subject": subject, - "body": body} - url = f"https://api.openvoiceos.com/send/mail/{self.uuid}" - r = requests.post(url, data=reqdata, headers=self.headers) diff --git a/ovos_utils/parse.py b/ovos_utils/parse.py index 9d39b38f..cc385672 100644 --- a/ovos_utils/parse.py +++ b/ovos_utils/parse.py @@ -1,6 +1,7 @@ -from difflib import SequenceMatcher import re +from difflib import SequenceMatcher from enum import IntEnum, auto + from ovos_utils.log import LOG try: @@ -113,4 +114,3 @@ def remove_parentheses(answer): if not answer: return None return answer - diff --git a/ovos_utils/process_utils.py b/ovos_utils/process_utils.py index 72e1b4e0..1a8c981b 100644 --- a/ovos_utils/process_utils.py +++ b/ovos_utils/process_utils.py @@ -22,9 +22,8 @@ from threading import Event from time import sleep, monotonic - -from ovos_utils.log import LOG from ovos_utils.file_utils import get_temp_path +from ovos_utils.log import LOG @dataclass @@ -290,6 +289,7 @@ class PIDLock: # python 3+ 'class Lock' of the same type is started, this class will 'attempt' to stop the previously running process and then change the process ID in the lock file. """ + @classmethod def init(cls): # TODO: Path to deprecation @@ -301,6 +301,7 @@ def init(cls): "'mycroft' basedir") base_dir = "mycroft" cls.DIRECTORY = cls.DIRECTORY or get_temp_path(base_dir) + # # Class constants DIRECTORY = None diff --git a/ovos_utils/res/fallback_mycroft.conf b/ovos_utils/res/fallback_mycroft.conf deleted file mode 100644 index 3f9eac0d..00000000 --- a/ovos_utils/res/fallback_mycroft.conf +++ /dev/null @@ -1,461 +0,0 @@ -{ - // Definition and documentation of all variables used by mycroft-core. - // - // Settings seen here are considered DEFAULT. Settings can also be - // overridden at the REMOTE level (set by the user via - // https://home.mycroft.ai), at the SYSTEM level (typically in the file - // '/etc/mycroft/mycroft.conf'), or at the USER level (typically in the - // file '~/.config/mycroft/mycroft.conf'). - // - // The load order of settings is: - // DEFAULT - // REMOTE - // SYSTEM - // USER - // - // The Override: comments below indicates where these settings are generally - // set outside of this file. The load order is always followed, so an - // individual systems can still apply changes at the SYSTEM or USER levels. - - // Language used for speech-to-text and text-to-speech. - // Code is a BCP-47 identifier (https://tools.ietf.org/html/bcp47), lowercased - // TODO: save unmodified, lowercase upon demand - "lang": "en-us", - - // Measurement units, either 'metric' or 'english' - // Override: REMOTE - "system_unit": "metric", - - // Time format, either 'half' (e.g. "11:37 pm") or 'full' (e.g. "23:37") - // Override: REMOTE - "time_format": "half", - - // Date format, either 'MDY' (e.g. "11-29-1978") or 'DMY' (e.g. "29-11-1978") - // Override: REMOTE - "date_format": "MDY", - - // Whether to opt in to data collection - // Override: REMOTE - "opt_in": false, - - // Play a beep when system begins to listen? - "confirm_listening": true, - - // File locations of sounds to play for system events - "sounds": { - "start_listening": "snd/start_listening.wav", - "end_listening": "snd/end_listening.wav", - "acknowledge": "snd/acknowledge.mp3" - }, - - // Mechanism used to play WAV audio files - // Override: SYSTEM - "play_wav_cmdline": "aplay %1", - - // Mechanism used to play MP3 audio files - // Override: SYSTEM - "play_mp3_cmdline": "mpg123 %1", - - // Mechanism used to play OGG audio files - // Override: SYSTEM - "play_ogg_cmdline": "ogg123 -q %1", - - // Location where the system resides - // NOTE: Although this is set here, an Enclosure can override the value. - // For example a mycroft-core running in a car could use the GPS. - // Override: REMOTE - "location": { - "city": { - "code": "Lawrence", - "name": "Lawrence", - "state": { - "code": "KS", - "name": "Kansas", - "country": { - "code": "US", - "name": "United States" - } - } - }, - "coordinate": { - "latitude": 38.971669, - "longitude": -95.23525 - }, - "timezone": { - "code": "America/Chicago", - "name": "Central Standard Time", - "dstOffset": 3600000, - "offset": -21600000 - } - }, - - // default to $XDG_DATA_DIRS/mycroft - // "data_dir": "/opt/mycroft", - - // whenever core needs to cache some files this directory will be used, - // the main use case is for TTS files to avoid synthesizing the same thing - // more than once, you may set this to a permanent directory to keep these - // files across reboots, but be careful with space usage! - // if not set defaults to "/tmp/mycroft/cache" - // TIP: use "/dev/shm/mycroft/cache" if you want to keep the cache in RAM - "cache_path": "/tmp/mycroft/cache", - - # emit mycroft.ready signal when all these conditions are met - # different setups will have different needs - # eg, a server does not care about audio - # pairing -> device is paired - # internet -> device is connected to the internet - NOT IMPLEMENTED - # skills -> skills reported ready - # speech -> stt reported ready - # audio -> audio playback reported ready - # gui -> gui websocket reported ready - NOT IMPLEMENTED - # enclosure -> enclosure/HAL reported ready - NOT IMPLEMENTED - "ready_settings": ["skills"], - - // General skill values - "skills": { - - // don't start loading skills until internet is detected - // this config value is not present in mycroft-core ()internet is required) - // mycroft-lib expects that some instances will be running fully offline - "wait_for_internet": false, - - // relative to "data_dir" - "directory": "skills", - - // used by selene to display skill settings in web interface - "upload_skill_manifest": false, - - // blacklisted skills to not load - // NB: This is the basename() of the directory where the skill lives, so if - // the skill you want to blacklist is in /usr/share/mycroft/skills/mycroft-alarm.mycroftai/ - // then you should write `["mycroft-alarm.mycroftai"]` below. - "blacklisted_skills": [], - - // priority skills to be loaded first - "priority_skills": ["mycroft-pairing", "mycroft-volume"], - - // fallback skill configuration - "fallbacks": { - // you can add skill_id: priority to override the developer defined - // priority of those skills, this allows customization - // of unknown intent handling for default_skills + user preferences - "fallback_priorities": { - // "skill_id": 10 - }, - // fallback skill handling has 3 modes of operations: - // - "accept_all" # default mycroft-core behavior - // - "whitelist" # only call fallback for skills in "fallback_whitelist" - // - "blacklist" # only call fallback for skills NOT in "fallback_blacklist" - "fallback_mode": "accept_all", - "fallback_whitelist": [], - "fallback_blacklist": [] - }, - - // converse stage configuration - "converse": { - // the default number of seconds a skill remains active - // if the user does not interact with the skill in this timespan it - // will be deactivated, default 5 minutes (same as mycroft) - "timeout": 300, - // override of "skill_timeouts" per skill_id - "skill_timeouts": {}, - - // conversational mode has 3 modes of operations: - // - "accept_all" # default mycroft-core behavior - // - "whitelist" # only call converse for skills in "converse_whitelist" - // - "blacklist" # only call converse for skills NOT in "converse_blacklist" - "converse_mode": "accept_all", - "converse_whitelist": [], - "converse_blacklist": [], - - // converse activation has 4 modes of operations: - // - "accept_all" # default mycroft-core behavior, any skill can - // # activate itself unconditionally - // - "priority" # skills can only activate themselves if no skill with - // # higher priority is active - // - "whitelist" # only skills in "converse_whitelist" can activate themselves - // - "blacklist" # only skills NOT in converse "converse_blacklist" can activate themselves - // NOTE: this does not apply for regular skill activation, only to skill - // initiated activation requests - "converse_activation": "accept_all", - - // number of consecutive times a skill is allowed to activate itself - // per minute, -1 for no limit (default), 0 to disable self-activation - "max_activations": -1, - // override of "max_activations" per skill_id - "skill_activations": {}, - - // if false only skills can activate themselves - // if true any skill can activate any other skill - "cross_activation": true, - - // if false only skills can deactivate themselves - // if true any skill can deactivate any other skill - // NOTE: skill deactivation is not yet implemented - "cross_deactivation": true, - - // you can add skill_id: priority to override the developer defined - // priority of those skills, currently there is no api for skills to - // define their default priority, it is assumed to be 50, the only current - // canonical source for converse priorities is this setting - "converse_priorities": { - // "skill_id": 10 - } - } - - }, - - // system administrators can define different constraints in how configurations are loaded - // this is a mechanism to require root to change these config options - "system": { - // do not allow users to tamper with settings at all - "disable_user_config": false, - // do not allow remote backend to tamper with settings at all - "disable_remote_config": false, - // protected keys are individual settings that can not be changed at remote/user level - // nested dictionary keys can be defined with "key1:key2" syntax, - // eg. {"a": {"b": True, "c": False}} - // to protect "c" you would enter "a:c" in the section below - "protected_keys": { - // NOTE: selene backend expects "opt_in" to be changeable in their web ui - // that effectively gives them a means to enable spying without your input - // Mycroft AI can be trusted, but you dont need to anymore! - // The other keys are not currently populated by the remote backend - // they are defined for protection against bugs and for future proofing - // (what if facebook buys mycroft tomorrow?) - "remote": [ - "enclosure", - "server", - "system", - "websocket", - "gui_websocket", - "network_tests", - "listener:wake_word_upload:disable", - "skills:upload_skill_manifest", - "skills:auto_update", - "skills:priority_skills", - "skills:blacklisted_skills", - "opt_in" - ], - "user": [] - } - }, - - // Address of the REMOTE server - "server": { - "url": "https://api.mycroft.ai", - "version": "v1", - "update": false, - "disabled": true, - "metrics": false, - "sync_skill_settings": false - }, - - // The mycroft-core messagebus websocket - "websocket": { - "host": "0.0.0.0", - "port": 8181, - "route": "/core", - "ssl": false, - // in mycroft-core all skills share a bus, this allows malicious skills - // to manipulate it and affect other skills, this option ensures each skill - // gets it's own websocket connection - "shared_connection": true - }, - - // The GUI messagebus websocket. Once port is created per connected GUI - "gui_websocket": { - "host": "0.0.0.0", - "base_port": 18181, - "route": "/gui", - "ssl": false - }, - - // URIs to use for testing network connection. - "network_tests": { - "dns_primary": "8.8.8.8", - "dns_secondary": "8.8.4.4", - "web_url": "https://www.google.com", - "ncsi_endpoint": "http://www.msftncsi.com/ncsi.txt", - "ncsi_expected_text": "Microsoft NCSI" - }, - - // Settings used by the wake-up-word listener - // Override: REMOTE - "listener": { - "sample_rate": 16000, - - // if enabled the noise level is saved to a ipc file, useful for - // debuging if microphone is working but writes a lot to disk, - // recommended that you set "ipc_path" to a tmpfs - "mic_meter_ipc": false, - - // Set 'save_path' to configure the location of files stored if - // 'record_wake_words' and/or 'save_utterances' are set to 'true'. - // WARNING: Make sure that user 'mycroft' has write-access on the - // directory! - // "save_path": "/tmp", - // Set 'record_wake_words' to save a copy of wake word triggers - // as .wav files under: /'save_path'/mycroft_wake_words - "record_wake_words": false, - // Set 'save_utterances' to save each sentence sent to STT -- by default - // they are only kept briefly in-memory. This can be useful for for - // debugging or other custom purposes. Recordings are saved - // under: /'save_path'/mycroft_utterances/.wav - "save_utterances": false, - "wake_word_upload": { - "disable": true, - "url": "https://training.mycroft.ai/precise/upload" - }, - - // Override as SYSTEM or USER to select a specific microphone input instead of - // the PortAudio default input. - // "device_name": "somename", // can be regex pattern or substring - // or - // "device_index": 12, - - // Stop listing to the microphone during playback to prevent accidental triggering - // This is enabled by default, but instances with good microphone noise cancellation - // can disable this to listen all the time, allowing 'barge in' functionality. - "mute_during_output" : true, - - // How much (if at all) to 'duck' the speaker output during listening. A - // setting of 0.0 will not duck at all. A 1.0 will completely mute output - // while in a listening state. Values in between will lower the volume - // partially (this is optional behavior, depending on the enclosure). - "duck_while_listening" : 0.3, - - // In milliseconds - "phoneme_duration": 120, - "multiplier": 1.0, - "energy_ratio": 1.5, - - // DEPRECATED, multiple hotwords are supported now, see "hotwords" section below - //"wake_word": "hey mycroft", - //"stand_up_word": "wake up", - - // Settings used by microphone to set recording timeout - "recording_timeout": 10.0, - "recording_timeout_with_silence": 3.0, - - // instant listen is an experimental setting, it removes the need for - // the pause between "hey mycroft" and starting to speak the utterance, - //however it might slightly downgrade STT accuracy depending on engine used - "instant_listen": false - }, - - // Settings used for any precise wake words - "precise": { - "use_precise": false, - "dist_url": "https://github.com/MycroftAI/precise-data/raw/dist/{arch}/latest", - "model_url": "https://raw.githubusercontent.com/MycroftAI/precise-data/models/{wake_word}.tar.gz" - }, - - // Hotword configurations - "hotwords": { - "hey mycroft": { - "module": "ovos-ww-plugin-pocketsphinx", - "phonemes": "HH EY . M AY K R AO F T", - "threshold": 1e-90, - "lang": "en-us", - "listen": true, - "sound": "snd/start_listening.wav" - // Specify custom model via: - // "local_model_file": "~/.local/share/mycroft/precise/models/something.pb" - // Precise options: - // "sensitivity": 0.5, // Higher = more sensitive - // "trigger_level": 3 // Higher = more delay & less sensitive - }, - - "wake up": { - "module": "ovos-ww-plugin-pocketsphinx", - "phonemes": "W EY K . AH P", - "threshold": 1e-20, - "lang": "en-us", - "wakeup": true - } - }, - - // DEPRECATED: the concept of enclosure will no longer exist in ovos-core - // this has been replaced with PHAL - "enclosure": { - // Platform name - // Override: SYSTEM (set by specific enclosures) - "platform": "OpenVoiceOS", - - // The NTP sync should only forced on Raspberry Pi based devices. - "ntp_sync_on_boot": false, - - // for backwards compat NTP sync is automatically enabled for - // ('mycroft_mark_1', 'picroft', 'mycroft_mark_2pi') - // to disable forced ntp_sync in official mycroft platforms - // set this to false - "force_mycroft_ntp": false - - }, - - // Level of logs to store, one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" - // NOTE: This configuration setting is special and can only be changed in the - // SYSTEM or USER configuration file, it will not be read if defined in the - // DEFAULT (here) or in the REMOTE mycroft config. - // If not defined, the default log level is INFO. - //"log_level": "INFO", - - // Messagebus types that will NOT be output to logs - "ignore_logs": ["enclosure.mouth.viseme", "enclosure.mouth.display"], - - // Settings related to remote sessions - // Overrride: none - "session": { - // Time To Live, in seconds - "ttl": 180 - }, - - // Speech to Text parameters - // Override: REMOTE - "stt": { - // Engine. Options: "mycroft", "google", "wit", "ibm", "kaldi", "bing", - // "houndify", "deepspeech_server", "govivace", "yandex" - "module": "google" - // "deepspeech_server": { - // "uri": "http://localhost:8080/stt" - // }, - // "kaldi": { - // "uri": "http://localhost:8080/client/dynamic/recognize" - // }, - //"govivace": { - // "uri": "https://services.govivace.com:49149/telephony", - // "credential": { - // "token": "xxxxx" - // } - //} - }, - - // Text to Speech parameters - // Override: REMOTE - "tts": { - "pulse_duck": false, - "module": "ovos-tts-plugin-mimic2", - "fallback_module": "ovos-tts-plugin-mimic" - }, - - "padatious": { - "intent_cache": "~/.local/share/mycroft/intent_cache", - "train_delay": 4, - "single_thread": false, - "padaos_only": true - }, - - "Audio": { - "backends": { - "local": { - "type": "simple", - "active": true - } - }, - "default-backend": "local" - }, - - "debug": true -} diff --git a/ovos_utils/res/platform_fingerprints/spoofed_ovos.json b/ovos_utils/res/platform_fingerprints/spoofed_ovos.json deleted file mode 100644 index e0814b2f..00000000 --- a/ovos_utils/res/platform_fingerprints/spoofed_ovos.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "platform": "Linux-5.4.0-58-generic-x86_64-with-glibc2.29", - "enclosure": "OpenVoiceOS", - "python_version": "3.8.5", - "system": "Linux", - "version": "#64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020", - "arch": "x86_64", - "release": "5.4.0-58-generic", - "node": "user-Predator-PH315-51", - "desktop_env": "xfce4", - "ipc_path": "/ramdisk/mycroft/ipc/", - "input_device_name": "OpenVoiceOS", - "input_device_index": null, - "default_audio_backend": "vlc", - "msm_skills_dir": "skills", - "data_dir": "/opt/mycroft", - "priority_skills": [ - "mycroft-pairing", - "mycroft-volume" - ], - "backend_url": "https://api.mycroft.ai", - "mycroft_core_location": null, - "can_display": true, - "is_gui_installed": true, - "pulseaudio_running": true -} \ No newline at end of file diff --git a/ovos_utils/security.py b/ovos_utils/security.py index 3f746500..9b78bebf 100644 --- a/ovos_utils/security.py +++ b/ovos_utils/security.py @@ -1,11 +1,13 @@ -import platform -import pexpect -from socket import gethostname -from os.path import exists, join -from os import makedirs import os +import platform import random import string +from os import makedirs +from os.path import exists, join +from socket import gethostname + +import pexpect + from ovos_utils.log import LOG try: diff --git a/ovos_utils/signal.py b/ovos_utils/signal.py index dce01e86..36c29790 100644 --- a/ovos_utils/signal.py +++ b/ovos_utils/signal.py @@ -1,9 +1,7 @@ -import tempfile -import time - import os import os.path - +import tempfile +import time from ovos_utils.log import LOG, log_deprecation diff --git a/ovos_utils/skills.py b/ovos_utils/skills.py new file mode 100644 index 00000000..d38e041e --- /dev/null +++ b/ovos_utils/skills.py @@ -0,0 +1,35 @@ +from ovos_bus_client.util import wait_for_reply + + +def get_non_properties(obj): + """Get attributes that are not properties from object. + + Will return members of object class along with bases down to MycroftSkill. + + Args: + obj: object to scan + + Returns: + Set of attributes that are not a property. + """ + + def check_class(cls): + """Find all non-properties in a class.""" + # Current class + d = cls.__dict__ + np = [k for k in d if not isinstance(d[k], property)] + # Recurse through base classes excluding MycroftSkill and object + for b in [b for b in cls.__bases__ if b.__name__ not in ("object", "MycroftSkill")]: + np += check_class(b) + return np + + return set(check_class(obj.__class__)) + + +def skills_loaded(bus=None): + reply = wait_for_reply('mycroft.skills.all_loaded', + 'mycroft.skills.all_loaded.response', + bus=bus) + if reply: + return reply.data['status'] + return False diff --git a/ovos_utils/skills/__init__.py b/ovos_utils/skills/__init__.py deleted file mode 100644 index e861e313..00000000 --- a/ovos_utils/skills/__init__.py +++ /dev/null @@ -1,116 +0,0 @@ -from ovos_config.config import read_mycroft_config, update_mycroft_config -from ovos_utils.log import LOG, deprecated - - -def get_non_properties(obj): - """Get attributes that are not properties from object. - - Will return members of object class along with bases down to MycroftSkill. - - Args: - obj: object to scan - - Returns: - Set of attributes that are not a property. - """ - - def check_class(cls): - """Find all non-properties in a class.""" - # Current class - d = cls.__dict__ - np = [k for k in d if not isinstance(d[k], property)] - # Recurse through base classes excluding MycroftSkill and object - for b in [b for b in cls.__bases__ if b.__name__ not in ("object", "MycroftSkill")]: - np += check_class(b) - return np - - return set(check_class(obj.__class__)) - - -def skills_loaded(bus=None): - try: - from ovos_bus_client.util import wait_for_reply - except: - from ovos_utils.messagebus import wait_for_reply - reply = wait_for_reply('mycroft.skills.all_loaded', - 'mycroft.skills.all_loaded.response', - bus=bus) - if reply: - return reply.data['status'] - return False - - -@deprecated("This reference is deprecated, use " - "`ovos_workshop.permissions.blacklist_skill", "0.1.0") -def blacklist_skill(skill, config=None): - config = config or read_mycroft_config() - try: - from ovos_workshop.permissions import blacklist_skill as _wl - return _wl(skill, config) - except ImportError: - skills_config = config.get("skills", {}) - blacklisted_skills = skills_config.get("blacklisted_skills", []) - if skill not in blacklisted_skills: - blacklisted_skills.append(skill) - conf = { - "skills": { - "blacklisted_skills": blacklisted_skills - } - } - update_mycroft_config(conf) - return True - return False - - -@deprecated("This reference is deprecated, use " - "`ovos_workshop.permissions.whitelist_skill", "0.1.0") -def whitelist_skill(skill, config=None): - config = config or read_mycroft_config() - try: - from ovos_workshop.permissions import whitelist_skill as _wl - return _wl(skill, config) - except ImportError: - skills_config = config.get("skills", {}) - blacklisted_skills = skills_config.get("blacklisted_skills", []) - if skill in blacklisted_skills: - blacklisted_skills.pop(skill) - conf = { - "skills": { - "blacklisted_skills": blacklisted_skills - } - } - update_mycroft_config(conf) - return True - return False - - -@deprecated("This method is deprecated. Skills are now loaded based on " - "`runtime_requirements`", "0.1.0") -def make_priority_skill(skill, config=None): - # config = config or read_mycroft_config() - # skills_config = config.get("skills", {}) - # priority_skills = skills_config.get("priority_skills", []) - # if skill not in priority_skills: - # priority_skills.append(skill) - # conf = { - # "skills": { - # "priority_skills": priority_skills - # } - # } - # update_mycroft_config(conf) - # return True - return False - - -@deprecated("This reference is deprecated, use " - "`ovos_plugin_manager.skills.get_default_skill_dir", "0.1.0") -def get_skills_folder(config=None): - from ovos_utils.skills.locations import get_default_skills_directory - return get_default_skills_directory(config) - - -@deprecated("This reference is deprecated, use " - "`ovos_plugin_manager.skills.get_installed_skill_ids", "0.1.0") -def get_installed_skills(config=None): - from ovos_utils.skills.locations import get_installed_skill_ids - return get_installed_skill_ids(config) diff --git a/ovos_utils/skills/api.py b/ovos_utils/skills/api.py deleted file mode 100644 index 15186a1d..00000000 --- a/ovos_utils/skills/api.py +++ /dev/null @@ -1,74 +0,0 @@ -from ovos_utils.log import LOG, log_deprecation - -log_deprecation("ovos_utils.skills.api moved to ovos_workshop.skills.api", "0.1.0") - -try: - from ovos_workshop.skills.api import SkillApi -except: - from typing import Dict, Optional - from ovos_utils.fakebus import Message - - - class SkillApi: - """ - SkillApi provides a MessageBus interface to specific registered methods. - Methods decorated with `@skill_api_method` are exposed via the messagebus. - To use a skill's API methods, call `SkillApi.get` with the requested skill's - ID and an object is returned with an interface to all exposed methods. - """ - bus = None - - @classmethod - def connect_bus(cls, mycroft_bus): - """Registers the bus object to use.""" - cls.bus = mycroft_bus - - def __init__(self, method_dict: Dict[str, dict], timeout: int = 3): - """ - Initialize a SkillApi for the given methods - @param method_dict: dict of method name to dict containing: - `help` - method docstring - `type` - string Message type associated with this method - @param timeout: Seconds to wait for a Skill API response - """ - self.method_dict = method_dict - self.timeout = timeout - for key in method_dict: - def get_method(k): - def method(*args, **kwargs): - m = self.method_dict[k] - data = {'args': args, 'kwargs': kwargs} - method_msg = Message(m['type'], data) - response = \ - SkillApi.bus.wait_for_response(method_msg, - timeout=self.timeout) - if not response: - LOG.error(f"Timed out waiting for {method_msg}") - return None - elif 'result' not in response.data: - LOG.error(f"missing `result` in: {response.data}") - else: - return response.data['result'] - - return method - - self.__setattr__(key, get_method(key)) - - @staticmethod - def get(skill: str, api_timeout: int = 3) -> Optional[object]: - """ - Generate a SkillApi object for the requested skill if that skill exposes - and API methods. - @param skill: ID of skill to get an API object for - @param api_timeout: seconds to wait for a skill API response - @return: SkillApi object if available, else None - """ - if not SkillApi.bus: - raise RuntimeError("Requested update before `SkillAPI.bus` is set. " - "Call `SkillAPI.connect_bus` first.") - public_api_msg = f'{skill}.public_api' - api = SkillApi.bus.wait_for_response(Message(public_api_msg)) - if api: - return SkillApi(api.data, api_timeout) - else: - return None diff --git a/ovos_utils/skills/audioservice.py b/ovos_utils/skills/audioservice.py deleted file mode 100644 index 90ea7ecf..00000000 --- a/ovos_utils/skills/audioservice.py +++ /dev/null @@ -1,469 +0,0 @@ -# Copyright 2017 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from datetime import timedelta - -from ovos_utils.log import deprecated, log_deprecation - -log_deprecation("ClassicAudioServiceInterface and OCPInterface moved to ovos_bus_client.apis.ocp", "0.1.0") - -try: - from ovos_bus_client.apis.ocp import OCPInterface, ClassicAudioServiceInterface, ensure_uri -except ImportError: - from os.path import abspath - from ovos_utils.messagebus import get_mycroft_bus - from ovos_utils.fakebus import Message, dig_for_message - - def ensure_uri(s: str): - """ - Interpret paths as file:// uri's. - - Args: - s: string path to be checked - - Returns: - if s is uri, s is returned otherwise file:// is prepended - """ - if isinstance(s, str): - if '://' not in s: - return 'file://' + abspath(s) - else: - return s - elif isinstance(s, (tuple, list)): # Handle (mime, uri) arg - if '://' not in s[0]: - return 'file://' + abspath(s[0]), s[1] - else: - return s - else: - raise ValueError('Invalid track') - - - class ClassicAudioServiceInterface: - """AudioService class for interacting with the audio subsystem - - Audio is managed by OCP in the default implementation, - usually this class should not be directly used, see OCPInterface instead - - Args: - bus: Mycroft messagebus connection - """ - - def __init__(self, bus=None): - self.bus = bus or get_mycroft_bus() - - def queue(self, tracks=None): - """Queue up a track to playing playlist. - - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - """ - tracks = tracks or [] - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [ensure_uri(t) for t in tracks] - self.bus.emit(Message('mycroft.audio.service.queue', - data={'tracks': tracks})) - - def play(self, tracks=None, utterance=None, repeat=None): - """Start playback. - - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - utterance: forward utterance for further processing by the - audio service. - repeat: if the playback should be looped - """ - repeat = repeat or False - tracks = tracks or [] - utterance = utterance or '' - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [ensure_uri(t) for t in tracks] - self.bus.emit(Message('mycroft.audio.service.play', - data={'tracks': tracks, - 'utterance': utterance, - 'repeat': repeat})) - - def stop(self): - """Stop the track.""" - self.bus.emit(Message('mycroft.audio.service.stop')) - - def next(self): - """Change to next track.""" - self.bus.emit(Message('mycroft.audio.service.next')) - - def prev(self): - """Change to previous track.""" - self.bus.emit(Message('mycroft.audio.service.prev')) - - def pause(self): - """Pause playback.""" - self.bus.emit(Message('mycroft.audio.service.pause')) - - def resume(self): - """Resume paused playback.""" - self.bus.emit(Message('mycroft.audio.service.resume')) - - def get_track_length(self): - """ - getting the duration of the audio in seconds - """ - length = 0 - info = self.bus.wait_for_response( - Message('mycroft.audio.service.get_track_length'), - timeout=1) - if info: - length = info.data.get("length") or 0 - return length / 1000 # convert to seconds - - def get_track_position(self): - """ - get current position in seconds - """ - pos = 0 - info = self.bus.wait_for_response( - Message('mycroft.audio.service.get_track_position'), - timeout=1) - if info: - pos = info.data.get("position") or 0 - return pos / 1000 # convert to seconds - - def set_track_position(self, seconds): - """Seek X seconds. - - Arguments: - seconds (int): number of seconds to seek, if negative rewind - """ - self.bus.emit(Message('mycroft.audio.service.set_track_position', - {"position": seconds * 1000})) # convert to ms - - def seek(self, seconds=1): - """Seek X seconds. - - Args: - seconds (int): number of seconds to seek, if negative rewind - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - if seconds < 0: - self.seek_backward(abs(seconds)) - else: - self.seek_forward(seconds) - - def seek_forward(self, seconds=1): - """Skip ahead X seconds. - - Args: - seconds (int): number of seconds to skip - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - self.bus.emit(Message('mycroft.audio.service.seek_forward', - {"seconds": seconds})) - - def seek_backward(self, seconds=1): - """Rewind X seconds - - Args: - seconds (int): number of seconds to rewind - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - self.bus.emit(Message('mycroft.audio.service.seek_backward', - {"seconds": seconds})) - - def track_info(self): - """Request information of current playing track. - - Returns: - Dict with track info. - """ - info = self.bus.wait_for_response( - Message('mycroft.audio.service.track_info'), - reply_type='mycroft.audio.service.track_info_reply', - timeout=1) - return info.data if info else {} - - def available_backends(self): - """Return available audio backends. - - Returns: - dict with backend names as keys - """ - msg = Message('mycroft.audio.service.list_backends') - response = self.bus.wait_for_response(msg) - return response.data if response else {} - - @property - def is_playing(self): - """True if the audioservice is playing, else False.""" - return self.track_info() != {} - - - class OCPInterface: - """bus api interface for OCP subsystem - Args: - bus: Mycroft messagebus connection - """ - - def __init__(self, bus=None): - self.bus = bus or get_mycroft_bus() - - def _format_msg(self, msg_type, msg_data=None): - # this method ensures all skills are .forward from the utterance - # that triggered the skill, this ensures proper routing and metadata - msg_data = msg_data or {} - msg = dig_for_message() - if msg: - msg = msg.forward(msg_type, msg_data) - else: - msg = Message(msg_type, msg_data) - # at this stage source == skills, lets indicate audio service took over - sauce = msg.context.get("source") - if sauce == "skills": - msg.context["source"] = "audio_service" - return msg - - # OCP bus api - def queue(self, tracks): - """Queue up a track to OCP playing playlist. - - Args: - tracks: track dict or list of track dicts (OCP result style) - """ - - assert isinstance(tracks, list) - assert all(isinstance(t, dict) for t in tracks) - - msg = self._format_msg('ovos.common_play.playlist.queue', - {'tracks': tracks}) - self.bus.emit(msg) - - def play(self, tracks, utterance=None): - """Start playback. - Args: - tracks: track dict or list of track dicts (OCP result style) - utterance: forward utterance for further processing by OCP - - TODO handle utterance: - - allow services to register .voc files - - match utterance against vocs in OCP - - select audio service based on parsing - eg. "play X in spotify" - """ - assert isinstance(tracks, list) - assert all(isinstance(t, dict) for t in tracks) - - utterance = utterance or '' - - msg = self._format_msg('ovos.common_play.play', - {"media": tracks[0], - "playlist": tracks, - "utterance": utterance}) - self.bus.emit(msg) - - - -class AudioServiceInterface(ClassicAudioServiceInterface): - """ClassicAudioService compatible class for interacting with OCP subsystem - Args: - bus: Mycroft messagebus connection - """ - - @deprecated("AudioServiceInterface has been deprecated, compatibility " - "layer in use. please move to OCPInterface", "0.1.0") - def __init__(self, bus=None): - super().__init__(bus) - - @staticmethod - def _uri2meta(uri): - if isinstance(uri, list): - uri = uri[0] - try: - from ovos_ocp_files_plugin.plugin import OCPFilesMetadataExtractor - return OCPFilesMetadataExtractor.extract_metadata(uri) - except: - meta = {"uri": uri, - "skill_id": "mycroft.audio_interface", - "playback": 2, # PlaybackType.AUDIO, # TODO mime type check - "status": 33 # TrackState.QUEUED_AUDIO - } - meta["skill_id"] = "mycroft.audio_interface" - return meta - - def _format_msg(self, msg_type, msg_data=None): - # this method ensures all skills are .forward from the utterance - # that triggered the skill, this ensures proper routing and metadata - msg_data = msg_data or {} - msg = dig_for_message() - if msg: - msg = msg.forward(msg_type, msg_data) - else: - msg = Message(msg_type, msg_data) - # at this stage source == skills, lets indicate audio service took over - sauce = msg.context.get("source") - if sauce == "skills": - msg.context["source"] = "audio_service" - return msg - - # OCP bus api - def queue(self, tracks=None): - """Queue up a track to playing playlist. - TODO allow rich metadata for OCP: - - support dicts in tracks - NOTE: skills using this won't be compatible with mycroft-core ... - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - """ - tracks = tracks or [] - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [self._uri2meta(t) for t in tracks] - msg = self._format_msg('ovos.common_play.playlist.queue', - {'tracks': tracks}) - self.bus.emit(msg) - - def play(self, tracks=None, utterance=None, repeat=None): - """Start playback. - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - utterance: forward utterance for further processing by the - audio service. - repeat: if the playback should be looped - """ - repeat = repeat or False - tracks = tracks or [] - utterance = utterance or '' - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [self._uri2meta(t) for t in tracks] - - msg = self._format_msg('ovos.common_play.play', - {"media": tracks[0], - "playlist": tracks, - "utterance": utterance, - 'repeat': repeat}) - self.bus.emit(msg) - - def stop(self): - """Stop the track.""" - msg = self._format_msg("ovos.common_play.stop") - self.bus.emit(msg) - - def next(self): - """Change to next track.""" - msg = self._format_msg("ovos.common_play.next") - self.bus.emit(msg) - - def prev(self): - """Change to previous track.""" - msg = self._format_msg("ovos.common_play.previous") - self.bus.emit(msg) - - def pause(self): - """Pause playback.""" - msg = self._format_msg("ovos.common_play.pause") - self.bus.emit(msg) - - def resume(self): - """Resume paused playback.""" - msg = self._format_msg("ovos.common_play.resume") - self.bus.emit(msg) - - def seek_forward(self, seconds=1): - """Skip ahead X seconds. - Args: - seconds (int): number of seconds to skip - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - msg = self._format_msg('ovos.common_play.seek', - {"seconds": seconds}) - self.bus.emit(msg) - - def seek_backward(self, seconds=1): - """Rewind X seconds - Args: - seconds (int): number of seconds to rewind - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - msg = self._format_msg('ovos.common_play.seek', - {"seconds": seconds * -1}) - self.bus.emit(msg) - - def get_track_length(self): - """ - getting the duration of the audio in miliseconds - """ - length = 0 - msg = self._format_msg('ovos.common_play.get_track_length') - info = self.bus.wait_for_response(msg, timeout=1) - if info: - length = info.data.get("length", 0) - return length - - def get_track_position(self): - """ - get current position in miliseconds - """ - pos = 0 - msg = self._format_msg('ovos.common_play.get_track_position') - info = self.bus.wait_for_response(msg, timeout=1) - if info: - pos = info.data.get("position", 0) - return pos - - def set_track_position(self, miliseconds): - """Go to X position. - Arguments: - miliseconds (int): position to go to in miliseconds - """ - msg = self._format_msg('ovos.common_play.set_track_position', - {"position": miliseconds}) - self.bus.emit(msg) - - def track_info(self): - """Request information of current playing track. - Returns: - Dict with track info. - """ - msg = self._format_msg('ovos.common_play.track_info') - response = self.bus.wait_for_response(msg) - return response.data if response else {} - - def available_backends(self): - """Return available audio backends. - Returns: - dict with backend names as keys - """ - msg = self._format_msg('ovos.common_play.list_backends') - response = self.bus.wait_for_response(msg) - return response.data if response else {} diff --git a/ovos_utils/skills/locations.py b/ovos_utils/skills/locations.py deleted file mode 100644 index 9ab84428..00000000 --- a/ovos_utils/skills/locations.py +++ /dev/null @@ -1,155 +0,0 @@ -from os import makedirs, listdir -from os.path import join, isdir, dirname, expanduser, isfile -from typing import List, Optional - -from ovos_config.config import read_mycroft_config -from ovos_config.locations import get_xdg_data_save_path, get_xdg_data_dirs - -from ovos_utils.log import LOG, log_deprecation - -log_deprecation("ovos_utils.skills.locations moved to ovos_plugin_manager.skills", "0.1.0") - -try: - from ovos_plugin_manager.skills import * -except: - def get_installed_skill_ids(conf: Optional[dict] = None) -> List[str]: - """ - Gets a list of `skill_id`s for all installed skills - Args: - conf: Configuration, else loads from ovos_config.config.Configuration - Returns: - list of `skill_id` strings for all installed skills - """ - _, skill_ids = get_plugin_skills() - for d in get_skill_directories(conf): - for skill_dir in listdir(d): - if isdir(join(d, skill_dir)) and isfile(join(d, skill_dir, - "__init__.py")): - if skill_dir in skill_ids: - LOG.info(f"{skill_dir} installed as plugin and local dir") - continue - skill_ids.append(skill_dir) - return skill_ids - - - def get_skill_directories(conf: Optional[dict] = None) -> List[str]: - """ returns list of skill directories ordered by expected loading order - This corresponds to: - - XDG_DATA_DIRS - - default directory (see get_default_skills_directory method for details) - - user defined extra directories - Each directory contains individual skill folders to be loaded - If a skill exists in more than one directory (same folder name) previous instances will be ignored - ie. directories at the end of the list have priority over earlier directories - NOTE: empty folders are interpreted as disabled skills - new directories can be defined in mycroft.conf by specifying a full path - each extra directory is expected to contain individual skill folders to be loaded - the xdg folder name can also be changed, it defaults to "skills" - eg. ~/.local/share/mycroft/FOLDER_NAME - { - "skills": { - "directory": "skills", - "extra_directories": ["path/to/extra/dir/to/scan/for/skills"] - } - } - Args: - conf: Configuration, else loads from ovos_config.config.Configuration - Returns: - list of fully-qualified directories containing non-plugin skills - """ - # the contents of each skills directory must be individual skill folders - # we are still dependent on the mycroft-core structure of skill_id/__init__.py - - conf = conf or read_mycroft_config() - folder = conf["skills"].get("directory") or "skills" - # load all valid XDG paths - # NOTE: skills are actually code, but treated as user data! - # they should be considered applets rather than full applications - skill_locations = list(reversed( - [join(p, folder) for p in get_xdg_data_dirs() if - isdir(join(p, folder))] - )) - - # load the default skills folder - # only meaningful if xdg support is disabled - default = get_default_skills_directory(conf) - if default not in skill_locations: - skill_locations.append(default) - - # load additional explicitly configured directories - conf = conf.get("skills") or {} - # extra_directories is a list of directories containing skill subdirectories - # NOT a list of individual skill folders - # preserve order while removing any duplicate entries - extra_dirs = (expanduser(d) for d in conf.get("extra_directories") or []) - for d in extra_dirs: - if isdir(d) and d not in skill_locations: - skill_locations.append(d) - return skill_locations - - - def get_default_skills_directory(conf: Optional[dict] = None) -> str: - """ return default directory to scan for skills - This is only meaningful if xdg is disabled in ovos.conf - If xdg is enabled then data_dir is always XDG_DATA_DIR - If xdg is disabled then data_dir by default corresponds to /opt/mycroft - users can define the data directory in mycroft.conf - the skills folder name (relative to data_dir) can also be defined there - NOTE: folder name also impacts all XDG skill directories! - { - "data_dir": "/opt/mycroft", - "skills": { - "directory": "skills" - } - } - Args: - conf: Configuration, else loads from ovos_config.config.Configuration - Returns: - Absolute path to default skills directory - """ - conf = conf or read_mycroft_config() - path_override = conf["skills"].get("directory_override") - folder = conf["skills"].get("directory") or "skills" - - # if .conf wants to use a specific path, use it! - if path_override: - log_deprecation("'directory_override' is deprecated!" - "add the new path to 'extra_directories' instead", - "0.1.0") - skills_folder = expanduser(path_override) - elif conf["skills"].get("extra_directories") and \ - len(conf["skills"].get("extra_directories")) > 0: - skills_folder = expanduser(conf["skills"]["extra_directories"][0]) - else: - skills_folder = join(get_xdg_data_save_path(), folder) - # create folder if needed - try: - makedirs(skills_folder, exist_ok=True) - except PermissionError: # old style /opt/mycroft/skills not available - skills_folder = join(get_xdg_data_save_path(), folder) - makedirs(skills_folder, exist_ok=True) - - return skills_folder - - - def get_plugin_skills() -> (list, list): - """ - Get the package directories for any pip installed skill plugins - Returns: - lists of skill directories and plugin skill IDs - """ - import importlib.util - try: - from ovos_plugin_manager.skills import find_skill_plugins - except ImportError: - LOG.warning("ovos-plugin-manager not available to load plugin skills") - return [], [] - skill_dirs = list() - plugins = find_skill_plugins() - skill_ids = list(plugins.keys()) - for skill_class in plugins.values(): - skill_dir = dirname(importlib.util.find_spec( - skill_class.__module__).origin) - skill_dirs.append(skill_dir) - LOG.info(f"Located plugin skills: {skill_ids}") - return skill_dirs, skill_ids diff --git a/ovos_utils/skills/settings.py b/ovos_utils/skills/settings.py deleted file mode 100644 index 1d476040..00000000 --- a/ovos_utils/skills/settings.py +++ /dev/null @@ -1,132 +0,0 @@ -import json -from os.path import join, expanduser, exists - -import requests -from json_database import JsonStorageXDG, JsonStorage - -from ovos_utils.log import LOG, log_deprecation, deprecated - -log_deprecation("ovos_utils.skills.settings moved to ovos_workshop.settings", "0.1.0") - -try: - from ovos_workshop.settings import * - -except ImportError: - - def settings2meta(settings, section_name="Skill Settings"): - """ generates basic settingsmeta """ - fields = [] - - for k, v in settings.items(): - if k.startswith("_"): - continue - label = k.replace("-", " ").replace("_", " ").title() - if isinstance(v, bool): - fields.append({ - "name": k, - "type": "checkbox", - "label": label, - "value": str(v).lower() - }) - if isinstance(v, str): - fields.append({ - "name": k, - "type": "text", - "label": label, - "value": v - }) - if isinstance(v, int): - fields.append({ - "name": k, - "type": "number", - "label": label, - "value": str(v) - }) - return { - "skillMetadata": { - "sections": [ - { - "name": section_name, - "fields": fields - } - ] - } - } - - - class PrivateSettings(JsonStorageXDG): - def __init__(self, skill_id): - super(PrivateSettings, self).__init__(skill_id) - - @property - def settingsmeta(self): - return settings2meta(self, self.name) - - -@deprecated("deprecated without replacement, selene backend is dead", "0.1.0") -def get_remote_settings(skill_id, identity_file=None, backend_url=None): - """ WARNING: selene backend does not use proper skill_id, if you have - skills with same name but different author settings will overwrite each - other on the backend, THIS METHOD IS NOT SAFE - - skill matching is currently done by checking "if {skill} in string" - once mycroft fixes it on their side this will start using a proper - unique identifier - """ - data = get_all_remote_settings(identity_file, backend_url) - for k, v in data.items(): - if skill_id in k: - return v or {} - return data - - -@deprecated("deprecated without replacement, selene backend is dead", "0.1.0") -def get_all_remote_settings(identity_file=None, backend_url=None): - """ WARNING: selene backend does not use proper skill_id, if you have - skills with same name but different author settings will overwrite each - other on the backend, THIS METHOD IS NOT SAFE - """ - backend_url = backend_url or "https://api.mycroft.ai" - identity_file = identity_file or expanduser( - join("~", ".mycroft", "identity", "identity2.json")) - if not exists(identity_file): - return {} - with open(identity_file) as f: - identity = json.load(f) - url = backend_url + "/v1/device/" + identity["uuid"] + "/skill/settings" - params = {"Authorization": "Bearer " + identity["access"], - "Content-Type": "application/json" - } - return requests.get(url, headers=params).json() - - -@deprecated("deprecated without replacement, skill settings no longer shipped in skill folder", "0.1.0") -def get_local_settings(skill_dir, skill_name=None) -> dict: - """Build a JsonStorage using the JSON string stored in settings.json.""" - if skill_name: - log_deprecation("skill_name is an unused legacy argument", "0.1.0") - if skill_dir.endswith("/settings.json"): - settings_path = skill_dir - else: - settings_path = join(skill_dir, 'settings.json') - LOG.info(settings_path) - return JsonStorage(settings_path) - - -@deprecated("deprecated without replacement, skill settings no longer shipped in skill folder", "0.1.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 = 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.error(f'error saving skill settings to {settings_path}') - else: - LOG.info(f'Skill settings successfully saved to {settings_path}') diff --git a/ovos_utils/sound.py b/ovos_utils/sound.py new file mode 100644 index 00000000..d211b4d6 --- /dev/null +++ b/ovos_utils/sound.py @@ -0,0 +1,117 @@ +import os +import subprocess +from copy import deepcopy +from distutils.spawn import find_executable + +from ovos_utils.log import LOG + +try: + from ovos_config.config import read_mycroft_config +except ImportError: + LOG.warning("Config not provided and ovos_config not available") + + + def read_mycroft_config(): + return dict() + +# Create a custom environment to use that can let duck a music role. +# This is kept separate from the normal os.environ to ensure that +# any thirdparty software launched through +# a ovos process can select if they wish to honor this. +_ENVIRONMENT = deepcopy(os.environ) +_ENVIRONMENT['PULSE_PROP'] = 'media.role=phone' + + +def _get_pulse_environment(config): + """Return environment for pulse audio depeding on ducking config.""" + tts_config = config.get('tts', {}) + if tts_config and tts_config.get('pulse_duck'): + return _ENVIRONMENT + else: + return os.environ + + +def _find_player(uri): + _, ext = os.path.splitext(uri) + + # scan installed executables that can handle playback + sox_play = find_executable("play") + # sox should handle almost every format, but fails in some urls + if sox_play: + return sox_play + f" --type {ext} %1" + # determine best available player + ogg123_play = find_executable("ogg123") + if "ogg" in ext and ogg123_play: + return ogg123_play + " -q %1" + pw_play = find_executable("pw-play") + # pw_play handles both wav and mp3 + if pw_play: + return pw_play + " %1" + # wav file + if 'wav' in ext: + pulse_play = find_executable("paplay") + if pulse_play: + return pulse_play + " %1" + alsa_play = find_executable("aplay") + if alsa_play: + return alsa_play + " %1" + # guess mp3 + mpg123_play = find_executable("mpg123") + if mpg123_play: + return mpg123_play + " %1" + LOG.error("Can't find player for: %s", uri) + return None + + +def play_audio(uri, play_cmd=None, environment=None): + """Play an audio file. + + This wraps the other play_* functions, choosing the correct one based on + the file extension. The function will return directly and play the file + in the background. + + Args: + uri: uri to play + environment (dict): optional environment for the subprocess call + + Returns: subprocess.Popen object. None if the format is not supported or + an error occurs playing the file. + """ + config = read_mycroft_config() + environment = environment or _get_pulse_environment(config) + + # NOTE: some urls like youtube streams will cause extension detection to fail + # let's handle it explicitly + uri = uri.split("?")[0] + # Replace file:// uri's with normal paths + uri = uri.replace('file://', '') + + _, ext = os.path.splitext(uri) + + if not play_cmd: + if "ogg" in ext: + play_cmd = config.get("play_ogg_cmdline") + elif "wav" in ext: + play_cmd = config.get("play_wav_cmdline") + elif "mp3" in ext: + play_cmd = config.get("play_mp3_cmdline") + + if not play_cmd: + play_cmd = _find_player(uri) + + if not play_cmd: + LOG.error(f"Failed to play: No playback functionality available") + return None + + play_cmd = play_cmd.split(" ") + + for index, cmd in enumerate(play_cmd): + if cmd == "%1": + play_cmd[index] = uri + + try: + return subprocess.Popen(play_cmd, env=environment) + except Exception as e: + LOG.error(f"Failed to play: {play_cmd}") + LOG.exception(e) + return None diff --git a/ovos_utils/sound/__init__.py b/ovos_utils/sound/__init__.py deleted file mode 100644 index 056b3ce0..00000000 --- a/ovos_utils/sound/__init__.py +++ /dev/null @@ -1,276 +0,0 @@ -import os -import subprocess -import time -from copy import deepcopy -from distutils.spawn import find_executable - -from ovos_utils.file_utils import resolve_resource_file -from ovos_utils.log import LOG, deprecated -from ovos_utils.signal import check_for_signal - -try: - from ovos_config.config import read_mycroft_config -except ImportError: - LOG.warning("Config not provided and ovos_config not available") - - - def read_mycroft_config(): - return dict() - -# Create a custom environment to use that can let duck a music role. -# This is kept separate from the normal os.environ to ensure that -# any thirdparty software launched through -# a ovos process can select if they wish to honor this. -_ENVIRONMENT = deepcopy(os.environ) -_ENVIRONMENT['PULSE_PROP'] = 'media.role=phone' - - -def _get_pulse_environment(config): - """Return environment for pulse audio depeding on ducking config.""" - tts_config = config.get('tts', {}) - if tts_config and tts_config.get('pulse_duck'): - return _ENVIRONMENT - else: - return os.environ - - -def _play_default_sound_locally(sound_name): - audio_file = resolve_resource_file( - read_mycroft_config().get('sounds', {}).get(sound_name)) - if not audio_file: - LOG.warning(f"Could not find '{sound_name}' audio file!") - return - process = play_audio(audio_file) - if not process: - LOG.warning(f"Unable to play '{sound_name}' audio file!") - return process - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_acknowledge_sound(): - """Acknowledge a successful request. - - This method plays a sound to acknowledge a request that does not - require a verbal response. This is intended to provide simple feedback - to the user that their request was handled successfully. - """ - return _play_default_sound_locally('acknowledge') - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_listening_sound(): - """Audibly indicate speech recording started.""" - return _play_default_sound_locally('start_listening') - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_end_listening_sound(): - """Audibly indicate speech recording is no longer happening.""" - return _play_default_sound_locally('end_listening') - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_error_sound(): - """Audibly indicate a failed request. - - This method plays a error sound to signal an error that does not - require a verbal response. This is intended to provide simple feedback - to the user that their request was NOT handled successfully. - """ - return _play_default_sound_locally('error') - - -def _find_player(uri): - _, ext = os.path.splitext(uri) - - # scan installed executables that can handle playback - sox_play = find_executable("play") - # sox should handle almost every format, but fails in some urls - if sox_play: - return sox_play + f" --type {ext} %1" - # determine best available player - ogg123_play = find_executable("ogg123") - if "ogg" in ext and ogg123_play: - return ogg123_play + " -q %1" - pw_play = find_executable("pw-play") - # pw_play handles both wav and mp3 - if pw_play: - return pw_play + " %1" - # wav file - if 'wav' in ext: - pulse_play = find_executable("paplay") - if pulse_play: - return pulse_play + " %1" - alsa_play = find_executable("aplay") - if alsa_play: - return alsa_play + " %1" - # guess mp3 - mpg123_play = find_executable("mpg123") - if mpg123_play: - return mpg123_play + " %1" - LOG.error("Can't find player for: %s", uri) - return None - - -def play_audio(uri, play_cmd=None, environment=None): - """Play an audio file. - - This wraps the other play_* functions, choosing the correct one based on - the file extension. The function will return directly and play the file - in the background. - - Args: - uri: uri to play - environment (dict): optional environment for the subprocess call - - Returns: subprocess.Popen object. None if the format is not supported or - an error occurs playing the file. - """ - config = read_mycroft_config() - environment = environment or _get_pulse_environment(config) - - # NOTE: some urls like youtube streams will cause extension detection to fail - # let's handle it explicitly - uri = uri.split("?")[0] - # Replace file:// uri's with normal paths - uri = uri.replace('file://', '') - - _, ext = os.path.splitext(uri) - - if not play_cmd: - if "ogg" in ext: - play_cmd = config.get("play_ogg_cmdline") - elif "wav" in ext: - play_cmd = config.get("play_wav_cmdline") - elif "mp3" in ext: - play_cmd = config.get("play_mp3_cmdline") - - if not play_cmd: - play_cmd = _find_player(uri) - - if not play_cmd: - LOG.error(f"Failed to play: No playback functionality available") - return None - - play_cmd = play_cmd.split(" ") - - for index, cmd in enumerate(play_cmd): - if cmd == "%1": - play_cmd[index] = uri - - try: - return subprocess.Popen(play_cmd, env=environment) - except Exception as e: - LOG.error(f"Failed to play: {play_cmd}") - LOG.exception(e) - return None - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_wav(uri, play_cmd=None, environment=None): - """ Play a wav-file. - - Returns: subprocess.Popen object - """ - config = read_mycroft_config() - environment = environment or _get_pulse_environment(config) - play_cmd = play_cmd or config.get("play_wav_cmdline") or "paplay %1" - play_wav_cmd = str(play_cmd).split(" ") - for index, cmd in enumerate(play_wav_cmd): - if cmd == "%1": - play_wav_cmd[index] = uri - try: - return subprocess.Popen(play_wav_cmd, env=environment) - except Exception as e: - LOG.error("Failed to launch WAV: {}".format(play_wav_cmd)) - LOG.debug("Error: {}".format(repr(e)), exc_info=True) - return None - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_mp3(uri, play_cmd=None, environment=None): - """ Play a mp3-file. - - Returns: subprocess.Popen object - """ - config = read_mycroft_config() - environment = environment or _get_pulse_environment(config) - play_cmd = play_cmd or config.get("play_mp3_cmdline") or "mpg123 %1" - play_mp3_cmd = str(play_cmd).split(" ") - for index, cmd in enumerate(play_mp3_cmd): - if cmd == "%1": - play_mp3_cmd[index] = uri - try: - return subprocess.Popen(play_mp3_cmd, env=environment) - except Exception as e: - LOG.error("Failed to launch MP3: {}".format(play_mp3_cmd)) - LOG.debug("Error: {}".format(repr(e)), exc_info=True) - return None - - -@deprecated("please emit mycroft.audio.play_sound instead", "0.1.0") -def play_ogg(uri, play_cmd=None, environment=None): - """ Play a ogg-file. - - Returns: subprocess.Popen object - """ - config = read_mycroft_config() - environment = environment or _get_pulse_environment(config) - play_cmd = play_cmd or config.get("play_ogg_cmdline") or "ogg123 -q %1" - play_ogg_cmd = str(play_cmd).split(" ") - for index, cmd in enumerate(play_ogg_cmd): - if cmd == "%1": - play_ogg_cmd[index] = uri - try: - return subprocess.Popen(play_ogg_cmd, env=environment) - except Exception as e: - LOG.error("Failed to launch OGG: {}".format(play_ogg_cmd)) - LOG.debug("Error: {}".format(repr(e)), exc_info=True) - return None - - -@deprecated("please use ovos-dinkum-listener in recording mode instead", "0.1.0") -def record(file_path, duration, rate, channels): - """Simple function to record from the default mic. - - The recording is done in the background by the arecord commandline - application. - - Args: - file_path: where to store the recorded data - duration: how long to record - rate: sample rate - channels: number of channels - - Returns: - process for performing the recording. - """ - command = ['arecord', '-r', str(rate), '-c', str(channels)] - command += ['-d', str(duration)] if duration > 0 else [] - command += [file_path] - return subprocess.Popen(command) - - -@deprecated("file signals have been removed," - " TODO add new bus message for this method", "0.1.0") -def is_speaking(): - """Determine if Text to Speech is occurring - - Returns: - bool: True while still speaking - """ - return check_for_signal("isSpeaking", -1) - - -@deprecated("file signals have been removed," - " use session_id and recognizer_loop:audio_output_end to track this", "0.1.0") -def wait_while_speaking(): - """Pause as long as Text to Speech is still happening - - Pause while Text to Speech is still happening. This always pauses - briefly to ensure that any preceeding request to speak has time to - begin. - """ - time.sleep(0.3) # Wait briefly in for any queued speech to begin - while is_speaking(): - time.sleep(0.1) diff --git a/ovos_utils/sound/alsa.py b/ovos_utils/sound/alsa.py deleted file mode 100644 index b77dc070..00000000 --- a/ovos_utils/sound/alsa.py +++ /dev/null @@ -1,102 +0,0 @@ -try: - import alsaaudio -except ImportError: - alsaaudio = None -from ovos_utils.log import LOG, deprecated - - -class AlsaControl: - _mixer = None - - @deprecated("This class is deprecated! Controls moved to " - "ovos_phal_plugin_alsa.AlsaVolumeControlPlugin", "0.1.0") - def __init__(self, control=None): - if alsaaudio is None: - LOG.error("pyalsaaudio not installed") - LOG.info("Run pip install pyalsaaudio==0.8.2") - raise ImportError - if control is None: - control = alsaaudio.mixers()[0] - self.get_mixer(control) - - @property - def mixer(self): - return self._mixer - - def get_mixer(self, control="Master"): - if self._mixer is None: - try: - mixer = alsaaudio.Mixer(control) - except Exception as e: - try: - mixer = alsaaudio.Mixer(control) - except Exception as e: - try: - if control != "Master": - LOG.warning("could not allocate requested mixer, " - "falling back to 'Master'") - mixer = alsaaudio.Mixer("Master") - else: - raise - except Exception as e: - LOG.error("Couldn't allocate mixer") - LOG.exception(e) - raise - self._mixer = mixer - return self.mixer - - def increase_volume(self, percent): - volume = self.get_volume() - if isinstance(volume, list): - volume = volume[0] - volume += percent - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self.mixer.setvolume(int(volume)) - - def decrease_volume(self, percent): - volume = self.get_volume() - if isinstance(volume, list): - volume = volume[0] - volume -= percent - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self.mixer.setvolume(int(volume)) - - def set_volume_percent(self, percent): - self.set_volume(percent) - - def set_volume(self, volume): - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self.mixer.setvolume(int(volume)) - - def volume_range(self): - return self.mixer.getrange() - - def is_muted(self): - return bool(self.mixer.getmute()[0]) - - def mute(self): - return self.mixer.setmute(1) - - def unmute(self): - return self.mixer.setmute(0) - - def toggle_mute(self): - if self.is_muted(): - self.unmute() - else: - self.mute() - - def get_volume(self): - return self.mixer.getvolume()[0] - - def get_volume_percent(self): - return self.get_volume() diff --git a/ovos_utils/sound/pulse.py b/ovos_utils/sound/pulse.py deleted file mode 100644 index 7286801f..00000000 --- a/ovos_utils/sound/pulse.py +++ /dev/null @@ -1,161 +0,0 @@ -import subprocess -import re -import collections -from ovos_utils.log import LOG, deprecated - - -class PulseAudio: - volume_re = re.compile('^set-sink-volume ([^ ]+) (.*)') - mute_re = re.compile('^set-sink-mute ([^ ]+) ((?:yes)|(?:no))') - - @deprecated("This class is deprecated! Controls moved to " - "ovos_phal_plugin_pulseaudio.PulseAudioVolumeControlPlugin", - "0.1.0") - def __init__(self): - self._mute = collections.OrderedDict() - self._volume = collections.OrderedDict() - self.update() - - def normalize_sinks(self): - self.unmute_all() - volume = self.get_volume() - self.set_all_volumes(volume) - - def update(self): - proc = subprocess.Popen(['pacmd', 'dump'], stdout=subprocess.PIPE) - - for line in proc.stdout: - line = line.decode("utf-8") - volume_match = PulseAudio.volume_re.match(line) - mute_match = PulseAudio.mute_re.match(line) - - if volume_match: - self._volume[volume_match.group(1)] = int( - volume_match.group(2), 16) - elif mute_match: - self._mute[mute_match.group(1)] = mute_match.group( - 2).lower() == "yes" - - def _vol_to_percent(self, vol): - max_vol = 65536 - percent = vol * 100 / max_vol - return percent - - def _percent_to_vol(self, percent): - max_vol = 65536 - vol = percent * max_vol / 100 - return vol - - def get_volume_percent(self, sink=None): - vol = self.get_sink_volume(sink) - return self._vol_to_percent(vol) - - def get_mute(self, sink=None): - if not sink: - sink = list(self._mute.keys())[0] - - return self._mute[sink] - - def get_volume(self, sink=None): - return self.get_sink_volume(sink) - - def get_sink_volume(self, sink=None): - if not sink: - sink = list(self._volume.keys())[0] - - return self._volume[sink] - - def set_mute(self, mute, sink=None): - if not sink: - sink = list(self._mute.keys())[0] - - subprocess.Popen( - ['pacmd', 'set-sink-mute', sink, 'yes' if mute else 'no'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - self._mute[sink] = mute - - def set_volume(self, volume, sink=None): - self.set_sink_volume(volume, sink) - - def set_volume_percent(self, volume, sink=None): - self.set_sink_volume(self._percent_to_vol(volume), sink) - - def set_sink_volume(self, volume, sink=None): - if not sink: - sink = list(self._volume.keys())[0] - volume = int(volume) - subprocess.Popen(['pacmd', 'set-sink-volume', sink, hex(volume)], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - self._volume[sink] = volume - - def mute_all(self): - for sink in self.list_sinks(): - self.set_mute(True, sink) - - def unmute_all(self): - for sink in self.list_sinks(): - self.set_mute(False, sink) - - def set_all_volumes(self, volume): - self.set_all_sink_volumes(volume) - - def get_all_volumes(self): - return self.get_all_sink_volumes() - - def set_all_volumes_percent(self, percent): - volume = self._percent_to_vol(percent) - self.set_all_sink_volumes(volume) - - def get_all_volumes_percent(self): - return [self._vol_to_percent(volume) for volume in - self.get_all_sink_volumes()] - - def set_all_sink_volumes(self, volume): - for sink in self.list_sinks(): - self.set_volume(volume, sink) - - def get_all_sink_volumes(self): - volumes = [] - for sink in self.list_sinks(): - volumes.append(self.get_volume(sink)) - return volumes - - def list_sinks(self): - proc = subprocess.Popen(['pacmd', 'list-sinks'], - stdout=subprocess.PIPE) - sinks = [] - for line in proc.stdout: - line = line.decode("utf-8").strip() - if line.startswith("name: <"): - sink = line.replace("name: <", "")[:-1] - sinks.append(sink) - return sinks - - def list_sources(self): - proc = subprocess.Popen(['pacmd', 'list-sources'], - stdout=subprocess.PIPE) - sinks = [] - for line in proc.stdout: - line = line.decode("utf-8").strip() - if line.startswith("name: <"): - sink = line.replace("name: <", "")[:-1] - sinks.append(sink) - return sinks - - def increase_volume(self, percent): - volume = self.get_volume_percent() - volume += percent - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self.set_all_volumes_percent(volume) - - def decrease_volume(self, percent): - volume = self.get_volume_percent() - volume -= percent - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self.set_all_volumes_percent(volume) diff --git a/ovos_utils/ssml.py b/ovos_utils/ssml.py index b612be28..1d18d26c 100644 --- a/ovos_utils/ssml.py +++ b/ovos_utils/ssml.py @@ -70,7 +70,7 @@ def say_strong(self, text=None): raise TypeError('Parameter text must not be None') if len(self.text) and not self.text.endswith(" "): self.text += " " - self.text += "" + text\ + self.text += "" + text \ + "" return self @@ -80,7 +80,7 @@ def say_weak(self, text=None): raise TypeError('Parameter text must not be None') if len(self.text) and not self.text.endswith(" "): self.text += " " - self.text += "" + text\ + self.text += "" + text \ + " " return self diff --git a/ovos_utils/system.py b/ovos_utils/system.py index 1e64a127..a9b4bc93 100644 --- a/ovos_utils/system.py +++ b/ovos_utils/system.py @@ -4,27 +4,10 @@ import shutil import subprocess import sys -import sysconfig -from enum import Enum -from os.path import expanduser, exists, join from ovos_utils.log import LOG -# TODO: Deprecate MycroftRootLocations in 0.1.0 -class MycroftRootLocations(str, Enum): - PICROFT = "/home/pi/mycroft-core" - BIGSCREEN = "/home/mycroft/mycroft-core" - OVOS = "/usr/lib/python3.9/site-packages" - OLD_MARK1 = "/opt/venvs/mycroft-core/lib/python3.4/site-packages" - MARK1 = "/opt/venvs/mycroft-core/lib/python3.7/site-packages" - MARK2 = "/opt/mycroft" - HOME = expanduser("~/mycroft-core") # git clones - - -_USER_DEFINED_ROOT = None - - def is_running_from_module(module_name): # Stack: # [0] - _log() @@ -178,51 +161,6 @@ def check_service_active(service_name, sudo=False, user=False) -> bool: return state == 0 -# platform fingerprinting -def set_root_path(path): - global _USER_DEFINED_ROOT - _USER_DEFINED_ROOT = path - LOG.info(f"mycroft root set to {path}") - - -def find_root_from_sys_path(): - """Find mycroft root folder from sys.path, eg. venv site-packages.""" - for p in [path for path in sys.path if path != '']: - if exists(join(p, 'mycroft', 'configuration', 'mycroft.conf')): - return p - else: - return None - - -def find_root_from_sitepackages(): - """Find root from system or venv's sitepackages.""" - site = sysconfig.get_paths()['platlib'] - if exists(join(site, 'mycroft', 'configuration', 'mycroft.conf')): - return site - else: - return None - - -def search_mycroft_core_location(): - """Check python path (.venv), system packages and finally known mycroft - locations.""" - # downstream wants to override the root location - if _USER_DEFINED_ROOT: - return _USER_DEFINED_ROOT - # if we are in a .venv that should take precedence over everything else - if find_root_from_sitepackages(): - return find_root_from_sitepackages() - # if there is a system wide install that should take precedence over - # hardcoded locations - elif find_root_from_sys_path(): - return find_root_from_sys_path() - # finally look at default locations - for p in MycroftRootLocations: - if os.path.isdir(p): - return p - return None - - def get_desktop_environment(): # From http://stackoverflow.com/questions/2035657/what-is-my-current-desktop-environment # and http://ubuntuforums.org/showthread.php?t=652320 @@ -306,7 +244,7 @@ def has_screen(): have_display = b"device_name=" in subprocess.check_output("tvservice -n 2>&1", shell=True) except Exception as e: pass - + # fallback check using matplotlib if available # seems to be foolproof and OS agnostic # but do not want to drag the dependency @@ -345,4 +283,3 @@ def patched_getattr(name): module.__getattr__ = patched_getattr return func - diff --git a/ovos_utils/time.py b/ovos_utils/time.py index 28d2649f..0df363bd 100644 --- a/ovos_utils/time.py +++ b/ovos_utils/time.py @@ -1,7 +1,8 @@ from datetime import datetime -from dateutil.tz import gettz, tzlocal from typing import Any +from dateutil.tz import gettz, tzlocal + # used to calculate timespans DAYS_IN_1_YEAR = 365.2425 DAYS_IN_1_MONTH = 30.42 diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 4cbbbc84..fc61cda6 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -1,8 +1,7 @@ # The following lines are replaced during the release process. # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 0 -VERSION_BUILD = 38 -VERSION_POST = 0 -VERSION_ALPHA = 0 +VERSION_MINOR = 1 +VERSION_BUILD = 0 +VERSION_ALPHA = 3 # END_VERSION_BLOCK diff --git a/requirements/extras.txt b/requirements/extras.txt index 4f726695..c2815fca 100644 --- a/requirements/extras.txt +++ b/requirements/extras.txt @@ -1,3 +1,5 @@ rapidfuzz~=2.0 -ovos-bus-client < 0.1.0 -ovos-config < 0.1.0 \ No newline at end of file +ovos_plugin_manager>=0.0.25a2 +ovos-config>=0.0.12 +ovos-workshop>=0.0.13a22 +ovos-bus-client >=0.0.8a1 \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index 8c750061..00000000 --- a/requirements/test.txt +++ /dev/null @@ -1,3 +0,0 @@ -ovos_plugin_manager>=0.0.25a2 -ovos-config>=0.0.12a6 -ovos-workshop>=0.0.13a22 \ No newline at end of file diff --git a/setup.py b/setup.py index 4fd54b49..4b2cbaec 100644 --- a/setup.py +++ b/setup.py @@ -56,13 +56,6 @@ def required(requirements_file): name='ovos_utils', version=get_version(), packages=['ovos_utils', - 'ovos_utils.intents', - 'ovos_utils.sound', - "ovos_utils.enclosure", - 'ovos_utils.enclosure.mark1', - 'ovos_utils.enclosure.mark1.eyes', - 'ovos_utils.enclosure.mark1.faceplate', - 'ovos_utils.skills', 'ovos_utils.lang'], url='https://github.com/OpenVoiceOS/ovos_utils', install_requires=required("requirements/requirements.txt"), diff --git a/test/unittests/test_audio_utils.py b/test/unittests/test_audio_utils.py deleted file mode 100644 index f247979f..00000000 --- a/test/unittests/test_audio_utils.py +++ /dev/null @@ -1,173 +0,0 @@ - -from unittest import TestCase, mock - - -from ovos_utils.sound import (play_ogg, play_mp3, play_wav, play_audio, - record) -from ovos_utils.file_utils import get_temp_path - -test_config = { - 'play_wav_cmdline': 'mock_wav %1', - 'play_mp3_cmdline': 'mock_mp3 %1', - 'play_ogg_cmdline': 'mock_ogg %1' -} - - -class Anything: - """Class matching any object. - - Useful for assert_called_with arguments. - """ - def __eq__(self, other): - return True - - -@mock.patch('ovos_utils.sound.read_mycroft_config') -@mock.patch('ovos_utils.sound.subprocess') -class TestPlaySounds(TestCase): - def test_play_ogg(self, mock_subprocess, mock_conf): - mock_conf.return_value = test_config - play_ogg('insult.ogg') - mock_subprocess.Popen.assert_called_once_with(['mock_ogg', - 'insult.ogg'], - env=Anything()) - - @mock.patch('ovos_utils.sound.LOG') - def test_play_ogg_file_not_found(self, mock_log, - mock_subprocess, mock_conf): - """Test that simple log is raised when subprocess can't find command. - """ - def raise_filenotfound(*arg, **kwarg): - raise FileNotFoundError('TEST FILE NOT FOUND') - - mock_subprocess.Popen.side_effect = raise_filenotfound - mock_conf.return_value = test_config - self.assertEqual(play_ogg('insult.ogg'), None) - mock_log.error.called_once_with(Anything()) - - @mock.patch('ovos_utils.sound.LOG') - def test_play_ogg_exception(self, mock_log, - mock_subprocess, mock_conf): - """Test that stack trace is provided when unknown excpetion occurs""" - def raise_exception(*arg, **kwarg): - raise Exception - - mock_subprocess.Popen.side_effect = raise_exception - mock_conf.return_value = test_config - self.assertEqual(play_ogg('insult.ogg'), None) - mock_log.exception.called_once_with(Anything()) - - def test_play_mp3(self, mock_subprocess, mock_conf): - mock_conf.return_value = test_config - play_mp3('praise.mp3') - mock_subprocess.Popen.assert_called_once_with(['mock_mp3', - 'praise.mp3'], - env=Anything()) - - @mock.patch('ovos_utils.sound.LOG') - def test_play_mp3_file_not_found(self, mock_log, - mock_subprocess, mock_conf): - """Test that simple log is raised when subprocess can't find command. - """ - def raise_filenotfound(*arg, **kwarg): - raise FileNotFoundError('TEST FILE NOT FOUND') - - mock_subprocess.Popen.side_effect = raise_filenotfound - mock_conf.return_value = test_config - self.assertEqual(play_mp3('praise.mp3'), None) - mock_log.error.called_once_with(Anything()) - - @mock.patch('ovos_utils.sound.LOG') - def test_play_mp3_exception(self, mock_log, - mock_subprocess, mock_conf): - """Test that stack trace is provided when unknown excpetion occurs""" - def raise_exception(*arg, **kwarg): - raise Exception - - mock_subprocess.Popen.side_effect = raise_exception - mock_conf.return_value = test_config - self.assertEqual(play_mp3('praise.mp3'), None) - mock_log.exception.called_once_with(Anything()) - - def test_play_wav(self, mock_subprocess, mock_conf): - mock_conf.return_value = test_config - play_wav('indifference.wav') - mock_subprocess.Popen.assert_called_once_with(['mock_wav', - 'indifference.wav'], - env=Anything()) - - @mock.patch('ovos_utils.sound.LOG') - def test_play_wav_file_not_found(self, mock_log, - mock_subprocess, mock_conf): - """Test that simple log is raised when subprocess can't find command. - """ - def raise_filenotfound(*arg, **kwarg): - raise FileNotFoundError('TEST FILE NOT FOUND') - - mock_subprocess.Popen.side_effect = raise_filenotfound - mock_conf.return_value = test_config - self.assertEqual(play_wav('indifference.wav'), None) - mock_log.error.called_once_with(Anything()) - - @mock.patch('ovos_utils.sound.LOG') - def test_play_wav_exception(self, mock_log, - mock_subprocess, mock_conf): - """Test that stack trace is provided when unknown excpetion occurs""" - def raise_exception(*arg, **kwarg): - raise Exception - - mock_subprocess.Popen.side_effect = raise_exception - mock_conf.return_value = test_config - self.assertEqual(play_wav('indifference.wav'), None) - mock_log.exception.called_once_with(Anything()) - - def test_play_audio_file(self, mock_subprocess, mock_conf): - mock_conf.return_value = test_config - play_audio('indifference.wav') - mock_subprocess.Popen.assert_called_once_with(['mock_wav', - 'indifference.wav'], - env=Anything()) - mock_subprocess.Popen.reset_mock() - - play_audio('praise.mp3') - mock_subprocess.Popen.assert_called_once_with(['mock_mp3', - 'praise.mp3'], - env=Anything()) - mock_subprocess.Popen.reset_mock() - mock_conf.return_value = test_config - play_audio('insult.ogg') - mock_subprocess.Popen.assert_called_once_with(['mock_ogg', - 'insult.ogg'], - env=Anything()) - - -@mock.patch('ovos_utils.sound.subprocess') -class TestRecordSounds(TestCase): - def test_record_with_duration(self, mock_subprocess): - mock_proc = mock.Mock()(name='mock process') - mock_subprocess.Popen.return_value = mock_proc - rate = 16000 - channels = 1 - filename = get_temp_path('test.wav') - duration = 42 - res = record(filename, duration, rate, channels) - mock_subprocess.Popen.assert_called_once_with(['arecord', - '-r', str(rate), - '-c', str(channels), - '-d', str(duration), - filename]) - self.assertEqual(res, mock_proc) - - def test_record_without_duration(self, mock_subprocess): - mock_proc = mock.Mock(name='mock process') - mock_subprocess.Popen.return_value = mock_proc - rate = 16000 - channels = 1 - filename = get_temp_path('test.wav') - duration = 0 - res = record(filename, duration, rate, channels) - mock_subprocess.Popen.assert_called_once_with(['arecord', - '-r', str(rate), - '-c', str(channels), - filename]) - self.assertEqual(res, mock_proc) diff --git a/test/unittests/test_enclosure.py b/test/unittests/test_enclosure.py deleted file mode 100644 index 28963c3e..00000000 --- a/test/unittests/test_enclosure.py +++ /dev/null @@ -1,33 +0,0 @@ -import unittest -from unittest.mock import patch - -from ovos_utils.fakebus import FakeBus, Message - - -class TestEnclosureAPI(unittest.TestCase): - from ovos_utils.enclosure.api import EnclosureAPI - skill_id = "Enclosure Test" - bus = FakeBus() - api = EnclosureAPI(bus, skill_id) - # TODO: Test api methods - - @patch('ovos_utils.enclosure.api.dig_for_message') - def test_get_source_message(self, dig): - # No message in stack - dig.return_value = None - msg = self.api._get_source_message() - self.assertIsInstance(msg, Message) - self.assertEqual(msg.context["destination"], ["enclosure"]) - self.assertEqual(msg.context["skill_id"], self.skill_id) - - # With message in stack - test_message = Message("test", {"data": "something"}, - {"source": [""], "destination": [""]}) - dig.return_value = test_message - msg = self.api._get_source_message() - self.assertEqual(msg, test_message) - - -class TestMark1(unittest.TestCase): - # TODO Implement tests or move to separate PHAL plugin - pass diff --git a/test/unittests/test_events.py b/test/unittests/test_events.py index c8757695..cc0709fa 100644 --- a/test/unittests/test_events.py +++ b/test/unittests/test_events.py @@ -185,7 +185,7 @@ def test_event_container(self): class TestEventSchedulerInterface(unittest.TestCase): from ovos_utils.events import EventSchedulerInterface bus = FakeBus() - interface = EventSchedulerInterface(bus=bus, name="test") + interface = EventSchedulerInterface(bus=bus, skill_id="test") def test_00_init(self): from ovos_utils.events import EventContainer @@ -195,13 +195,9 @@ def test_00_init(self): self.assertEqual(self.interface.events.bus, self.bus) self.assertEqual(self.interface.scheduled_repeats, list()) - # Deprecated properties - self.assertEqual(self.interface.sched_id, self.interface.skill_id) - self.assertEqual(self.interface.name, self.interface.skill_id) - def test_set_bus(self): bus = FakeBus() - interface = self.EventSchedulerInterface(bus=bus, name="test") + interface = self.EventSchedulerInterface(bus=bus, skill_id="test") interface.set_bus(self.bus) self.assertEqual(interface.bus, self.bus) self.assertEqual(interface.events.bus, self.bus) diff --git a/test/unittests/test_gui.py b/test/unittests/test_gui.py deleted file mode 100644 index 77632d2b..00000000 --- a/test/unittests/test_gui.py +++ /dev/null @@ -1,450 +0,0 @@ -import unittest -from os.path import join, dirname, isfile -from threading import Event -from unittest.mock import patch, call, Mock - -from ovos_bus_client.message import Message - - -class TestGui(unittest.TestCase): - @patch("ovos_utils.gui.has_screen") - def test_can_display(self, has_screen): - from ovos_utils.gui import can_display - has_screen.return_value = False - self.assertFalse(can_display()) - has_screen.return_value = True - self.assertTrue(can_display()) - has_screen.return_value = None - self.assertFalse(can_display()) - - @patch("ovos_utils.gui.is_installed") - def test_is_gui_installed(self, is_installed): - from ovos_utils.gui import is_gui_installed - is_installed.return_value = False - self.assertFalse(is_gui_installed()) - is_installed.assert_has_calls([call("mycroft-gui-app"), - call("ovos-shell"), - call("mycroft-embedded-shell"), - call("plasmashell")]) - is_installed.return_value = True - self.assertTrue(is_gui_installed()) - is_installed.assert_called_with("mycroft-gui-app") - - # Test passed applications - self.assertTrue(is_gui_installed(["test"])) - is_installed.assert_called_with("test") - - @patch("ovos_utils.gui.is_process_running") - def test_is_gui_running(self, is_running): - from ovos_utils.gui import is_gui_running - is_running.return_value = False - self.assertFalse(is_gui_running()) - is_running.assert_has_calls([call("mycroft-gui-app"), - call("ovos-shell"), - call("mycroft-embedded-shell"), - call("plasmashell")]) - is_running.return_value = True - self.assertTrue(is_gui_running()) - is_running.assert_called_with("mycroft-gui-app") - - # Test passed applications - self.assertTrue(is_gui_running(["test"])) - is_running.assert_called_with("test") - - def test_is_gui_connected(self): - from ovos_utils.gui import is_gui_connected - # TODO - - def test_can_use_local_gui(self): - from ovos_utils.gui import can_use_local_gui - # TODO - - def test_can_use_gui(self): - from ovos_utils.gui import can_use_gui - # TODO - - def test_extend_about_data(self): - from ovos_utils.gui import extend_about_data - # TODO - - def test_gui_widgets(self): - from ovos_utils.gui import GUIWidgets - # TODO - - def test_gui_tracker(self): - from ovos_utils.gui import GUITracker - # TODO - - def test_gui_dict(self): - from ovos_utils.gui import _GUIDict - # TODO - - def test_get_ui_directories(self): - from ovos_utils.gui import get_ui_directories - test_dir = join(dirname(__file__), "test_ui") - - # gui dir (best practice) - dirs = get_ui_directories(test_dir) - self.assertEqual(dirs, {"all": join(test_dir, "gui")}) - - # ui and uid dirs (legacy) - dirs = get_ui_directories(join(test_dir, "legacy")) - self.assertEqual(dirs, {"qt5": join(test_dir, "legacy", "ui")}) - - -class TestGuiInterface(unittest.TestCase): - from ovos_utils.fakebus import FakeBus - from ovos_bus_client.apis.gui import GUIInterface - bus = FakeBus() - config = {"extension": "test"} - ui_base_dir = join(dirname(__file__), "test_ui") - ui_dirs = {'qt5': join(ui_base_dir, 'ui')} - iface_name = "test_interface" - - volunteered_upload = Mock() - bus.on('gui.volunteer_page_upload', volunteered_upload) - - interface = GUIInterface(iface_name, bus, None, config, ui_dirs) - - def test_00_gui_interface_init(self): - self.assertEqual(self.interface.config, self.config) - self.assertEqual(self.interface.bus, self.bus) - self.assertIsNone(self.interface.remote_url) - self.assertIsNone(self.interface.on_gui_changed_callback) - self.assertEqual(self.interface.ui_directories, self.ui_dirs) - self.assertEqual(self.interface.skill_id, self.iface_name) - self.assertIsNone(self.interface.page) - self.assertIsInstance(self.interface.connected, bool) - self.volunteered_upload.assert_called_once() - upload_message = self.volunteered_upload.call_args[0][0] - self.assertEqual(upload_message.data["skill_id"], self.iface_name) - - # Test GUI init with no ui directories - self.GUIInterface("no_ui_dirs_gui", self.bus, None, self.config) - self.volunteered_upload.assert_called_once_with(upload_message) - - def test_build_message_type(self): - name = "test" - self.assertEqual(self.interface.build_message_type(name), - f"{self.iface_name}.{name}") - - name = f"{self.iface_name}.{name}" - self.assertEqual(self.interface.build_message_type(name), name) - - def test_setup_default_handlers(self): - # TODO - pass - - def test_upload_gui_pages(self): - msg = Message("") - handled = Event() - - def on_pages(message): - nonlocal msg - msg = message - handled.set() - - self.bus.on('gui.page.upload', on_pages) - - # Upload default/legacy behavior (qt5 `ui` dir) - message = Message('test', {}, {'context': "Test"}) - self.interface.upload_gui_pages(message) - self.assertTrue(handled.wait(2)) - - self.assertEqual(msg.context['context'], message.context['context']) - self.assertEqual(msg.msg_type, "gui.page.upload") - self.assertEqual(msg.data['__from'], self.iface_name) - - pages = msg.data['pages'] - self.assertIsInstance(pages, dict) - for key, val in pages.items(): - self.assertIsInstance(key, str) - self.assertIsInstance(val, str) - - test_file_key = "test.qml" - self.assertEqual(bytes.fromhex(pages.get(test_file_key)), - b"Mock File Contents", pages) - - test_file_key = join("subdir", "test.qml") - self.assertEqual(bytes.fromhex(pages.get(test_file_key)), - b"Nested Mock", pages) - - # Upload all resources - handled.clear() - self.interface.ui_directories['all'] = join(dirname(__file__), - 'test_ui', 'gui') - message = Message('test', {"framework": "all"}, {'context': "All"}) - self.interface.upload_gui_pages(message) - self.assertTrue(handled.wait(2)) - - self.assertEqual(msg.context['context'], message.context['context']) - self.assertEqual(msg.msg_type, "gui.page.upload") - self.assertEqual(msg.data['__from'], self.iface_name) - - pages = msg.data['pages'] - self.assertIsInstance(pages, dict) - for key, val in pages.items(): - self.assertIsInstance(key, str) - self.assertIsInstance(val, str) - - self.assertEqual(bytes.fromhex(pages.get("qt5/test.qml")), - b"qt5", pages) - self.assertEqual(bytes.fromhex(pages.get("qt6/test.qml")), - b"qt6", pages) - - # Upload requested other skill - handled.clear() - message = Message('test', {"framework": "all", - "skill_id": "other_skill"}) - self.interface.upload_gui_pages(message) - self.assertFalse(handled.wait(2)) - - def test_register_handler(self): - # TODO - pass - - def test_set_on_gui_changed(self): - # TODO - pass - - def test_gui_set(self): - # TODO - pass - - def test_sync_data(self): - # TODO - pass - - def test_get(self): - # TODO - pass - - def test_clear(self): - # TODO - pass - - def test_send_event(self): - # TODO - pass - - @patch('ovos_bus_client.apis.gui.resolve_resource_file') - @patch('ovos_bus_client.apis.gui.resolve_ovos_resource_file') - def test_pages2uri(self, ovos_res, res): - def _resolve(name, config): - self.assertEqual(config, self.interface.config) - if name.startswith("ui/core"): - return f"res/{name}" - - def _ovos_resolve(name, extra_dirs): - self.assertEqual(extra_dirs, - list(self.interface.ui_directories.values())) - if name.startswith("ui/ovos"): - return f"ovos/{name}" - - # Mock actual resource resolution methods - ovos_res.side_effect = _ovos_resolve - res.side_effect = _resolve - - # remote_url is None - # OVOS Res - self.assertEqual(self.interface._pages2uri(["ui/ovos/test"]), - ["file://ovos/ui/ovos/test"]) - ovos_res.assert_called_once() - self.assertEqual(self.interface._pages2uri(["ovos/test"]), - ["file://ovos/ui/ovos/test"]) - res.assert_not_called() - # Core Res - self.assertEqual(self.interface._pages2uri(["ui/core/test"]), - ["file://res/ui/core/test"]) - res.assert_called_once() - self.assertEqual(self.interface._pages2uri(["core/test"]), - ["file://res/ui/core/test"]) - - def test_normalize_page_name(self): - legacy_name = "test.qml" - name_with_path = "subdir/test" - name_with_dot = "subdir/test.file" - self.assertEqual(self.interface._normalize_page_name(legacy_name), - "test") - self.assertEqual(self.interface._normalize_page_name(name_with_path), - "subdir/test") - self.assertEqual(self.interface._normalize_page_name(name_with_dot), - "subdir/test.file") - - def test_show_page(self): - real_show_pages = self.interface.show_pages - self.interface.show_pages = Mock() - - # Default args - self.interface.show_page("test") - self.interface.show_pages.assert_called_once_with(["test"], 0, - None, False) - - self.interface.show_page("test2", True, True) - self.interface.show_pages.assert_called_with(["test2"], 0, - True, True) - - self.interface.show_page("test3", 30, True) - self.interface.show_pages.assert_called_with(["test3"], 0, - 30, True) - self.interface.show_pages = real_show_pages - - def test_show_pages(self): - msg: Message = Message("") - handled = Event() - - def _gui_value_set(message): - self.assertEqual(message.data['__from'], self.interface.skill_id) - - def _gui_page_show(message): - nonlocal msg - msg = message - handled.set() - - self.bus.on('gui.value.set', _gui_value_set) - self.bus.on('gui.page.show', _gui_page_show) - - # Test resource absolute paths - file_base_dir = join(dirname(__file__), "test_ui", "ui") - files = [join(file_base_dir, "test.qml"), - join(file_base_dir, "subdir", "test.qml")] - self.interface.show_pages(files) - self.assertTrue(handled.wait(2)) - self.assertEqual(msg.msg_type, "gui.page.show") - for page in msg.data['page']: - self.assertTrue(page.startswith("file://")) - path = page.replace("file://", "") - self.assertTrue(isfile(path), page) - self.assertEqual(len(msg.data['page']), len(msg.data['page_names'])) - self.assertIsInstance(msg.data["index"], int) - self.assertEqual(msg.data['__from'], self.interface.skill_id) - self.assertIsNone(msg.data["__idle"]) - self.assertIsInstance(msg.data["__animations"], bool) - self.assertEqual(msg.data["ui_directories"], - self.interface.ui_directories) - - # Test resources resolved locally - handled.clear() - files = ["file.qml", "subdir/file.qml"] - index = 1 - override_idle = 30 - override_animations = True - self.interface.show_pages(files, index, override_idle, - override_animations) - self.assertTrue(handled.wait(2)) - self.assertEqual(msg.msg_type, "gui.page.show") - for page in msg.data['page']: - self.assertTrue(page.startswith("file://")) - path = page.replace("file://", "") - self.assertTrue(isfile(path), page) - self.assertEqual(msg.data["page_names"], ["file", "subdir/file"]) - self.assertEqual(msg.data["index"], index) - self.assertEqual(msg.data["__from"], self.interface.skill_id) - self.assertEqual(msg.data["__idle"], override_idle) - self.assertEqual(msg.data["__animations"], override_animations) - self.assertEqual(msg.data["ui_directories"], - self.interface.ui_directories) - - # Test resources not resolved locally - handled.clear() - files = ["file.qml", "other.page"] - index = 1 - override_idle = 30 - override_animations = True - self.interface.show_pages(files, index, override_idle, - override_animations) - self.assertTrue(handled.wait(2)) - self.assertEqual(msg.msg_type, "gui.page.show") - self.assertEqual(msg.data["page"], list()) - self.assertEqual(msg.data["page_names"], ["file", "other.page"]) - self.assertEqual(msg.data["index"], index) - self.assertEqual(msg.data["__from"], self.interface.skill_id) - self.assertEqual(msg.data["__idle"], override_idle) - self.assertEqual(msg.data["__animations"], override_animations) - self.assertEqual(msg.data["ui_directories"], - self.interface.ui_directories) - - def test_remove_page(self): - real_remove_pages = self.interface.remove_pages - self.interface.remove_pages = Mock() - self.interface.remove_page("test_page") - self.interface.remove_pages.assert_called_once_with(["test_page"]) - self.interface.remove_pages = real_remove_pages - - def test_remove_pages(self): - msg = Message("") - handled = Event() - - def _gui_page_delete(message): - nonlocal msg - msg = message - handled.set() - - self.bus.on("gui.page.delete", _gui_page_delete) - - # Test resolved page - pages = ["test.qml"] - self.interface.remove_pages(pages) - self.assertTrue(handled.wait(2)) - self.assertEqual(msg.msg_type, "gui.page.delete") - self.assertEqual(len(msg.data['page']), len(pages)) - for page in msg.data['page']: - self.assertTrue(page.startswith("file://")) - path = page.replace("file://", "") - self.assertTrue(isfile(path), page) - self.assertEqual(msg.data['page_names'], ["test"]) - self.assertEqual(msg.data['__from'], self.interface.skill_id) - - # Test unresolved pages - handled.clear() - pages = ['file.qml', 'dir/other.file'] - self.interface.remove_pages(pages) - self.assertTrue(handled.wait(2)) - self.assertEqual(msg.msg_type, "gui.page.delete") - self.assertEqual(msg.data['page'], []) - self.assertEqual(msg.data['page_names'], ["file", "dir/other.file"]) - self.assertEqual(msg.data['__from'], self.interface.skill_id) - - def test_show_notification(self): - # TODO - pass - - def test_show_controlled_notification(self): - # TODO - pass - - def test_remove_controlled_notification(self): - # TODO - pass - - def test_show_text(self): - # TODO - pass - - def test_show_image(self): - # TODO - pass - - def test_show_animated_image(self): - # TODO - pass - - def test_show_html(self): - # TODO - pass - - def test_show_url(self): - # TODO - pass - - def test_input_box(self): - # TODO - pass - - def test_release(self): - # TODO - pass - - def test_shutdown(self): - # TODO - pass diff --git a/test/unittests/test_intents.py b/test/unittests/test_intents.py deleted file mode 100644 index a98667ed..00000000 --- a/test/unittests/test_intents.py +++ /dev/null @@ -1,353 +0,0 @@ -import unittest -from threading import Event -from time import sleep -from unittest.mock import Mock - -from ovos_utils.fakebus import FakeBus - - -class TestIntent(unittest.TestCase): - from ovos_utils.intents import Intent - # TODO - - -class TestIntentBuilder(unittest.TestCase): - from ovos_utils.intents import IntentBuilder - # TODO - - -class TestAdaptIntent(unittest.TestCase): - from ovos_utils.intents import AdaptIntent - # TODO - pass - - -class TestConverse(unittest.TestCase): - from ovos_utils.intents.converse import ConverseTracker - # TODO - pass - - -class TestIntentServiceInterfaceFunctions(unittest.TestCase): - def test_to_alnum(self): - from ovos_utils.intents.intent_service_interface import to_alnum - test_alnum = "test_skill123" - self.assertEqual(test_alnum, to_alnum(test_alnum)) - test_dash = "test-skill123" - self.assertEqual(test_alnum, to_alnum(test_dash)) - test_slash = "test/skill123" - self.assertEqual(test_alnum, to_alnum(test_slash)) - - def test_munge_regex(self): - from ovos_utils.intents.intent_service_interface import munge_regex - skill_id = "test_skill" - non_regex = "just a string with no entity" - with_regex = "a string with this (?P.*)" - munged_regex = f"a string with this (?P<{skill_id}entity>.*)" - - self.assertEqual(non_regex, munge_regex(non_regex, skill_id)) - self.assertEqual(munged_regex, munge_regex(with_regex, skill_id)) - - def test_munge_intent_parser(self): - from ovos_utils.intents.intent_service_interface import \ - munge_intent_parser - # TODO - - def test_intent_query_api(self): - from ovos_utils.intents.intent_service_interface import IntentQueryApi - # TODO - - def test_open_intent_envelope(self): - pass - # TODO: Deprecated? - - -class TestIntentServiceInterface(unittest.TestCase): - from ovos_utils.intents.intent_service_interface import \ - IntentServiceInterface - bus = FakeBus() - intent_interface = IntentServiceInterface(bus) - test_id = "test_interface.test" - - def test_00_init(self): - self.assertEqual(self.intent_interface.bus, self.bus) - self.intent_interface._bus = None - self.assertIsNone(self.intent_interface._bus) - self.assertIsInstance(self.intent_interface.skill_id, str) - self.assertEqual(self.intent_interface.registered_intents, list()) - self.assertEqual(self.intent_interface.detached_intents, list()) - self.assertEqual(self.intent_interface.intent_names, list()) - - with self.assertRaises(RuntimeError): - _ = self.intent_interface.bus - self.intent_interface.set_bus(self.bus) - self.assertEqual(self.intent_interface.bus, self.bus) - - self.intent_interface.set_id(self.test_id) - self.assertEqual(self.intent_interface.skill_id, self.test_id) - - def test_register_adapt_keyword(self): - event = Event() - - register_vocab = Mock(side_effect=lambda x: event.set()) - - self.bus.on("register_vocab", register_vocab) - - # Test without aliases - event.clear() - self.intent_interface.register_adapt_keyword('test_intent', - 'test', lang='en-us') - self.assertTrue(event.wait(2)) - register_vocab.assert_called_once() - message = register_vocab.call_args[0][0] - self.assertEqual(message.msg_type, "register_vocab") - self.assertEqual(message.context, - {"skill_id": self.intent_interface.skill_id, - "session": message.context["session"]}) - data = message.data - self.assertEqual(data['entity_value'], 'test') - self.assertEqual(data['entity_type'], 'test_intent') - self.assertEqual(data['lang'], 'en-us') - - # Test with aliases - register_vocab.reset_mock() - event.clear() - self.intent_interface.register_adapt_keyword('test_intent', 'test', - ['test2', 'test3'], - 'en-us') - self.assertTrue(event.wait(2)) - while len(register_vocab.call_args_list) < 3: - # TODO: Better method to wait for all of the expected calls - sleep(0.2) - self.assertEqual(register_vocab.call_count, 3) - first_msg = register_vocab.call_args_list[0][0][0] - second_msg = register_vocab.call_args_list[1][0][0] - third_msg = register_vocab.call_args_list[2][0][0] - self.assertEqual(first_msg.serialize(), message.serialize()) - self.assertEqual(second_msg.context, message.context) - self.assertEqual(second_msg.data['entity_value'], 'test2') - self.assertEqual(second_msg.data['entity_type'], 'test_intent') - self.assertEqual(second_msg.data['lang'], 'en-us') - self.assertEqual(third_msg.context, message.context) - self.assertEqual(third_msg.data['entity_value'], 'test3') - self.assertEqual(third_msg.data['entity_type'], 'test_intent') - self.assertEqual(third_msg.data['lang'], 'en-us') - - self.bus.remove("register_vocab", register_vocab) - - def test_register_adapt_regex(self): - valid_regex = "(is|as) (?P.*)" - invalid_regex = "(is|as) no entity" - called = Event() - register_vocab = Mock(side_effect=lambda x: called.set()) - self.bus.on("register_vocab", register_vocab) - lang = "en-gb" - - # Test valid regex - self.intent_interface.register_adapt_regex(valid_regex, lang) - self.assertTrue(called.wait(2)) - message = register_vocab.call_args[0][0] - self.assertEqual(message.msg_type, "register_vocab") - self.assertEqual(message.data['regex'], valid_regex) - self.assertEqual(message.data['lang'], lang) - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - - # Test invalid regex (no validation in interface) - called.clear() - self.intent_interface.register_adapt_regex(invalid_regex, lang) - self.assertTrue(called.wait(2)) - message = register_vocab.call_args[0][0] - self.assertEqual(message.msg_type, "register_vocab") - self.assertEqual(message.data['regex'], invalid_regex) - self.assertEqual(message.data['lang'], lang) - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - - self.bus.remove("register_vocab", register_vocab) - - def test_register_adapt_intent(self): - mock_adapt_intent = Mock() - mock_intent_dict = {"name": "test_intent", - "requires": ['required_word'], - "at_least_one": ['opt1', 'opt2'], - "optional": []} - mock_adapt_intent.__dict__ = mock_intent_dict - - event = Event() - register_intent = Mock(side_effect=lambda x: event.set()) - self.bus.on("register_intent", register_intent) - - # Test register intent same name - self.intent_interface.register_adapt_intent("test_intent", - mock_adapt_intent) - self.assertTrue(event.wait(2)) - message = register_intent.call_args[0][0] - self.assertEqual(message.msg_type, "register_intent") - self.assertEqual(message.data, mock_intent_dict) - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - self.assertEqual(self.intent_interface.registered_intents[-1], - ("test_intent", mock_adapt_intent)) - self.assertNotIn(("test_intent", mock_adapt_intent), - self.intent_interface.detached_intents) - - # Test register intent different name no longer detached - event.clear() - self.intent_interface.detached_intents.append(("test_detached", - mock_intent_dict)) - self.intent_interface.register_adapt_intent("test_detached", - mock_adapt_intent) - self.assertTrue(event.wait(2)) - message = register_intent.call_args[0][0] - self.assertEqual(message.msg_type, "register_intent") - self.assertEqual(message.data, mock_intent_dict) - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - self.assertEqual(self.intent_interface.registered_intents[-1], - ("test_detached", mock_adapt_intent)) - self.assertNotIn(("test_detached", mock_adapt_intent), - self.intent_interface.detached_intents) - - self.bus.remove("register_intent", register_intent) - - def test_remove_intent(self): - test_name = "remove_intent" - test_data = {"name": "remove"} - - event = Event() - handle_detach = Mock(side_effect=lambda x: event.set()) - self.bus.on("detach_intent", handle_detach) - - # Test valid remove intent - self.intent_interface.registered_intents.append((test_name, test_data)) - self.intent_interface.registered_intents.append((test_name, test_data)) - self.intent_interface.remove_intent(test_name) - self.assertTrue(event.wait(2)) - handle_detach.assert_called_once() - self.assertNotIn((test_name, test_data), - self.intent_interface.registered_intents) - self.assertIn((test_name, test_data), - self.intent_interface.detached_intents) - message = handle_detach.call_args[0][0] - self.assertEqual(message.msg_type, "detach_intent") - self.assertEqual(message.data['intent_name'], - f"{self.intent_interface.skill_id}:{test_name}") - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - - # Test invalid remove intent (no change) - event.clear() - self.intent_interface.remove_intent(test_name) - self.assertTrue(event.wait(2)) - self.assertEqual(handle_detach.call_count, 2) - new_message = handle_detach.call_args[0][0] - self.assertEqual(message.serialize(), new_message.serialize()) - - self.bus.remove("detach_intent", handle_detach) - - def test_intent_is_detached(self): - detached_name = "is_detached" - detached_mock = Mock() - self.intent_interface.detached_intents.append((detached_name, - detached_mock)) - self.assertTrue(self.intent_interface.intent_is_detached(detached_name)) - self.assertFalse(self.intent_interface.intent_is_detached("not_detach")) - - def test_set_adapt_context(self): - context = "ctx_key" - word = "ctx_val" - origin = "origin" - - event = Event() - add_context = Mock(side_effect=lambda x: event.set()) - self.bus.on("add_context", add_context) - - self.intent_interface.set_adapt_context(context, word, origin) - self.assertTrue(event.wait(2)) - add_context.assert_called_once() - message = add_context.call_args[0][0] - self.assertEqual(message.msg_type, "add_context") - self.assertEqual(message.data, {"context": context, - "word": word, - "origin": origin}) - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - - self.bus.remove("add_context", add_context) - - def test_remove_adapt_context(self): - context = "ctx_key" - - event = Event() - remove_context = Mock(side_effect=lambda x: event.set()) - self.bus.on("remove_context", remove_context) - - self.intent_interface.remove_adapt_context(context) - self.assertTrue(event.wait(2)) - remove_context.assert_called_once() - message = remove_context.call_args[0][0] - self.assertEqual(message.msg_type, "remove_context") - self.assertEqual(message.data, {"context": context}) - self.assertEqual(message.context['skill_id'], - self.intent_interface.skill_id) - - self.bus.remove("remove_context", remove_context) - - def test_register_padatious_intent(self): - from pathlib import Path - intent_name = "test" - lang = "en-us" - with self.assertRaises(ValueError): - self.intent_interface.register_padatious_intent(intent_name, Path(), - lang) - with self.assertRaises(FileNotFoundError): - self.intent_interface.register_padatious_intent(intent_name, - "/test", lang) - - # TODO - - def test_register_padatious_entity(self): - from pathlib import Path - intent_name = "test" - lang = "en-us" - with self.assertRaises(ValueError): - self.intent_interface.register_padatious_entity(intent_name, Path(), - lang) - with self.assertRaises(FileNotFoundError): - self.intent_interface.register_padatious_entity(intent_name, - "/test", lang) - - # TODO - - def test_detach_all(self): - # TODO - pass - - def test_get_intent(self): - valid_intent = Mock() - disabled_intent = Mock() - self.intent_interface.registered_intents.append(("get_valid", - valid_intent)) - self.intent_interface.detached_intents.append(("get_disabled", - disabled_intent)) - self.assertEqual(self.intent_interface.get_intent("get_valid"), - valid_intent) - self.assertEqual(self.intent_interface.get_intent("get_disabled"), - disabled_intent) - self.assertIsNone(self.intent_interface.get_intent("invalid_intent")) - - def test_iter(self): - self.intent_interface.registered_intents.append(("test_iter", Mock())) - intents = [] - for intent in self.intent_interface: - self.assertIn(intent, self.intent_interface.registered_intents) - intents.append(intent) - self.assertEqual(len(intents), - len(self.intent_interface.registered_intents)) - - def test_contains(self): - test_intent = ("test_contains", Mock()) - self.intent_interface.registered_intents.append(test_intent) - self.assertTrue(test_intent[0] in self.intent_interface) - self.assertFalse("test_not_contains" in self.intent_interface) diff --git a/test/unittests/test_skills.py b/test/unittests/test_skills.py deleted file mode 100644 index 2b698f45..00000000 --- a/test/unittests/test_skills.py +++ /dev/null @@ -1,225 +0,0 @@ -import unittest -from os import environ -from os.path import isdir, join, dirname, basename -from unittest.mock import patch - -from ovos_utils.fakebus import FakeBus, Message -from ovos_utils.skills.locations import get_skill_directories -from ovos_utils.skills.locations import get_default_skills_directory -from ovos_utils.skills.locations import get_installed_skill_ids -from ovos_utils.skills.locations import get_plugin_skills - -try: - import ovos_config -except ImportError: - ovos_config = None - - -def _api_method_1(message: Message) -> str: - return message.serialize() - - -def _api_method_2(**kwargs) -> int: - return len(kwargs) - - -class TestSkills(unittest.TestCase): - def test_get_non_properties(self): - from ovos_utils.skills import get_non_properties - # TODO - - def test_skills_loaded(self): - from ovos_utils.skills import skills_loaded - # TODO - - @patch("ovos_utils.skills.update_mycroft_config") - def test_blacklist_skill(self, update_config): - from ovos_utils.skills import blacklist_skill - # TODO - - @patch("ovos_utils.skills.update_mycroft_config") - def test_whitelist_skill(self, update_config): - from ovos_utils.skills import whitelist_skill - # TODO - - -class TestAudioservice(unittest.TestCase): - def test_ensure_uri(self): - from ovos_utils.skills.audioservice import ensure_uri - valid_uri = "file:///test" - non_uri = "/test" - # rel_uri = "test" - self.assertEqual(ensure_uri(valid_uri), valid_uri) - self.assertEqual(ensure_uri(non_uri), valid_uri) - # TODO: Relative path is relative to method and not caller? - # self.assertEqual(ensure_uri(rel_uri), - # f"file://{join(dirname(__file__), 'test')}") - - def test_classic_audio_service_interface(self): - from ovos_utils.skills.audioservice import ClassicAudioServiceInterface - # TODO - - def test_audio_service_interface(self): - from ovos_utils.skills.audioservice import AudioServiceInterface - # TODO - - def test_ocp_interface(self): - from ovos_utils.skills.audioservice import OCPInterface - # TODO - - -class TestLocations(unittest.TestCase): - @patch("ovos_plugin_manager.skills.get_plugin_skills") - def test_get_installed_skill_ids(self, plugins): - plugins.return_value = (['plugin_dir', 'plugin_dir_2'], - ['plugin_id', 'plugin_id_2']) - environ["XDG_DATA_DIRS"] = join(dirname(__file__), "test_skills_xdg") - config = {"skills": { - "extra_directories": [join(dirname(__file__), "test_skills_dir")] - }} - skill_ids = get_installed_skill_ids(config) - self.assertEqual(set(skill_ids), {"plugin_id", "plugin_id_2", - "skill-test-1.openvoiceos", - "skill-test-2.openvoiceos"}) - - def test_get_skill_directories(self): - if not ovos_config: - return # skip test since ovos.conf isn't taken into account - - # Default behavior, only one valid XDG path - environ["XDG_DATA_DIRS"] = environ["XDG_DATA_HOME"] = \ - join(dirname(__file__), "test_skills_xdg") - config = {"skills": {"extra_directories": []}} - default_dir = join(dirname(__file__), "test_skills_xdg", - "mycroft", "skills") - self.assertEqual(get_skill_directories(config), [default_dir]) - - # Define single extra directory to append - extra_dir = join(dirname(__file__), "test_skills_dir") - config['skills']['extra_directories'] = [extra_dir] - self.assertEqual(get_skill_directories(config), - [default_dir, extra_dir]) - - # Define duplicated directories in extra_directories - config['skills']['extra_directories'] += [extra_dir, default_dir] - self.assertEqual(get_skill_directories(config), - [default_dir, extra_dir]) - - # Define invalid directories in extra_directories - config['skills']['extra_directories'] += ["/not/a/directory"] - self.assertEqual(get_skill_directories(config), - [default_dir, extra_dir]) - - # Default directory - mock_config = {'skills': {}} - default_directories = get_skill_directories(mock_config) - for directory in default_directories: - self.assertEqual(basename(directory), 'skills') - # Configured directory - mock_config['skills']['directory'] = 'test' - test_directories = get_skill_directories(mock_config) - for directory in test_directories: - self.assertEqual(basename(directory), 'test') - self.assertEqual(len(default_directories), len(test_directories)) - # Extra directory - extra_dir = join(dirname(__file__), 'skills') - mock_config['skills']['extra_directories'] = [extra_dir] - extra_directories = get_skill_directories(mock_config) - self.assertEqual(extra_directories[-1], extra_dir) - for directory in test_directories: - self.assertIn(directory, extra_directories) - - def test_get_default_skills_directory(self): - if not ovos_config: - return # skip test since ovos.conf isn't taken into account - test_skills_dir = join(dirname(__file__), "test_skills_dir") - - # Configured override (legacy) - config = {"skills": {"directory_override": test_skills_dir}} - self.assertEqual(get_default_skills_directory(config), test_skills_dir) - - # Configured extra_directories - config = {"skills": {"extra_directories": [test_skills_dir, "/tmp"]}} - self.assertEqual(get_default_skills_directory(config), test_skills_dir) - - environ["XDG_DATA_HOME"] = join(dirname(__file__), "test_skills_xdg") - xdg_skills_dir = join(dirname(__file__), "test_skills_xdg", - "mycroft", "skills") - # XDG (undefined extra_directories) - config = {"skills": {}} - self.assertEqual(get_default_skills_directory(config), xdg_skills_dir) - - # XDG (empty extra_directories) - config = {"skills": {"extra_directories": []}} - self.assertEqual(get_default_skills_directory(config), xdg_skills_dir) - - # Default directory - mock_config = {'skills': {}} - default_dir = get_default_skills_directory(mock_config) - self.assertTrue(isdir(default_dir)) - self.assertEqual(basename(default_dir), 'skills') - self.assertEqual(dirname(dirname(default_dir)), - join(dirname(__file__), "test_skills_xdg")) - # Override directory - mock_config['skills']['directory'] = 'test' - test_dir = get_default_skills_directory(mock_config) - self.assertTrue(isdir(test_dir)) - self.assertEqual(basename(test_dir), 'test') - self.assertEqual(dirname(dirname(test_dir)), - join(dirname(__file__), "test_skills_xdg")) - - def test_get_plugin_skills(self): - dirs, ids = get_plugin_skills() - for d in dirs: - self.assertTrue(isdir(d)) - for s in ids: - self.assertIsInstance(s, str) - self.assertEqual(len(dirs), len(ids)) - - -class TestSkillApi(unittest.TestCase): - from ovos_utils.skills.api import SkillApi - bus = FakeBus() - SkillApi.connect_bus(bus) - - def test_skill_api_init(self): - from ovos_utils.skills.api import SkillApi - test_api = SkillApi({"serialize": {'help': '', - 'type': 'test._api_method_1'}, - "get_length": {'help': '', - 'type': 'test._api_method_2'}}) - self.assertEqual(test_api.bus, self.bus) - self.assertEqual(SkillApi.bus, self.bus) - self.assertIsNotNone(test_api.serialize) - self.assertIsNotNone(test_api.get_length) - self.assertTrue(callable(test_api.serialize)) - self.assertTrue(callable(test_api.get_length)) - - def test_skill_api_get(self): - from ovos_utils.skills.api import SkillApi - - def _valid_public_api(message): - self.bus.emit(message.response( - {"serialize": {'help': '', 'type': 'test._api_method_1'}, - "get_length": {'help': '', 'type': 'test._api_method_2'}})) - - self.bus.on("test_skill.public_api", _valid_public_api) - - # Test get valid API - api = SkillApi.get("test_skill") - self.assertIsInstance(api, SkillApi) - self.assertTrue(callable(api.serialize)) - self.assertTrue(callable(api.get_length)) - - # Test second API - api2 = SkillApi.get("test_skill") - self.assertEqual(api.method_dict, api2.method_dict) - - # Test invalid API - self.assertIsNone(SkillApi.get("other_skill")) - - # Test get without bus - SkillApi.bus = None - with self.assertRaises(RuntimeError): - SkillApi.get("test_skill") - SkillApi.connect_bus(self.bus) diff --git a/test/unittests/test_sound.py b/test/unittests/test_sound.py index b95e25c4..fca73be9 100644 --- a/test/unittests/test_sound.py +++ b/test/unittests/test_sound.py @@ -8,22 +8,6 @@ def test_get_pulse_environment(self): from ovos_utils.sound import _get_pulse_environment # TODO - def test_play_acknowledge_sound(self): - from ovos_utils.sound import play_acknowledge_sound - # TODO - - def test_play_listening_sound(self): - from ovos_utils.sound import play_listening_sound - # TODO - - def test_play_end_listening_sound(self): - from ovos_utils.sound import play_end_listening_sound - # TODO - - def test_play_error_sound(self): - from ovos_utils.sound import play_error_sound - # TODO - def test_find_player(self): from ovos_utils.sound import _find_player # TODO @@ -32,26 +16,3 @@ def test_play_audio(self): from ovos_utils.sound import play_audio # TODO - def test_play_wav(self): - from ovos_utils.sound import play_wav - # TODO - - def test_play_mp3(self): - from ovos_utils.sound import play_wav - # TODO - - def test_play_ogg(self): - from ovos_utils.sound import play_ogg - # TODO - - def test_record(self): - from ovos_utils.sound import record - # TODO - - def test_is_speaking(self): - from ovos_utils.sound import is_speaking - # TODO - - def test_wait_while_speaking(self): - from ovos_utils.sound import wait_while_speaking - # TODO diff --git a/test/unittests/test_system.py b/test/unittests/test_system.py index 4644731d..e513ac80 100644 --- a/test/unittests/test_system.py +++ b/test/unittests/test_system.py @@ -56,30 +56,6 @@ def test_check_service_active(self): # TODO pass - def test_set_root_path(self): - from ovos_utils.system import set_root_path - set_root_path("test") - from ovos_utils.system import _USER_DEFINED_ROOT - self.assertEqual(_USER_DEFINED_ROOT, "test") - set_root_path("mycroft") - from ovos_utils.system import _USER_DEFINED_ROOT - self.assertEqual(_USER_DEFINED_ROOT, "mycroft") - set_root_path(None) - from ovos_utils.system import _USER_DEFINED_ROOT - self.assertIsNone(_USER_DEFINED_ROOT) - - def test_find_root_from_sys_path(self): - # TODO - pass - - def test_find_root_from_sitepackages(self): - # TODO - pass - - def test_search_mycroft_core_location(self): - # TODO - pass - def test_get_desktop_environment(self): # TODO pass From 23e70215d23cf719540b9fe1311744af228192cb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 21:08:46 +0000 Subject: [PATCH 02/87] Increment Version to 0.1.0a4 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index fc61cda6..43e8541c 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK From 77e451a50ced5c15edc90b4cf9f9ad1884927281 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 21:09:14 +0000 Subject: [PATCH 03/87] Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b425147..c0a56ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [V0.1.0a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a3) (2023-12-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.38...V0.1.0a3) + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 262d9c158eb180d61f307026bbfa1e5bb954eb84 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 30 Dec 2023 01:06:28 +0000 Subject: [PATCH 04/87] fix/log_spam (#213) --- ovos_utils/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_utils/gui.py b/ovos_utils/gui.py index 845cf73a..c53705b8 100644 --- a/ovos_utils/gui.py +++ b/ovos_utils/gui.py @@ -85,8 +85,8 @@ def get_ui_directories(root_dir: str) -> dict: LOG.debug("Skill implements resources in `gui` directory") ui_directories["all"] = join(base_directory, "gui") return ui_directories - LOG.info("Checking for legacy UI directories") + if isdir(join(base_directory, "ui")): - LOG.debug("Handling `ui` directory as `qt5`") + LOG.debug("legacy UI directory found - Handling `ui` directory as `qt5`") ui_directories["qt5"] = join(base_directory, "ui") return ui_directories From 4c875ddcff6cb34b0a9d16240af13e8e0a8209dd Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 30 Dec 2023 01:06:41 +0000 Subject: [PATCH 05/87] Increment Version to 0.1.0a5 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 43e8541c..a1eca47f 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 4 +VERSION_ALPHA = 5 # END_VERSION_BLOCK From c94032ec33308001100dd672a591b54212331449 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 30 Dec 2023 01:07:11 +0000 Subject: [PATCH 06/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a56ec7..ec8ef9fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.1.0a5](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a5) (2023-12-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a4...0.1.0a5) + +**Merged pull requests:** + +- fix/log\_spam [\#213](https://github.com/OpenVoiceOS/ovos-utils/pull/213) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.1.0a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a4) (2023-12-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a3...V0.1.0a4) + ## [V0.1.0a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a3) (2023-12-29) [Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.38...V0.1.0a3) From e011854c1f608457817721b0359edb74130ee902 Mon Sep 17 00:00:00 2001 From: Swen Gross <25036977+emphasize@users.noreply.github.com> Date: Sat, 30 Dec 2023 19:23:24 +0100 Subject: [PATCH 07/87] Feat/ovos logs script (#203) * rename var for clearity remove extra env var option readme docs add requirements revert env var situation make ovos-config optional add ovos-logs reduce command optics adjustments add `ovos-logs` console script * reorder to account for directories kwarg --------- Co-authored-by: JarbasAi --- ovos_utils/log.py | 97 ++++- ovos_utils/log_parser.py | 663 ++++++++++++++++++++++++++++++++++ readme.md | 79 +++- requirements/requirements.txt | 3 + setup.py | 7 +- 5 files changed, 838 insertions(+), 11 deletions(-) create mode 100644 ovos_utils/log_parser.py diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 5f8fe83d..86c30048 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -17,7 +17,19 @@ import sys from logging.handlers import RotatingFileHandler from os.path import join -from typing import List +from pathlib import Path +from typing import Optional, List, Set + +ALL_SERVICES = {"bus", + "audio", + "skills", + "voice", + "gui", + "ovos", + "phal", + "phal-admin", + "hivemind", + "hivemind-voice-sat"} class LOG: @@ -78,18 +90,17 @@ def __init__(cls, name='OVOS'): @classmethod def init(cls, config=None): - + from ovos_utils.xdg_utils import xdg_state_home try: from ovos_config.meta import get_xdg_base - default_base = get_xdg_base() + xdg_base = get_xdg_base() except ImportError: - default_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER") or \ - "mycroft" - from ovos_utils.xdg_utils import xdg_state_home + xdg_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER") or "mycroft" + + xdg_path = os.path.join(xdg_state_home(), xdg_base) config = config or {} - cls.base_path = config.get("path") or \ - f"{xdg_state_home()}/{default_base}" + cls.base_path = config.get("path") or xdg_path cls.max_bytes = config.get("max_bytes", 50000000) cls.backup_count = config.get("backup_count", 3) cls.level = config.get("level") or LOG.level @@ -229,7 +240,6 @@ def log_deprecation(log_message: str = "DEPRECATED", determination. i.e. an internal exception handling method should log the first call external to that package """ - import inspect stack = inspect.stack()[1:] # [0] is this method call_info = "Unknown Origin" origin_module = func_module @@ -278,3 +288,72 @@ def log_wrapper(*args, **kwargs): return log_wrapper return wrapped + + +def get_log_path(service: str, directories: Optional[List[str]] = None) \ + -> Optional[str]: + """ + Get the path to the log directory for a given service. + Default behaviour is to check the configured paths for the service. + If a list of directories is provided, check that list for the service log + + Args: + service: service name + directories: (optional) list of directories to check for service + + Returns: + path to log directory for service + """ + if directories: + for directory in directories: + file = os.path.join(directory, f"{service}.log") + if os.path.exists(file): + return directory + return None + + from ovos_utils.xdg_utils import xdg_state_home + try: + from ovos_config import Configuration + from ovos_config.meta import get_xdg_base + except ImportError: + xdg_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER", "mycroft") + return os.path.join(xdg_state_home(), xdg_base) + + config = Configuration().get("logging", dict()).get("logs", dict()) + # service specific config or default config location + path = config.get(service, {}).get("path") or config.get("path") + # default xdg location + if not path: + path = os.path.join(xdg_state_home(), get_xdg_base()) + + return path + + +def get_log_paths() -> Set[str]: + """ + Get all log paths for all service logs + Different services may have different log paths + + Returns: + set of paths to log directories + """ + paths = set() + ALL_SERVICES.union({s.replace("-", "_") for s in ALL_SERVICES}) + for service in ALL_SERVICES: + paths.add(get_log_path(service)) + + return paths + + +def get_available_logs(directories: Optional[List[str]] = None) -> List[str]: + """ + Get a list of all available log files + Args: + directories: (optional) list of directories to check for service + + Returns: + list of log files + """ + directories = directories or get_log_paths() + return [Path(f).stem for path in directories + for f in os.listdir(path) if Path(f).suffix == ".log"] diff --git a/ovos_utils/log_parser.py b/ovos_utils/log_parser.py new file mode 100644 index 00000000..c29979db --- /dev/null +++ b/ovos_utils/log_parser.py @@ -0,0 +1,663 @@ +import re +import os +from datetime import datetime +from traceback import FrameSummary +from dataclasses import dataclass +from typing import Any, Tuple, List, Generator, Dict, Union, Optional + +from dateutil.parser import parse +import rich_click as click +from rich.console import Console +from rich.style import Style +from rich.table import Table +import pydoc +from combo_lock import ComboLock + +try: + from ovos_config import Configuration + use24h = Configuration().get("time_format", "full") == "full" + date_format = Configuration().get("date_format", "DMY") +except ImportError: + use24h = True + date_format = "DMY" + +from ovos_utils.log import get_log_path, get_log_paths, get_available_logs + + +TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%f' +LOGLOCK = ComboLock("ovos_logs_console_script") + + +@dataclass +class LogLine: + timestamp: datetime = None + source: str = "" + location: str = "" + level: str = "" + message: str = "" + + def __str__(self): + # sytsem messages etc. + if not all([self.source, self.location, self.level]): + return self.message + return f"{self.format_timestamp()} - {self.source} - {self.location} - {self.level} - {self.message}" + + def format_timestamp(self): + if self.timestamp: + return self.timestamp.strftime(TIME_FORMAT)[:-3] + return "" + + +# Traceback frame +class Frame(FrameSummary): + def __init__(self, filename, lineno, name, line): + super().__init__(filename, lineno, name, line=line) + + def as_dict(self): + return { + "location": self.format_location(), + "level": "TRACEBACK", + "message": self.line + } + + def as_logline(self): + return LogLine(**self.as_dict()) + + def format_location(self): + if "/bin/" in self.filename: + package = self.filename.split("/bin/")[-1].replace(".py", "")\ + .replace("-", "_").replace("/", ".") + elif "site-packages" not in self.filename and \ + (pyver := re.search(r"python\d\.\d+[\\/]", self.filename)): + package = self.filename.split(pyver.group())[-1].replace(".py", "")\ + .replace("-", "_").replace("/", ".") + else: + package = self.filename.split("site-packages/")[-1].replace(".py", "")\ + .replace("-", "_").replace("/", ".") + method = self.name.replace(".py", "").replace("-", "_") + return f"{package}:{method}:{self.lineno}" + + def __str__(self): + return f' File "{self.filename}", line {self.lineno}, in {self.name}\n {self.line}\n' + + +class Traceback: + PATTERN = r'File "(?P[^"]+)", line (?P\d+), in (?P\S+)\n\s*(?P.+)' + + def __init__(self, frames: List[Frame], exception: str, timestamp: datetime = None): + self.frames = frames + self.exception = exception + self._timestamp = timestamp + + @property + def exception_location(self): + return self.frames[-1].format_location() + + @property + def timestamp(self): + return self._timestamp + + @timestamp.setter + def timestamp(self, value): + self._timestamp = value + + def to_loglines(self) -> List[LogLine]: + + lines = [LogLine(timestamp=self.timestamp, + location=self.exception_location, + level="EXCEPTION", + message=self.exception)] + + for frame in self.frames: + lines.append(frame.as_logline()) + + return lines + + @classmethod + def from_list(cls, lines): + lines = [line if line.endswith("\n") else line + "\n" for line in lines] + multiline = "".join(lines) + return cls.from_string(multiline) + + @classmethod + def from_string(cls, s): + matches = re.findall(cls.PATTERN, s, re.MULTILINE) + frames = [] + for match in matches: + data = dict(zip(["filename", "lineno", "name", "line"], match)) + frames.append(Frame(**data)) + exception = next(line for line in s.split("\n")[::-1] if line) + return cls(frames, exception) + + def __str__(self): + multiline = "Traceback (most recent call last):\n" + for frame in self.frames: + multiline += str(frame) + multiline += f"{self.exception}\n" + return multiline + + +class OVOSLogParser: + LOG_PATTERN = r'(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{1,6}) - (?P.+?) - (?P.+?) - (?P\w+) - (?P.*)' + + @classmethod + def parse(self, log_line, last_timestamp=None) -> LogLine: + log_line.rstrip("\n") + match = re.match(self.LOG_PATTERN, log_line) + data = {} + if match: + data = match.groupdict() + data['timestamp'] = datetime.strptime(data['timestamp'], TIME_FORMAT) + return LogLine(**data) + + data["timestamp"] = last_timestamp or "" + data["message"] = log_line + return LogLine(**data) + + @classmethod + def parse_file(self, source) -> Generator[Union[LogLine, Traceback], None, None]: + if not os.path.exists(source): + raise FileNotFoundError(f"File {source} does not exist") + + with open(source, 'r') as file: + trace = None + last_timestamp = None + for line in file: + # gather all lines of the traceback + if line == "Traceback (most recent call last):\n": + trace = [line] + continue + # TODO do tracebacks always end on a empty line? + elif trace and line == "\n": + trace.append(line) + traceback = Traceback.from_list(trace) + traceback.timestamp = last_timestamp + yield traceback + trace = None + elif trace: + trace.append(line) + else: + log = self.parse(line, last_timestamp) + if log.message == "\n": + continue + timestamp = log.timestamp + if timestamp: + last_timestamp = timestamp + yield log + + +console = Console() + +EXPECTED_DATE_FORMAT = "YYYY-MM-DD" if date_format == "YMD" else "DD-MM-YYYY" +EXPECTED_DATE = "2023-12-01" if date_format == "YMD" else "01-12-2023" +EXPECTED_DATETIME_FORMAT = f"[{EXPECTED_DATE_FORMAT}] HH:MM[:SS] {'AM/PM' if not use24h else ''}" +EXPECTED_TIME = f"09:00:05 {'PM' if not use24h else ''}" + +LOGSOPTHELP = """logs to be sliced +\nmultiple: -l bus -l audio""" +STARTTIMEHELP = f"""start time of the log slice (default: since service restart, input format: {EXPECTED_DATETIME_FORMAT.strip()}) +\n Example: -s \"{EXPECTED_DATE} 12:00{' AM/PM' if not use24h else ''}\" / -s 12:00:05{' AM/PM' if not use24h else ''}""" + +click.rich_click.STYLE_ARGUMENT = "dark_red" +click.rich_click.STYLE_OPTION = "dark_red" +click.rich_click.STYLE_SWITCH = "indian_red" +click.rich_click.USE_MARKDOWN = True +click.rich_click.COMMAND_GROUPS = { + "ovos-logs": [ + { + "name": "Slice logs by time", + "commands": ["slice"], + "table_styles": { + "row_styles": ["white"], + "padding": (0, 2), + "title_justify": "left" + }, + }, + { + "name": "List logs by severity", + "commands": ["list"], + "table_styles": { + "row_styles": ["white"], + "padding": (0, 2), + }, + }, + { + "name": "Downsize logs", + "commands": ["reduce"], + "table_styles": { + "row_styles": ["white"], + "padding": (0, 1), + }, + }, + { + "name": "Show logs (using less)", + "commands": ["show"], + "table_styles": { + "row_styles": ["white"], + "padding": (0, 2), + }, + } + ] +} + + +def get_last_load_time(directories: Optional[List[str]] = None) -> Optional[datetime]: + # if nothing's found return the beginning of unix time + last_timestamp = datetime.fromtimestamp(0) + if directories is None: + directory = get_log_path("skills") + else: + directory = get_log_path("skills", directories) + + if directory: + with open(os.path.join(directory,"skills.log"), "r") as f: + for line in f.readlines()[::-1]: + logline = OVOSLogParser.parse(line) + if logline.timestamp: + last_timestamp = logline.timestamp + if logline.message == "Loading message bus configs": + break + return last_timestamp + + +def valid_log(logs, paths): + for log in logs: + if log.lower() not in get_available_logs(paths): + return False + return True + + +def parse_time(time_str): + try: + time = parse(time_str) + except ValueError: + return None + return time + + +def get_timestamped_filename(basename: str, ext: str, basedir = "~", timeformat = '%Y%m%d_%H%M%S'): + if basedir == "~": + basedir = os.path.expanduser("~") + + t = datetime.now().strftime(timeformat) + return os.path.join(basedir, f"{basename}_{t}.{ext}") + + +def parse_timeframe(start, end, directories: Optional[List[str]] = None) -> Tuple[Any, Any]: + """ + Parses the start and end time given a string input. + If the start is None, parse the skill log to determine the last service load time and + if that fails return the beginning starting datetime of the log. + If the end is None, return the current datetime. + + :param start: start time of the log slice (default: since service restart) + :param end: end time of the log slice (default: now) + :param directories: the directory logs reside in + :return: start and end time + """ + if start is None: + start = get_last_load_time(directories) + else: + start = parse_time(start) + + if end is None: + end = datetime.now() + else: + end = parse_time(end) + return start, end + + +@click.group() +def ovos_logs(): + """\b + Small helper tool to quickly navigate the logs, create slices and quickview errors + + `ovos-logs [COMMAND] --help` for further information about the specific command ARGUMENTS + \b + """ + pass + + +@ovos_logs.command() +@click.option("--start", "-s", help=STARTTIMEHELP) +@click.option("--until", "-u", help=f"end time of the log slice [default: now]") +@click.option("--logs", "-l", multiple=True, default=get_available_logs(), help=LOGSOPTHELP, show_default=True) +@click.option("--paths", "-p", multiple=True, default=get_log_paths(), help=f"the directory logs reside in", show_default=True) +@click.option("--file", "-f", is_flag=False, flag_value=get_timestamped_filename("slice", "log"), + default=None, help=f"output as file (if flagged, but not specified: {get_timestamped_filename('slice', 'log')})") +def slice(start, until, logs, paths, file): + """\b + Optionally define start (`-s`) and the time until (`-u`) the slice should be limited to. + \b + Different logs can be included using the `-l` option. If not specified, all logs will be included. + Optionally the directory where the logs are stored (`-p`) and the file where the slices should be dumped (`-f`) + can be specified. + \b + > Examples: + > ovos-logs slice # Slice all logs from service start up until now + > ovos-logs slice -s 01-12-2023 -u 01-12-2023 17:00:20 # Slice all logs from the start of december the first until 17:00:20 + > ovos-logs slice -l bus -l skills -f ~/myslice.log # Slice skills.log and bus.log from service start up until now and dump it to the file ~/myslice.log + """ + logs_present = [] + + if not all(os.path.exists(path) for path in paths): + return console.print(f"Directory [{[p for p in paths if not os.path.exists(p)]}] does not exist") + else: + logs_present = get_available_logs(paths) + + start, end = parse_timeframe(start, until, paths) + if end is None or start is None: + return console.print(f"Need a valid end time in the format ") + elif start > end: + return console.print(f"Start time [{start}] is after end time [{end}]") + + if not logs: + logs = logs_present + elif not valid_log(logs, paths): + return console.print(f"Invalid log name, valid logs are {logs_present}") + + _templog: Dict[str, List[LogLine]] = dict() + + for service in logs: + path = get_log_path(service, paths) + logfile = os.path.join(path, f"{service}.log") + if not os.path.exists(logfile): + continue + _templog[service] = [] + for log in OVOSLogParser.parse_file(logfile): + if start <= log.timestamp < end: + if isinstance(log, Traceback): + _templog[service].extend(log.to_loglines()) + else: + _templog[service].append(log) + if not _templog[service]: + del _templog[service] + + if not _templog: + return console.print("No logs found in the specified time frame") + + if file is not None: + # test if file is writable + try: + with open(file, 'w') as f: + pass + except: + return console.print(f"File [{file}] is not writable. Aborted") + else: + console.print(f"Log slice saved to [bold]{file}[/bold]") + + for service in _templog: + table = Table(title=service) + table.add_column("Time", style="cyan", no_wrap=True) + table.add_column() + table.add_column("Message", style="magenta") + table.add_column("Origin", style="green") + lineno = 0 + for logline in _templog[service]: + lineno += 1 + style = None + timestamp = logline.timestamp or "" + if isinstance(timestamp, datetime): + timestamp = timestamp.strftime("%H:%M:%S.%f" if use24h else "%I:%M:%S.%f")[:-3] + if not use24h: + timestamp += logline.timestamp.strftime(" %p") + + level = logline.level or "" + message = logline.message or "" + if level == "ERROR": + level = "[bold red]" + level[:1] + elif level == "EXCEPTION": + level = "[bold red]" + level[:3] + elif level == "WARNING": + level = "[bold yellow]" + level[:1] + elif level == "DEBUG": + level = "[bold blue]" + level[:1] + elif level == "TRACEBACK": + level = "[white]" + level[:5] + message = "[grey42]" + message + elif level == "INFO": + level = "" + message = "[navajo_white1]" + message + if lineno % 2 == 0: + style = Style(bgcolor="grey7") + table.add_row( + timestamp, + level, + message, + logline.location or "", + style=style + ) + if len(logline.message) > 200: + table.add_row() + + console.print(table) + if file: + Console(file=open(file, 'a')).print(table) + + +@ovos_logs.command() +@click.option("--error", "-e", is_flag=True, help="display error messages") +@click.option("--warning", "-w", is_flag=True, help="display warning messages") +@click.option("--exception", "-x", is_flag=True, help="display exceptions") +@click.option("--debug", "-d", is_flag=True, help="display debug messages") +@click.option("--start", "-s", help=STARTTIMEHELP) +@click.option("--until", "-u", help=f"end time of the log slice [default: now]") +@click.option("--logs", "-l", multiple=True, default=get_available_logs(), help=LOGSOPTHELP, show_default=True) +@click.option("--paths", "-p", multiple=True, type=click.Path(), default=get_log_paths(), help=f"the directory logs reside in", show_default=True) +@click.option("--file", "-f", is_flag=False, type=click.Path(), flag_value=get_timestamped_filename("list", "log"), default=None, + help=f"output as file (if flagged, but not specified: {get_timestamped_filename('list', 'log')})") +def list(error, warning, exception, debug, start, until, logs, paths, file): + """\b + Log level has to be specified. + \b + Optionally define start (`-s`) and the time until (`-u`) the slice should be limited to. + \b + Different logs can be included using the `-l` option. If not specified, all logs will be included. + \b + Optionally the directory where the logs are stored (`-p`) and the file where the slices should be dumped (`-f`) + can be specified. + \b + > Examples: + > ovos-logs list -x # List all exceptions from service start up until now + > ovos-logs list -e -w -s 01-12-2023 -u 01-12-2023 17:00:20 # List all errors and warnings from the start of december the first until 17:00:20 + > ovos-logs list -x -l bus -l skills -f # List all exceptions from skills.log and bus.log and dump it to the file ~/list_xxx_xxx.log + """ + if not any([error, warning, debug, exception]): + return console.print("Need at least one of --error, --warning, --exception or --debug") + else: + log_levels = [lv_str for lv, lv_str in [(error, "ERROR"), (warning, "WARNING"), + (debug, "DEBUG"), (exception, "EXCEPTION")] if lv] + + if not all(os.path.exists(path) for path in paths): + return console.print(f"Directory [{[p for p in paths if not os.path.exists(p)]}] does not exist") + else: + logs_present = get_available_logs(paths) + + start, end = parse_timeframe(start, until, paths) + if end is None or start is None: + return console.print(f"Need a valid end time in the format {EXPECTED_DATETIME_FORMAT}") + elif start > end: + return console.print(f"Start time [{start}] is after end time [{end}]") + + if not logs: + logs = logs_present + elif not valid_log(logs, paths): + return console.print(f"Invalid log name, valid logs are {logs_present}") + + _templog: Dict[str, List[LogLine]] = dict() + + for service in logs: + path = get_log_path(service, paths) + if path is None: + continue + logfile = os.path.join(path, f"{service}.log") + _templog[service] = [] + for log in OVOSLogParser.parse_file(logfile): + if isinstance(log, Traceback): + if exception: + _templog[service].extend(log.to_loglines()) + continue + # LOG.exception + if exception and log.level == "EXCEPTION": + _templog[service].append(log) + if error and log.level == "ERROR": + _templog[service].append(log) + if warning and log.level == "WARNING": + _templog[service].append(log) + if debug and log.level == "DEBUG": + _templog[service].append(log) + if not _templog[service]: + del _templog[service] + + if not _templog: + return console.print("No logs found for the specified log level") + + if file is not None: + # test if file is writable + try: + with open(file, 'w') as f: + pass + except: + return console.print(f"File [{file}] is not writable. Aborted") + else: + console.print(f"Log list saved to [bold]{file}[/bold]") + + for service in _templog: + table = Table(title=f"{service} ({','.join(log_levels)})") + # for traceback indication + table.add_column("Time", style="cyan", no_wrap=True) + if exception or len(log_levels) > 1: + table.add_column() + table.add_column("Message", style="magenta") + table.add_column("Origin", style="green") + lineno = 0 + for log in _templog[service]: + style = None + lineno += 1 + timestamp = log.timestamp or "" + if timestamp: + timestamp = timestamp.strftime("%H:%M:%S.%f" if use24h else "%I:%M:%S.%f")[:-3] + if not use24h and timestamp: + timestamp += log.timestamp.strftime(" %p") + level = log.level.upper() + message = log.message.rstrip("\n") + if level == "ERROR": + level = "[bold red]" + level[:1] + elif level == "EXCEPTION": + level = "[bold red]" + level[:3] + elif level == "WARNING": + level = "[bold yellow]" + level[:1] + elif level == "DEBUG": + level = "[bold blue]" + level[:1] + elif level == "TRACEBACK": + level = "[white]" + level[:5] + message = "[grey42]" + message + elif level == "INFO": + level = "" + message = "[navajo_white1]" + message + + if lineno % 2 == 0: + style = Style(bgcolor="grey7") + row = [timestamp, level, message, log.location] + if not exception and len(log_levels) < 2: + row.pop(1) + table.add_row(*row, style=style) + if len(log.message) > 200: + table.add_row() + + console.print(table) + if file: + Console(file=open(file, 'a')).print(table) + + +@ovos_logs.command() +@click.option("--log", "-l", required=True, type=click.Choice(get_available_logs(), case_sensitive=False), help=f"log to show; available: {get_available_logs()}") +@click.option("--paths", "-p", multiple=True, type=click.Path(), default=get_log_paths(), help=f"the directory logs reside in", show_default=True) +def show(log, paths): + """\b + A service log has to be specified (`-l`). + \b + Optionally the directory where the logs are stored (`-p`) can be specified. + \b + > Examples: + > ovos-logs show -l skills # Display skills.log + > ovos-logs show -l debug -p ~/custom_path/ # Display debug.log from a custom path + """ + if not any(os.path.exists(os.path.join(path, f"{log}.log")) for path in paths): + return console.print(f"File does not exist") + else: + log = os.path.join(get_log_path(log, paths), f"{log}.log") + + pydoc.pager(open(log).read()) + + +@ovos_logs.command() +@click.option("--size", "-s", is_flag=False, flag_value=None, default=0, help="truncate logs to a given size (in bytes)") +@click.option("--date", "-d", help="truncate logs to a given date") +@click.option("--logs", "-l", multiple=True, default=get_available_logs(), help=LOGSOPTHELP, show_default=True) +@click.option("--paths", "-p", multiple=True, type=click.Path(), default=get_log_paths(), help=f"the directory logs reside in", show_default=True) +def reduce(size, date, logs, paths): + """\b + Reduce logs to a given size (in bytes) or remove entries before a given date. + \b + Different logs can be included using the `-l` option. If not specified, all logs will be included. + Optionally the directory where the logs are stored (`-p`) can be specified. + \b + > Examples: + > ovos-logs reduce # Reduce all logs to 0 bytes + > ovos-logs reduce -s 1000000 # Reduce all logs to ~1MB (latest logs) + > ovos-logs reduce -d "1-12-2023 17:00" # Reduce all logs to entries after the specified date/time + > ovos-logs reduce -s 1000000 -l skills -l bus # Reduce skills.log and bus.log to ~1MB (latest logs) + """ + + if date: + size = None + date = parse_time(date) + if date is None: + return console.print(f"The date/time provided couldn't be parsed. Expected format: {EXPECTED_DATETIME_FORMAT}") + + if not all(os.path.exists(path) for path in paths): + return console.print(f"Directory [{[p for p in paths if not os.path.exists(p)]}] does not exist") + else: + logs_present = get_available_logs(paths) + + if not logs: + logs = logs_present + elif not valid_log(logs, paths): + return console.print(f"Invalid log name, valid logs are {logs_present}") + + for service in logs: + path = get_log_path(service, paths) + logfile = os.path.join(path, f"{service}.log") + reduced = False + with LOGLOCK: + if size: + with open(logfile, 'r') as f: + f.seek(0, os.SEEK_END) + fullsize = f.tell() + f.seek(max(fullsize - size, 0)) + # skip cutoff line + f.readline() + remaining_lines = f.readlines() + if fullsize > size and remaining_lines: + reduced = True + with open(logfile, 'w') as f: + f.writelines(remaining_lines) + elif date: + loglines = [] + for log in OVOSLogParser.parse_file(logfile): + if log.timestamp and log.timestamp < date: + reduced = True + continue + loglines.append(log) + if reduced: + with open(logfile, 'w') as f: + for log in loglines: + f.write(str(log) + "\n") + else: + reduced = True + with open(logfile, 'w') as f: + f.write("") + + if reduced: + console.print(f"{service} log reduced") diff --git a/readme.md b/readme.md index fe8103d7..7dd896e6 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# OVOS - utils +# OVOS-utils collection of simple utilities for use across the mycroft ecosystem @@ -8,3 +8,80 @@ collection of simple utilities for use across the mycroft ecosystem pip install ovos_utils ``` +## Commandline scripts +### ovos-logs + Small helper tool to quickly navigate the logs, create slices and quickview errors + +--------------- +- **ovos-logs slice [options]** + + **Slice logs of a given time period. Defaults on the last service start (`-s`) until now (`-u`)** + + _Different logs can be picked using the `-l` option. All logs will be included if not specified._ + _Optionally the directory where the logs are stored (`-p`) and the file where the slices should be dumped (`-f`) can be specified._ + + + _[ex: `ovos-logs slice`]_ + _Slice all logs from service start up until now._ + + _[ex: `ovos-logs slice -s 17:05:20 -u 17:05:25`]_ + _Slice all logs from 17:05:20 until 17:05:25._ + _**no logs in that timeframe in other present logs_ + Screenshot 2023-12-25 185004 + + _[ex: `ovos-logs slice -s 17:05:20 -u 17:05:25 -l skills`]_ + _Slice skills.log from 17:05:20 until 17:05:25._ + + _[ex: `ovos-logs slice -s 17:05:20 -u 17:05:25 -f ~/testslice.log`]_ + _Slice the logs from 17:05:20 until 17:05:25 on all log files and dump the slices in the file ~/testslice.log (default: `~/slice_.log`)._ + Screenshot 2023-12-25 190732 +-------------- + +- **ovos-logs list [-e|-w|-d|-x] [options]** + + **List logs by severity (error/warning/debug/exception). A log level has to be specified - more than one can be listed** + + _A start and end date can be specified using the `-s` and `-u` options. Defaults to the last service start until now._ + _Different logs can be picked using the `-l` option. All logs will be included if not specified._ + _Optionally, the directory where the logs are stored (`-p`) and the file where the slices should be dumped (`-f`) can be passed as arguments._ + + _[ex: `ovos-logs list -x`]_ + _List the logs with level EXCEPTION (plus tracebacks) from the last service start until now._ + Screenshot 2023-12-25 184321 + + _[ex: `ovos-logs list -w -e -s 20-12-2023 -l bus -l skills`]_ + _List the logs with level WARNING and ERROR from the 20th of December 2023 until now from the logs bus.log and skills.log._ + Screenshot 2023-12-25 173739 +--------------------- + +- **ovos-logs reduce [options]** + + **Downsize logs to a given size (in bytes) or remove entries before a given date.** + + _Different logs can be included using the `-l` option. If not specified, all logs will be included._ + _Optionally the directory where the logs are stored (`-p`) can be specified._ + + _[ex: `ovos-logs reduce`]_ + _Downsize all logs to 0 bytes_ + + _[ex: `ovos-logs reduce -s 1000000`]_ + _Downsize all logs to ~1MB (latest logs)_ + + _[ex: `ovos-logs reduce -d "1-12-2023 17:00"`]_ + _Downsize all logs to entries after the specified date/time_ + + _[ex: `ovos-logs reduce -s 1000000 -l skills -l bus`]_ + _Downsize skills.log and bus.log to ~1MB (latest logs)_ + +--------------------- + +- **ovos-logs show -l [servicelog]** + + **Show logs** + + _[ex: `ovos-logs show -l bus`]_ + _Show the logs from bus.log._ + + _[ex: wrong servicelog]_ + _**logs shown depending on the logs present in the folder_ + diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0c05ebaf..95f6e3e5 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,3 +4,6 @@ json_database~=0.7 kthread~=0.2 watchdog pyee +combo-lock~=0.2 +rich-click~=1.7 +rich~=13.7 diff --git a/setup.py b/setup.py index 4b2cbaec..97e29527 100644 --- a/setup.py +++ b/setup.py @@ -67,5 +67,10 @@ def required(requirements_file): license='Apache', author='jarbasAI', author_email='jarbasai@mailfence.com', - description='collection of simple utilities for use across the mycroft ecosystem' + description='collection of simple utilities for use across the mycroft ecosystem', + entry_points={ + 'console_scripts': [ + 'ovos-logs=ovos_utils.log_parser:ovos_logs' + ] + } ) From 917c74c1d0dd7d963463a741980e824a13f4ccaf Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 30 Dec 2023 18:23:37 +0000 Subject: [PATCH 08/87] Increment Version to 0.1.0a6 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index a1eca47f..99a5be17 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 5 +VERSION_ALPHA = 6 # END_VERSION_BLOCK From 6936b3ab8ae6e36665a5e98ba48342ab6bef626e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 30 Dec 2023 18:24:10 +0000 Subject: [PATCH 09/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8ef9fe..91ca9157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a5](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a5) (2023-12-30) +## [0.1.0a6](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a6) (2023-12-30) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a4...0.1.0a5) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a5...0.1.0a6) + +**Implemented enhancements:** + +- Feat/ovos logs script [\#203](https://github.com/OpenVoiceOS/ovos-utils/pull/203) ([emphasize](https://github.com/emphasize)) + +## [V0.1.0a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a5) (2023-12-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a4...V0.1.0a5) **Merged pull requests:** From 26dfca62491f6275281de44f33f01f9fe0260126 Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Sat, 6 Jan 2024 01:00:05 +0000 Subject: [PATCH 10/87] refactor/ocp_utils (#215) shared constants moved to the common module these enums are used in several places andhaving them elsewhere introduces circular dependencies Co-authored-by: JarbasAi --- ovos_utils/ocp.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 ovos_utils/ocp.py diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py new file mode 100644 index 00000000..861d285d --- /dev/null +++ b/ovos_utils/ocp.py @@ -0,0 +1,124 @@ +from enum import IntEnum + +OCP_ID = "ovos.common_play" + + +class MatchConfidence(IntEnum): + EXACT = 95 + VERY_HIGH = 90 + HIGH = 80 + AVERAGE_HIGH = 70 + AVERAGE = 50 + AVERAGE_LOW = 30 + LOW = 15 + VERY_LOW = 1 + + +class TrackState(IntEnum): + DISAMBIGUATION = 1 # media result, not queued for playback + + PLAYING_SKILL = 20 # Skill is handling playback internally + PLAYING_AUDIO = 21 # Skill forwarded playback to audio service + PLAYING_VIDEO = 22 # Skill forwarded playback to video service + PLAYING_MPRIS = 24 # External media player is handling playback + PLAYING_WEBVIEW = 25 # Media playback handled in browser (eg. javascript) + + QUEUED_SKILL = 30 # Waiting playback to be handled inside skill + QUEUED_AUDIO = 31 # Waiting playback in audio service + QUEUED_VIDEO = 32 # Waiting playback in video service + QUEUED_WEBVIEW = 34 # Waiting playback in browser service + + +class MediaState(IntEnum): + # https://doc.qt.io/qt-5/qmediaplayer.html#MediaStatus-enum + # The status of the media cannot be determined. + UNKNOWN = 0 + # There is no current media. PlayerState == STOPPED + NO_MEDIA = 1 + # The current media is being loaded. The player may be in any state. + LOADING_MEDIA = 2 + # The current media has been loaded. PlayerState== STOPPED + LOADED_MEDIA = 3 + # Playback of the current media has stalled due to + # insufficient buffering or some other temporary interruption. + # PlayerState != STOPPED + STALLED_MEDIA = 4 + # The player is buffering data but has enough data buffered + # for playback to continue for the immediate future. + # PlayerState != STOPPED + BUFFERING_MEDIA = 5 + # The player has fully buffered the current media. PlayerState != STOPPED + BUFFERED_MEDIA = 6 + # Playback has reached the end of the current media. PlayerState == STOPPED + END_OF_MEDIA = 7 + # The current media cannot be played. PlayerState == STOPPED + INVALID_MEDIA = 8 + + +class PlayerState(IntEnum): + # https://doc.qt.io/qt-5/qmediaplayer.html#State-enum + STOPPED = 0 + PLAYING = 1 + PAUSED = 2 + + +class LoopState(IntEnum): + NONE = 0 + REPEAT = 1 + REPEAT_TRACK = 2 + + +class PlaybackType(IntEnum): + SKILL = 0 # skills handle playback whatever way they see fit, + # eg spotify / mycroft common play + VIDEO = 1 # Video results + AUDIO = 2 # Results should be played audio only + MPRIS = 4 # External MPRIS compliant player + WEBVIEW = 5 # webview, render a url instead of media player + UNDEFINED = 100 # data not available, hopefully status will be updated soon.. + + +class PlaybackMode(IntEnum): + AUTO = 0 # play each entry as considered appropriate, + # ie, make it happen the best way possible + AUDIO_ONLY = 10 # only consider audio entries + VIDEO_ONLY = 20 # only consider video entries + FORCE_AUDIO = 30 # cast video to audio unconditionally + EVENTS_ONLY = 50 # only emit ocp events, do not display or play anything. + # allows integration with external interfaces + + +class MediaType(IntEnum): + GENERIC = 0 # nothing else matches + AUDIO = 1 # things like ambient noises + MUSIC = 2 + VIDEO = 3 # eg, youtube videos + AUDIOBOOK = 4 + GAME = 5 # because it shares the verb "play", mostly for disambguation + PODCAST = 6 + RADIO = 7 # live radio + NEWS = 8 # news reports + TV = 9 # live tv stream + MOVIE = 10 + TRAILER = 11 + AUDIO_DESCRIPTION = 12 # narrated movie for the blind + VISUAL_STORY = 13 # things like animated comic books + BEHIND_THE_SCENES = 14 + DOCUMENTARY = 15 + RADIO_THEATRE = 16 + SHORT_FILM = 17 # typically movies under 45 min + SILENT_MOVIE = 18 + VIDEO_EPISODES = 19 # tv series etc + BLACK_WHITE_MOVIE = 20 + CARTOON = 21 + ANIME = 22 + ASMR = 23 + + ADULT = 69 # for content filtering + HENTAI = 70 # for content filtering + ADULT_AUDIO = 71 # for content filtering + + +def available_extractors(): + from ovos_plugin_common_play.ocp.utils import available_extractors as _real + return _real() # TODO From 9e66c736790caa94d5051913a27c729c4cf82c74 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 6 Jan 2024 01:00:20 +0000 Subject: [PATCH 11/87] Increment Version to 0.1.0a7 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 99a5be17..95691547 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 6 +VERSION_ALPHA = 7 # END_VERSION_BLOCK From 731d8088ff4b6ced155d0817ff28e16235d43c72 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 6 Jan 2024 01:01:02 +0000 Subject: [PATCH 12/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ca9157..1006a2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a6](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a6) (2023-12-30) +## [0.1.0a7](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a7) (2024-01-06) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a5...0.1.0a6) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a6...0.1.0a7) + +**Merged pull requests:** + +- refactor/ocp\_utils [\#215](https://github.com/OpenVoiceOS/ovos-utils/pull/215) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.1.0a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a6) (2023-12-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a5...V0.1.0a6) **Implemented enhancements:** From 220857b5ab6326d1b87d39b01565ecfbbe91f1ab Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Mon, 8 Jan 2024 05:59:57 +0000 Subject: [PATCH 13/87] refactor/ocp_utils (#216) * refactor/ocp_models move the MediaType and Playlist model to utils so they can be reused across ovos modules skills will then be able to return these objects directly in the ocp decorators * refactor/ovos_media * method from OPM * update import * update models * update models --------- Co-authored-by: JarbasAi --- ovos_utils/ocp.py | 352 +++++++++++++++++++++++++++++++++- requirements/requirements.txt | 1 + 2 files changed, 351 insertions(+), 2 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 861d285d..4d1787ee 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -1,4 +1,11 @@ +import mimetypes +from dataclasses import dataclass from enum import IntEnum +from typing import Optional, Tuple, List, Union + +import orjson + +from ovos_utils.log import LOG, deprecated OCP_ID = "ovos.common_play" @@ -119,6 +126,347 @@ class MediaType(IntEnum): ADULT_AUDIO = 71 # for content filtering +@deprecated("import ovos_utils.available_extractors from ovos_plugin_manager.ocp instead", "0.1.0") def available_extractors(): - from ovos_plugin_common_play.ocp.utils import available_extractors as _real - return _real() # TODO + # TODO - delete me, still imported in ovos-bus-client, but never made it into a non-alpha + try: + from ovos_plugin_manager.ocp import available_extractors as _ax + except ImportError: + try: + # before move to OPM + from ovos_plugin_common_play.ocp.utils import available_extractors as _ax + except ImportError: + LOG.error("please install/update ovos_plugin_manager") + raise + return _ax() + + +def find_mime(uri): + """ Determine mime type. """ + mime = mimetypes.guess_type(uri) + if mime: + return mime + else: + return None + + +@dataclass +class MediaEntry: + uri: str = "" + title: str = "" + artist: str = "" + match_confidence: int = 0 # 0 - 100 + skill_id: str = OCP_ID + playback: PlaybackType = PlaybackType.UNDEFINED + status: TrackState = TrackState.DISAMBIGUATION + media_type: MediaType = MediaType.GENERIC + length: int = 0 # in seconds + image: str = "" + skill_icon: str = "" + javascript: str = "" # to execute once webview is loaded + + def update(self, entry: dict, skipkeys: list = None, newonly: bool = False): + """ + Update this MediaEntry object with keys from the provided entry + @param entry: dict or MediaEntry object to update this object with + @param skipkeys: list of keys to not change + @param newonly: if True, only adds new keys; existing keys are unchanged + """ + skipkeys = skipkeys or [] + if isinstance(entry, MediaEntry): + entry = entry.as_dict + entry = entry or {} + for k, v in entry.items(): + if k not in skipkeys and hasattr(self, k): + if newonly and self.__getattribute__(k): + # skip, do not replace existing values + continue + self.__setattr__(k, v) + + @property + def infocard(self) -> dict: + """ + Return dict data used for a UI display + """ + return { + "duration": self.length, + "track": self.title, + "image": self.image, + "album": self.skill_id, + "source": self.skill_icon, + "uri": self.uri + } + + @property + def mpris_metadata(self) -> dict: + """ + Return dict data used by MPRIS + """ + from dbus_next.service import Variant + meta = {"xesam:url": Variant('s', self.uri)} + if self.artist: + meta['xesam:artist'] = Variant('as', [self.artist]) + if self.title: + meta['xesam:title'] = Variant('s', self.title) + if self.image: + meta['mpris:artUrl'] = Variant('s', self.image) + if self.length: + meta['mpris:length'] = Variant('d', self.length) + return meta + + @property + def as_dict(self) -> dict: + """ + Return a dict representation of this MediaEntry + """ + # orjson handles dataclasses directly + return orjson.loads(orjson.dumps(self).decode("utf-8")) + + @property + def mimetype(self) -> Optional[Tuple[Optional[str], Optional[str]]]: + """ + Get the detected mimetype tuple (type, encoding) if it can be determined + """ + if self.uri: + return find_mime(self.uri) + + def __eq__(self, other): + if isinstance(other, MediaEntry): + other = other.infocard + # dict comparison + return other == self.infocard + + +@dataclass +class Playlist(list): + title: str = "" + position: int = 0 + length: int = 0 # in seconds + image: str = "" + match_confidence: int = 0 # 0 - 100 + skill_id: str = OCP_ID + skill_icon: str = "" + + @property + def infocard(self) -> dict: + """ + Return dict data used for a UI display + (model shared with MediaEntry) + """ + return { + "duration": self.length, + "track": self.title, + "image": self.image, + "album": self.skill_id, + "source": self.skill_icon, + "uri": "" + } + + @property + def as_dict(self) -> dict: + """ + Return a dict representation of this MediaEntry + """ + # orjson handles dataclasses directly + return orjson.loads(orjson.dumps(self).decode("utf-8")) + + @property + def entries(self) -> List[MediaEntry]: + """ + Return a list of MediaEntry objects in the playlist + """ + entries = [] + for e in self: + if isinstance(e, dict): + e = MediaEntry(**e) + if isinstance(e, MediaEntry): + entries.append(e) + return entries + + @property + def current_track(self) -> Optional[MediaEntry]: + """ + Return the current MediaEntry or None if the playlist is empty + """ + if len(self) == 0: + return None + self._validate_position() + track = self[self.position] + if isinstance(track, dict): + track = MediaEntry(**track) + return track + + @property + def is_first_track(self) -> bool: + """ + Return `True` if the current position is the first track or if the + playlist is empty + """ + if len(self) == 0: + return True + return self.position == 0 + + @property + def is_last_track(self) -> bool: + """ + Return `True` if the current position is the last track of if the + playlist is empty + """ + if len(self) == 0: + return True + return self.position == len(self) - 1 + + def goto_start(self) -> None: + """ + Move to the first entry in the playlist + """ + self.position = 0 + + def clear(self) -> None: + """ + Remove all entries from the Playlist and reset the position + """ + super().clear() + self.position = 0 + + def sort_by_conf(self): + """ + Sort the Playlist by `match_confidence` with high confidence first + """ + self.sort( + key=lambda k: k.match_confidence if isinstance(k, MediaEntry) + else k.get("match_confidence", 0), reverse=True) + + def add_entry(self, entry: MediaEntry, index: int = -1) -> None: + """ + Add an entry at the requested index + @param entry: MediaEntry to add to playlist + @param index: index to insert entry at (default -1 to append) + """ + assert isinstance(index, int) + # TODO: Handle index out of range + if isinstance(entry, dict): + entry = MediaEntry(**entry) + assert isinstance(entry, MediaEntry) + if index == -1: + index = len(self) + + if index < self.position: + self.set_position(self.position + 1) + + self.insert(index, entry) + + def remove_entry(self, entry: Union[int, dict, MediaEntry]) -> None: + """ + Remove the requested entry from the playlist or raise a ValueError + @param entry: index or MediaEntry to remove from the playlist + """ + if isinstance(entry, int): + self.pop(entry) + return + if isinstance(entry, dict): + entry = MediaEntry(**entry) + assert isinstance(entry, MediaEntry) + for idx, e in enumerate(self.entries): + if e == entry: + self.pop(idx) + break + else: + raise ValueError(f"entry not in playlist: {entry}") + + def replace(self, new_list: List[Union[dict, MediaEntry]]) -> None: + """ + Replace the contents of this Playlist with new_list + @param new_list: list of MediaEntry or dict objects to set this list to + """ + self.clear() + for e in new_list: + self.add_entry(e) + + def set_position(self, idx: int): + """ + Set the position in the playlist to a specific index + @param idx: Index to set position to + """ + self.position = idx + self._validate_position() + + def goto_track(self, track: Union[MediaEntry, dict]) -> None: + """ + Go to the requested track in the playlist + @param track: MediaEntry to find and go to in the playlist + """ + if isinstance(track, MediaEntry): + requested_uri = track.uri + else: + requested_uri = track.get("uri", "") + for idx, t in enumerate(self): + if isinstance(t, MediaEntry): + pl_entry_uri = t.uri + else: + pl_entry_uri = t.get("uri", "") + if requested_uri == pl_entry_uri: + self.set_position(idx) + LOG.debug(f"New playlist position: {self.position}") + return + LOG.error(f"requested track not in the playlist: {track}") + + def next_track(self) -> None: + """ + Go to the next track in the playlist + """ + self.set_position(self.position + 1) + + def prev_track(self) -> None: + """ + Go to the previous track in the playlist + """ + self.set_position(self.position - 1) + + def _validate_position(self) -> None: + """ + Make sure the current position is valid; default `position` to 0 + """ + if self.position < 0 or self.position >= len(self): + LOG.error(f"Playlist pointer is in an invalid position " + f"({self.position}! Going to start of playlist") + self.position = 0 + + def __contains__(self, item): + if isinstance(item, dict): + item = MediaEntry(**item) + if isinstance(item, MediaEntry): + for e in self.entries: + if e.uri == item.uri: + return True + return False + + +if __name__ == "__main__": + m = MediaEntry("1") + m2 = MediaEntry("2") + m3 = MediaEntry("3") + p = Playlist("My Jams") # it's a list subclass + print(p) # Playlist(title='My Jams', position=0, length=0, image='', match_confidence=0, skill_id='ovos.common_play', skill_icon='') + p.append(m) + p += [m2] + p.add_entry(m3, 0) + assert p[0] is m3 + assert p[1] is m + assert m in p + assert m2 in p + assert m3 in p + print(p.entries) + np = [m3, m, m2] + assert p == np + assert np == p + assert p == p.entries + print(p.position) + p.goto_track(m2) + print(p.position) + p.goto_start() + print(p.position) + p.set_position(1) + print(p) # Playlist(title='My Jams', position=1, length=0, image='', match_confidence=0, skill_id='ovos.common_play', skill_icon='') + + diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 95f6e3e5..8fafc368 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,3 +7,4 @@ pyee combo-lock~=0.2 rich-click~=1.7 rich~=13.7 +orjson \ No newline at end of file From 5163faf4ffadabc29f2599e80fccf84cacc96825 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 8 Jan 2024 06:00:11 +0000 Subject: [PATCH 14/87] Increment Version to 0.1.0a8 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 95691547..a282e481 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 7 +VERSION_ALPHA = 8 # END_VERSION_BLOCK From 17d510a386769fb93ffe3158d23849647f27967e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 8 Jan 2024 06:00:45 +0000 Subject: [PATCH 15/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1006a2d4..ae84b596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a7](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a7) (2024-01-06) +## [0.1.0a8](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a8) (2024-01-08) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a6...0.1.0a7) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a7...0.1.0a8) + +**Implemented enhancements:** + +- refactor/ocp\_utils [\#216](https://github.com/OpenVoiceOS/ovos-utils/pull/216) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.1.0a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a7) (2024-01-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a6...V0.1.0a7) **Merged pull requests:** From d4d38edf676e496576d19a1fb7e18ee1954ea5e4 Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Fri, 12 Jan 2024 00:53:11 +0000 Subject: [PATCH 16/87] OCP serialization (#218) * OCP serialization improves the serialization and deserialization of the OCP objects makes them compatible with the dicts currently returned by OCP skills, paving the way to allow those skills to return these objects directly in a future workshop release * Update ocp.py --------- Co-authored-by: JarbasAI <33701864+JarbasAl@users.noreply.github.com> --- ovos_utils/ocp.py | 65 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 4d1787ee..bf4d7aed 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from enum import IntEnum from typing import Optional, Tuple, List, Union - +import inspect import orjson from ovos_utils.log import LOG, deprecated @@ -222,6 +222,20 @@ def as_dict(self) -> dict: # orjson handles dataclasses directly return orjson.loads(orjson.dumps(self).decode("utf-8")) + @staticmethod + def from_dict(track: dict): + if track.get("playlist"): + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(Playlist).parameters} + playlist = Playlist(**kwargs) + for e in track["playlist"]: + playlist.add_entry(e) + return playlist + else: + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(MediaEntry).parameters} + return MediaEntry(**kwargs) + @property def mimetype(self) -> Optional[Tuple[Optional[str], Optional[str]]]: """ @@ -262,13 +276,26 @@ def infocard(self) -> dict: "uri": "" } + @staticmethod + def from_dict(track: dict): + return MediaEntry.from_dict(track) + @property def as_dict(self) -> dict: """ Return a dict representation of this MediaEntry """ - # orjson handles dataclasses directly - return orjson.loads(orjson.dumps(self).decode("utf-8")) + data = { + "title": self.title, + "position": self.position, + "length": self.length, + "image": self.image, + "match_confidence": self.match_confidence, + "skill_id": self.skill_id, + "skill_icon": self.skill_icon, + "playlist": [e.as_dict for e in self.entries] + } + return data @property def entries(self) -> List[MediaEntry]: @@ -278,7 +305,7 @@ def entries(self) -> List[MediaEntry]: entries = [] for e in self: if isinstance(e, dict): - e = MediaEntry(**e) + e = MediaEntry.from_dict(e) if isinstance(e, MediaEntry): entries.append(e) return entries @@ -293,7 +320,7 @@ def current_track(self) -> Optional[MediaEntry]: self._validate_position() track = self[self.position] if isinstance(track, dict): - track = MediaEntry(**track) + track = MediaEntry.from_dict(track) return track @property @@ -334,7 +361,7 @@ def sort_by_conf(self): Sort the Playlist by `match_confidence` with high confidence first """ self.sort( - key=lambda k: k.match_confidence if isinstance(k, MediaEntry) + key=lambda k: k.match_confidence if isinstance(k, (MediaEntry, Playlist)) else k.get("match_confidence", 0), reverse=True) def add_entry(self, entry: MediaEntry, index: int = -1) -> None: @@ -344,10 +371,15 @@ def add_entry(self, entry: MediaEntry, index: int = -1) -> None: @param index: index to insert entry at (default -1 to append) """ assert isinstance(index, int) - # TODO: Handle index out of range + if index > len(self): + raise ValueError(f"Invalid index {index} requested, " + f"playlist only has {len(self)} entries") + if isinstance(entry, dict): - entry = MediaEntry(**entry) - assert isinstance(entry, MediaEntry) + entry = MediaEntry.from_dict(entry) + + assert isinstance(entry, (MediaEntry, Playlist)) + if index == -1: index = len(self) @@ -365,7 +397,7 @@ def remove_entry(self, entry: Union[int, dict, MediaEntry]) -> None: self.pop(entry) return if isinstance(entry, dict): - entry = MediaEntry(**entry) + entry = MediaEntry.from_dict(entry) assert isinstance(entry, MediaEntry) for idx, e in enumerate(self.entries): if e == entry: @@ -396,15 +428,22 @@ def goto_track(self, track: Union[MediaEntry, dict]) -> None: Go to the requested track in the playlist @param track: MediaEntry to find and go to in the playlist """ + if isinstance(track, dict): + track = MediaEntry.from_dict(track) + + assert isinstance(track, (MediaEntry, Playlist)) + if isinstance(track, MediaEntry): requested_uri = track.uri else: - requested_uri = track.get("uri", "") + requested_uri = track.title + for idx, t in enumerate(self): if isinstance(t, MediaEntry): pl_entry_uri = t.uri else: - pl_entry_uri = t.get("uri", "") + pl_entry_uri = t.title + if requested_uri == pl_entry_uri: self.set_position(idx) LOG.debug(f"New playlist position: {self.position}") @@ -434,7 +473,7 @@ def _validate_position(self) -> None: def __contains__(self, item): if isinstance(item, dict): - item = MediaEntry(**item) + item = MediaEntry.from_dict(item) if isinstance(item, MediaEntry): for e in self.entries: if e.uri == item.uri: From 13f27678e8938f34ef53fea0d0a8d2d09e447d0f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 12 Jan 2024 00:53:28 +0000 Subject: [PATCH 17/87] Increment Version to 0.1.0a9 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index a282e481..8d15ff0c 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 8 +VERSION_ALPHA = 9 # END_VERSION_BLOCK From cfa1b9a170f7763dba0da9376f5928e060932a6a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 12 Jan 2024 00:53:57 +0000 Subject: [PATCH 18/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae84b596..ae755d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a8](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a8) (2024-01-08) +## [0.1.0a9](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a9) (2024-01-12) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a7...0.1.0a8) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a8...0.1.0a9) + +**Implemented enhancements:** + +- OCP serialization [\#218](https://github.com/OpenVoiceOS/ovos-utils/pull/218) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.1.0a8](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a8) (2024-01-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a7...V0.1.0a8) **Implemented enhancements:** From 95ec8f73628d694573db231c69e8796b981ec10b Mon Sep 17 00:00:00 2001 From: Swen Gross <25036977+emphasize@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:37:08 +0100 Subject: [PATCH 19/87] clarify datetime arg (#219) --- ovos_utils/log_parser.py | 42 +++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/ovos_utils/log_parser.py b/ovos_utils/log_parser.py index c29979db..f7de2f5d 100644 --- a/ovos_utils/log_parser.py +++ b/ovos_utils/log_parser.py @@ -190,13 +190,15 @@ def parse_file(self, source) -> Generator[Union[LogLine, Traceback], None, None] EXPECTED_DATE_FORMAT = "YYYY-MM-DD" if date_format == "YMD" else "DD-MM-YYYY" EXPECTED_DATE = "2023-12-01" if date_format == "YMD" else "01-12-2023" -EXPECTED_DATETIME_FORMAT = f"[{EXPECTED_DATE_FORMAT}] HH:MM[:SS] {'AM/PM' if not use24h else ''}" -EXPECTED_TIME = f"09:00:05 {'PM' if not use24h else ''}" +EXPECTED_DATETIME_FORMAT = f'"[{EXPECTED_DATE_FORMAT}] HH:MM[:SS]{" AM/PM" if not use24h else ""}"' +EXPECTED_TIME = f'"09:00:05{" PM" if not use24h else ""}"' LOGSOPTHELP = """logs to be sliced -\nmultiple: -l bus -l audio""" -STARTTIMEHELP = f"""start time of the log slice (default: since service restart, input format: {EXPECTED_DATETIME_FORMAT.strip()}) -\n Example: -s \"{EXPECTED_DATE} 12:00{' AM/PM' if not use24h else ''}\" / -s 12:00:05{' AM/PM' if not use24h else ''}""" +\n\nmultiple: -l bus -l audio""" +STARTTIMEHELP = f"""start time of the log slice (default: since service restart, +input format: {EXPECTED_DATETIME_FORMAT}) +\n\nExample: -s \"{EXPECTED_DATE} 12:00{' AM/PM' if not use24h else ''}\" / -s + {'"' if not use24h else ''}12:00:05{' AM/PM"' if not use24h else ''}""" click.rich_click.STYLE_ARGUMENT = "dark_red" click.rich_click.STYLE_OPTION = "dark_red" @@ -335,7 +337,7 @@ def slice(start, until, logs, paths, file): \b > Examples: > ovos-logs slice # Slice all logs from service start up until now - > ovos-logs slice -s 01-12-2023 -u 01-12-2023 17:00:20 # Slice all logs from the start of december the first until 17:00:20 + > ovos-logs slice -s 01-12-2023 -u '01-12-2023 17:00:20' # Slice all logs from the start of december the first until 17:00:20 > ovos-logs slice -l bus -l skills -f ~/myslice.log # Slice skills.log and bus.log from service start up until now and dump it to the file ~/myslice.log """ logs_present = [] @@ -346,8 +348,10 @@ def slice(start, until, logs, paths, file): logs_present = get_available_logs(paths) start, end = parse_timeframe(start, until, paths) - if end is None or start is None: - return console.print(f"Need a valid end time in the format ") + if start is None: + return console.print(f"Need a valid start time in the format {EXPECTED_DATETIME_FORMAT}") + elif end is None: + return console.print(f"Need a valid end time in the format {EXPECTED_DATETIME_FORMAT}") elif start > end: return console.print(f"Start time [{start}] is after end time [{end}]") @@ -458,9 +462,9 @@ def list(error, warning, exception, debug, start, until, logs, paths, file): can be specified. \b > Examples: - > ovos-logs list -x # List all exceptions from service start up until now - > ovos-logs list -e -w -s 01-12-2023 -u 01-12-2023 17:00:20 # List all errors and warnings from the start of december the first until 17:00:20 - > ovos-logs list -x -l bus -l skills -f # List all exceptions from skills.log and bus.log and dump it to the file ~/list_xxx_xxx.log + > ovos-logs list -x # List all exceptions from service start up until now + > ovos-logs list -e -w -s 01-12-2023 -u '01-12-2023 17:00:20' # List all errors and warnings from the start of december the first until 17:00:20 + > ovos-logs list -x -l bus -l skills -f # List all exceptions from skills.log and bus.log and dump it to the file ~/list_xxx_xxx.log """ if not any([error, warning, debug, exception]): return console.print("Need at least one of --error, --warning, --exception or --debug") @@ -474,7 +478,9 @@ def list(error, warning, exception, debug, start, until, logs, paths, file): logs_present = get_available_logs(paths) start, end = parse_timeframe(start, until, paths) - if end is None or start is None: + if start is None: + return console.print(f"Need a valid start time in the format {EXPECTED_DATETIME_FORMAT}") + elif end is None: return console.print(f"Need a valid end time in the format {EXPECTED_DATETIME_FORMAT}") elif start > end: return console.print(f"Start time [{start}] is after end time [{end}]") @@ -580,8 +586,8 @@ def show(log, paths): Optionally the directory where the logs are stored (`-p`) can be specified. \b > Examples: - > ovos-logs show -l skills # Display skills.log - > ovos-logs show -l debug -p ~/custom_path/ # Display debug.log from a custom path + > ovos-logs show -l skills # Display skills.log + > ovos-logs show -l debug -p ~/custom_path/ # Display debug.log from a custom path """ if not any(os.path.exists(os.path.join(path, f"{log}.log")) for path in paths): return console.print(f"File does not exist") @@ -604,10 +610,10 @@ def reduce(size, date, logs, paths): Optionally the directory where the logs are stored (`-p`) can be specified. \b > Examples: - > ovos-logs reduce # Reduce all logs to 0 bytes - > ovos-logs reduce -s 1000000 # Reduce all logs to ~1MB (latest logs) - > ovos-logs reduce -d "1-12-2023 17:00" # Reduce all logs to entries after the specified date/time - > ovos-logs reduce -s 1000000 -l skills -l bus # Reduce skills.log and bus.log to ~1MB (latest logs) + > ovos-logs reduce # Reduce all logs to 0 bytes + > ovos-logs reduce -s 1000000 # Reduce all logs to ~1MB (latest logs) + > ovos-logs reduce -d "1-12-2023 17:00" # Reduce all logs to entries after the specified date/time + > ovos-logs reduce -s 1000000 -l skills -l bus # Reduce skills.log and bus.log to ~1MB (latest logs) """ if date: From 18f3e4738278548bf671c506eabe58fa8ace31d2 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 15 Jan 2024 19:37:24 +0000 Subject: [PATCH 20/87] Increment Version to 0.1.0a10 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 8d15ff0c..686ce292 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 9 +VERSION_ALPHA = 10 # END_VERSION_BLOCK From 2146f73abbae2d46cbff8e958a7fcf27ebbc287d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 15 Jan 2024 19:37:59 +0000 Subject: [PATCH 21/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae755d06..9f2e0617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a9](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a9) (2024-01-12) +## [0.1.0a10](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a10) (2024-01-15) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a8...0.1.0a9) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a9...0.1.0a10) + +**Merged pull requests:** + +- clarify datetime arg [\#219](https://github.com/OpenVoiceOS/ovos-utils/pull/219) ([emphasize](https://github.com/emphasize)) + +## [V0.1.0a9](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a9) (2024-01-12) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a8...V0.1.0a9) **Implemented enhancements:** From cfcdab8605132a911cf69710778702e8ffccf135 Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Thu, 25 Jan 2024 02:38:10 +0000 Subject: [PATCH 22/87] fix/restore deprecated OCP enums compat (#220) --- ovos_utils/ocp.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index bf4d7aed..4d2858e1 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -1,10 +1,10 @@ +import inspect import mimetypes from dataclasses import dataclass from enum import IntEnum from typing import Optional, Tuple, List, Union -import inspect -import orjson +import orjson from ovos_utils.log import LOG, deprecated OCP_ID = "ovos.common_play" @@ -23,16 +23,17 @@ class MatchConfidence(IntEnum): class TrackState(IntEnum): DISAMBIGUATION = 1 # media result, not queued for playback - PLAYING_SKILL = 20 # Skill is handling playback internally - PLAYING_AUDIO = 21 # Skill forwarded playback to audio service + PLAYING_AUDIOSERVICE = 21 ## DEPRECATED - used in ovos 0.0.7 PLAYING_VIDEO = 22 # Skill forwarded playback to video service + PLAYING_AUDIO = 23 # Skill forwarded playback to audio service PLAYING_MPRIS = 24 # External media player is handling playback PLAYING_WEBVIEW = 25 # Media playback handled in browser (eg. javascript) QUEUED_SKILL = 30 # Waiting playback to be handled inside skill - QUEUED_AUDIO = 31 # Waiting playback in audio service + QUEUED_AUDIOSERVICE = 31 ## DEPRECATED - used in ovos 0.0.7 QUEUED_VIDEO = 32 # Waiting playback in video service + QUEUED_AUDIO = 33 # Waiting playback in audio service QUEUED_WEBVIEW = 34 # Waiting playback in browser service @@ -80,6 +81,7 @@ class PlaybackType(IntEnum): # eg spotify / mycroft common play VIDEO = 1 # Video results AUDIO = 2 # Results should be played audio only + AUDIO_SERVICE = 3 ## DEPRECATED - used in ovos 0.0.7 MPRIS = 4 # External MPRIS compliant player WEBVIEW = 5 # webview, render a url instead of media player UNDEFINED = 100 # data not available, hopefully status will be updated soon.. @@ -91,6 +93,7 @@ class PlaybackMode(IntEnum): AUDIO_ONLY = 10 # only consider audio entries VIDEO_ONLY = 20 # only consider video entries FORCE_AUDIO = 30 # cast video to audio unconditionally + FORCE_AUDIOSERVICE = 40 ## DEPRECATED - used in ovos 0.0.7 EVENTS_ONLY = 50 # only emit ocp events, do not display or play anything. # allows integration with external interfaces From 7bdfc7ca5727c8f5ad687e4a273b0c948e31e41a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 25 Jan 2024 02:38:24 +0000 Subject: [PATCH 23/87] Increment Version to 0.1.0a11 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 686ce292..d03b9933 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 10 +VERSION_ALPHA = 11 # END_VERSION_BLOCK From b9a61ff943bf96d54d46dae07687f39c82178c01 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 25 Jan 2024 02:38:52 +0000 Subject: [PATCH 24/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2e0617..053487aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a10](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a10) (2024-01-15) +## [0.1.0a11](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a11) (2024-01-25) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a9...0.1.0a10) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a10...0.1.0a11) + +**Fixed bugs:** + +- fix/restore deprecated OCP enums compat [\#220](https://github.com/OpenVoiceOS/ovos-utils/pull/220) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.1.0a10](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a10) (2024-01-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a9...V0.1.0a10) **Merged pull requests:** From 4cfdf05cebee406f5f2e25f82a56ffdc1b04f02e Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Thu, 25 Jan 2024 05:37:06 +0000 Subject: [PATCH 25/87] fix/ocp_playlist (#221) allow initing Playlist object as a regular list Co-authored-by: JarbasAi --- ovos_utils/ocp.py | 4 + test/unittests/test_ocp_media.py | 173 +++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 test/unittests/test_ocp_media.py diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 4d2858e1..393e7129 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -264,6 +264,10 @@ class Playlist(list): skill_id: str = OCP_ID skill_icon: str = "" + def __init__(self, *args, **kwargs): + super().__init__(**kwargs) + list.__init__(self, *args) + @property def infocard(self) -> dict: """ diff --git a/test/unittests/test_ocp_media.py b/test/unittests/test_ocp_media.py new file mode 100644 index 00000000..3a2bd165 --- /dev/null +++ b/test/unittests/test_ocp_media.py @@ -0,0 +1,173 @@ +import unittest + +from ovos_utils.ocp import MediaEntry, Playlist, MediaType, PlaybackType, TrackState + +valid_search_results = [ + {'media_type': MediaType.MUSIC, + 'playback': PlaybackType.AUDIO, + 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'skill_icon': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'uri': 'https://freemusicarchive.org/track/07_-_Quantum_Jazz_-_Orbiting_A_Distant_Planet/stream/', + 'title': 'Orbiting A Distant Planet', + 'artist': 'Quantum Jazz', + 'skill_id': 'skill-free_music_archive.neongeckocom', + 'match_confidence': 65}, + {'media_type': MediaType.MUSIC, + 'playback': PlaybackType.AUDIO, + 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'skill_icon': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'uri': 'https://freemusicarchive.org/track/05_-_Quantum_Jazz_-_Passing_Fields/stream/', + 'title': 'Passing Fields', + 'artist': 'Quantum Jazz', + 'match_confidence': 65}, + {'media_type': MediaType.MUSIC, + 'playback': PlaybackType.AUDIO, + 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'skill_icon': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'uri': 'https://freemusicarchive.org/track/04_-_Quantum_Jazz_-_All_About_The_Sun/stream/', + 'title': 'All About The Sun', + 'artist': 'Quantum Jazz', + 'match_confidence': 65} +] + + +class TestMediaEntry(unittest.TestCase): + def test_init(self): + data = valid_search_results[0] + + # Test MediaEntry init + entry = MediaEntry(**data) + self.assertEqual(entry.title, data['title']) + self.assertEqual(entry.uri, data['uri']) + self.assertEqual(entry.artist, data['artist']) + self.assertEqual(entry.skill_id, data['skill_id']) + self.assertEqual(entry.status, TrackState.DISAMBIGUATION) + self.assertEqual(entry.playback, data['playback']) + self.assertEqual(entry.image, data['image']) + self.assertEqual(entry.skill_icon, data['skill_icon']) + self.assertEqual(entry.javascript, "") + + # Test playback passed as int + data['playback'] = int(data['playback']) + new_entry = MediaEntry(**data) + self.assertEqual(entry, new_entry) + + def test_from_dict(self): + dict_data = valid_search_results[1] + from_dict = MediaEntry.from_dict(dict_data) + self.assertIsInstance(from_dict, MediaEntry) + from_init = MediaEntry(dict_data["uri"], dict_data["title"], + image=dict_data["image"], + match_confidence=dict_data["match_confidence"], + playback=PlaybackType.AUDIO, + skill_icon=dict_data["skill_icon"], + media_type=dict_data["media_type"], + artist=dict_data["artist"]) + self.assertEqual(from_init, from_dict) + + # Test int playback + dict_data['playback'] = int(dict_data['playback']) + new_entry = MediaEntry.from_dict(dict_data) + self.assertEqual(from_dict, new_entry) + + self.assertIsInstance(MediaEntry.from_dict({}), MediaEntry) + + +class TestPlaylist(unittest.TestCase): + def test_properties(self): + # Empty Playlist + pl = Playlist() + self.assertEqual(pl.position, 0) + self.assertEqual(pl.entries, []) + self.assertIsNone(pl.current_track) + self.assertTrue(pl.is_first_track) + self.assertTrue(pl.is_last_track) + + # Playlist of dicts + pl = Playlist(valid_search_results) + self.assertEqual(pl.position, 0) + self.assertEqual(len(pl), len(valid_search_results)) + self.assertEqual(len(pl.entries), len(valid_search_results)) + for entry in pl.entries: + self.assertIsInstance(entry, MediaEntry) + self.assertIsInstance(pl.current_track, MediaEntry) + self.assertTrue(pl.is_first_track) + self.assertFalse(pl.is_last_track) + + def test_goto_start(self): + # TODO + pass + + def test_clear(self): + # TODO + pass + + def test_sort_by_conf(self): + # TODO + pass + + def test_add_entry(self): + # TODO + pass + + def test_remove_entry(self): + # TODO + pass + + def test_replace(self): + # TODO + pass + + def test_set_position(self): + # TODO + pass + + def test_goto_track(self): + # TODO + pass + + def test_next_track(self): + # TODO + pass + + def test_prev_track(self): + # TODO + pass + + def test_validate_position(self): + # Test empty playlist + pl = Playlist() + pl.position = 0 + pl._validate_position() + self.assertEqual(pl.position, 0) + pl.position = -1 + pl._validate_position() + self.assertEqual(pl.position, 0) + pl.position = 1 + pl._validate_position() + self.assertEqual(pl.position, 0) + + # Test playlist of len 1 + pl = Playlist([valid_search_results[0]]) + pl.position = 0 + pl._validate_position() + self.assertEqual(pl.position, 0) + pl.position = 1 + pl._validate_position() + self.assertEqual(pl.position, 0) + + # Test playlist of len>1 + pl = Playlist(valid_search_results) + pl.position = 0 + pl._validate_position() + self.assertEqual(pl.position, 0) + pl.position = 1 + pl._validate_position() + self.assertEqual(pl.position, 1) + pl.position = 10 + pl._validate_position() + self.assertEqual(pl.position, 0) + + +if __name__ == "__main__": + unittest.main() From 0bc25e9bb24e6c2d3a0d078f8ea0b80f74ba851c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 25 Jan 2024 05:37:21 +0000 Subject: [PATCH 26/87] Increment Version to 0.1.0a12 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index d03b9933..4b80456d 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 11 +VERSION_ALPHA = 12 # END_VERSION_BLOCK From 29a547a41f42fd7c4413f71c75ae6c3207451d0f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 25 Jan 2024 05:37:47 +0000 Subject: [PATCH 27/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 053487aa..f232d8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a11](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a11) (2024-01-25) +## [0.1.0a12](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a12) (2024-01-25) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a10...0.1.0a11) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a11...0.1.0a12) + +**Fixed bugs:** + +- fix/ocp\_playlist [\#221](https://github.com/OpenVoiceOS/ovos-utils/pull/221) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.1.0a11](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a11) (2024-01-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a10...V0.1.0a11) **Fixed bugs:** From e36b16c17bc42d804a0eb7a275b9b445f8b5bd8d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:35:25 +0000 Subject: [PATCH 28/87] Create dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..26e59a20 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/requirements" # Location of package manifests + schedule: + interval: "weekly" From 623738e6e7764e521da72f4fa1e0a22322e24de1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:28:21 +0000 Subject: [PATCH 29/87] Update pexpect requirement from ~=4.6 to ~=4.9 in /requirements (#223) Updates the requirements on [pexpect](https://github.com/pexpect/pexpect) to permit the latest version. - [Release notes](https://github.com/pexpect/pexpect/releases) - [Changelog](https://github.com/pexpect/pexpect/blob/master/doc/history.rst) - [Commits](https://github.com/pexpect/pexpect/compare/4.6...4.9) --- updated-dependencies: - dependency-name: pexpect dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8fafc368..70a857a1 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -pexpect~=4.6 +pexpect~=4.9 requests~=2.26 json_database~=0.7 kthread~=0.2 From 773ad884774ecc8b062b1825d8ebfb429db2ef13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:28:36 +0000 Subject: [PATCH 30/87] Update rapidfuzz requirement from ~=2.0 to ~=3.6 in /requirements (#224) Updates the requirements on [rapidfuzz](https://github.com/rapidfuzz/RapidFuzz) to permit the latest version. - [Release notes](https://github.com/rapidfuzz/RapidFuzz/releases) - [Changelog](https://github.com/rapidfuzz/RapidFuzz/blob/main/CHANGELOG.rst) - [Commits](https://github.com/rapidfuzz/RapidFuzz/compare/v2.0.0...v3.6.1) --- updated-dependencies: - dependency-name: rapidfuzz dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/extras.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/extras.txt b/requirements/extras.txt index c2815fca..60a6dc97 100644 --- a/requirements/extras.txt +++ b/requirements/extras.txt @@ -1,4 +1,4 @@ -rapidfuzz~=2.0 +rapidfuzz~=3.6 ovos_plugin_manager>=0.0.25a2 ovos-config>=0.0.12 ovos-workshop>=0.0.13a22 From 86cb354f049e6d382fea61f663ff32f5f5a60d51 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 30 Jan 2024 06:28:53 +0000 Subject: [PATCH 31/87] Increment Version to 0.1.0a13 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 4b80456d..c01b579b 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 12 +VERSION_ALPHA = 13 # END_VERSION_BLOCK From 56aabe28f39d8d4429ec85024fd711da9cc62d7c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 30 Jan 2024 06:29:24 +0000 Subject: [PATCH 32/87] Update Changelog --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f232d8e3..ae3e1a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ # Changelog -## [0.1.0a12](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a12) (2024-01-25) +## [0.1.0a13](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a13) (2024-01-30) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a11...0.1.0a12) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a12...0.1.0a13) + +**Merged pull requests:** + +- Update rapidfuzz requirement from ~=2.0 to ~=3.6 in /requirements [\#224](https://github.com/OpenVoiceOS/ovos-utils/pull/224) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Update pexpect requirement from ~=4.6 to ~=4.9 in /requirements [\#223](https://github.com/OpenVoiceOS/ovos-utils/pull/223) ([dependabot[bot]](https://github.com/apps/dependabot)) + +## [V0.1.0a12](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a12) (2024-01-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a11...V0.1.0a12) **Fixed bugs:** From 59a7b4a9287c3366ba329fdd9aacd73cf7e88e45 Mon Sep 17 00:00:00 2001 From: builderjer <34875857+builderjer@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:52:07 -0700 Subject: [PATCH 33/87] Option for systemd-timesyncd (#200) removed deprecated failing tests bump deprecation version to 0.2.0 fixed failing tests? Update unit_tests.yml removed python 3.7 from tests finished fixing deprecation fixed deprecated functions --- .github/workflows/unit_tests.yml | 2 +- ovos_utils/system.py | 63 ++++++++++++++++++++++++++------ ovos_utils/version.py | 2 +- test/unittests/test_system.py | 4 -- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d94b0e7e..a24f52ef 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -37,7 +37,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10" ] + python-version: [ 3.8, 3.9, "3.10" ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/ovos_utils/system.py b/ovos_utils/system.py index a9b4bc93..88b9a3a2 100644 --- a/ovos_utils/system.py +++ b/ovos_utils/system.py @@ -5,8 +5,7 @@ import subprocess import sys -from ovos_utils.log import LOG - +from ovos_utils.log import LOG, deprecated def is_running_from_module(module_name): # Stack: @@ -31,15 +30,17 @@ def is_running_from_module(module_name): return True return False - # system utils +@deprecated("DEPRECATED: use ovos-PHAL-plugin-system", "0.2.0") def ntp_sync(): - # Force the system clock to synchronize with internet time servers + """ + Force the system clock to synchronize with internet time servers + """ subprocess.call('service ntp stop', shell=True) subprocess.call('ntpd -gq', shell=True) subprocess.call('service ntp start', shell=True) - +@deprecated("DEPRECATED: use ovos-PHAL-plugin-system", "0.2.0") def system_shutdown(sudo=True): """ Turn the system completely off (with no option to inhibit it) @@ -51,7 +52,7 @@ def system_shutdown(sudo=True): LOG.debug(cmd) subprocess.call(cmd, shell=True) - +@deprecated("DEPRECATED: use ovos-PHAL-plugin-system", "0.2.0") def system_reboot(sudo=True): """ Shut down and restart the system @@ -63,7 +64,7 @@ def system_reboot(sudo=True): LOG.debug(cmd) subprocess.call(cmd, shell=True) - +@deprecated("DEPRECATED: use ovos-PHAL-plugin-system", "0.2.0") def ssh_enable(sudo=True, user=False): """ Permanently allow SSH access @@ -72,7 +73,7 @@ def ssh_enable(sudo=True, user=False): """ enable_service("ssh.service", sudo=sudo, user=user) - +@deprecated("DEPRECATED: use ovos-PHAL-plugin-system", "0.2.0") def ssh_disable(sudo=True, user=False): """ Permanently block SSH access from the outside @@ -81,7 +82,7 @@ def ssh_disable(sudo=True, user=False): """ disable_service("ssh.service", sudo=sudo, user=user) - +@deprecated("DEPRECATED: use ovos-PHAL-plugin-system", "0.2.0") def restart_mycroft_service(sudo=True, user=False): """ Restarts the `mycroft.service` systemd service @@ -90,6 +91,28 @@ def restart_mycroft_service(sudo=True, user=False): """ restart_service("mycroft.service", sudo=sudo, user=user) +def is_running_from_module(module_name): + # Stack: + # [0] - _log() + # [1] - debug(), info(), warning(), or error() + # [2] - caller + stack = inspect.stack() + + # Record: + # [0] - frame object + # [1] - filename + # [2] - line number + # [3] - function + # ... + for record in stack[2:]: + mod = inspect.getmodule(record[0]) + name = mod.__name__ if mod else '' + # module name in file path of caller + # or import name matches module name + if f"/{module_name}/" in record[1] or \ + name.startswith(module_name.replace("-", "_").replace(" ", "_")): + return True + return False def restart_service(service_name, sudo=True, user=False): """ @@ -160,6 +183,25 @@ def check_service_active(service_name, sudo=False, user=False) -> bool: state = subprocess.run(status_command, shell=True).returncode return state == 0 +def check_service_installed(service_name, sudo=False, user=False) -> bool: + """ + Checks if a systemd service is installed using systemctl + @param service_name: name of service to check + @param user: pass --user flag when calling systemctl + @param sudo: use sudo when calling systemctl + @return: True if the service is installed, else False + """ + if not service_name.endswith('.service'): + service_name = f"{service_name}.service" + installed_base_command = f"systemctl list-unit-files -t service" + if user: + status_command = f"{installed_base_command} --user" + elif sudo: + status_command = f"sudo {installed_base_command}" + # Add a grep to the command + status_command = f"{status_command} | grep -i {service_name}" + state = subprocess.call(status_command, shell=True) + return state == 0 def get_desktop_environment(): # From http://stackoverflow.com/questions/2035657/what-is-my-current-desktop-environment @@ -246,7 +288,7 @@ def has_screen(): pass # fallback check using matplotlib if available - # seems to be foolproof and OS agnostic + # seems to be foolproof and OS agnostic # but do not want to drag the dependency if not have_display: try: @@ -260,7 +302,6 @@ def has_screen(): pass return have_display - def module_property(func): """ Decorator to turn module functions into properties. diff --git a/ovos_utils/version.py b/ovos_utils/version.py index c01b579b..cac9840e 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 13 +VERSION_ALPHA = 14 # END_VERSION_BLOCK diff --git a/test/unittests/test_system.py b/test/unittests/test_system.py index e513ac80..9cd1543d 100644 --- a/test/unittests/test_system.py +++ b/test/unittests/test_system.py @@ -8,10 +8,6 @@ def test_is_running_from_module(self): self.assertFalse(is_running_from_module("mycroft")) self.assertTrue(is_running_from_module("unittest")) - def test_ntp_sync(self): - # TODO - pass - @patch("subprocess.Popen") def test_system_shutdown(self, popen): from ovos_utils.system import system_shutdown From 62a7ec12199c8e680e2f1d7f65619d136daad7d0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 18 Feb 2024 19:52:21 +0000 Subject: [PATCH 34/87] Increment Version to 0.1.0a15 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index cac9840e..16ecc16b 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 14 +VERSION_ALPHA = 15 # END_VERSION_BLOCK From 665f885ca06b6b5386ff673d6386d6ea6a4fc437 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 18 Feb 2024 19:52:49 +0000 Subject: [PATCH 35/87] Update Changelog --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3e1a5d..449d3cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.1.0a13](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a13) (2024-01-30) +## [0.1.0a15](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a15) (2024-02-18) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a12...0.1.0a13) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a13...0.1.0a15) + +**Closed issues:** + +- Move mark1 module to PHAL Plugin [\#123](https://github.com/OpenVoiceOS/ovos-utils/issues/123) + +**Merged pull requests:** + +- Option for systemd-timesyncd [\#200](https://github.com/OpenVoiceOS/ovos-utils/pull/200) ([builderjer](https://github.com/builderjer)) + +## [V0.1.0a13](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a13) (2024-01-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a12...V0.1.0a13) **Merged pull requests:** From c7006d5b898c510a0fa003b686b4c4574442c55d Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 9 Mar 2024 22:32:49 -0600 Subject: [PATCH 36/87] chore(docs): rename readme.md to README.md (#230) --- readme.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename readme.md => README.md (100%) diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md From 6377dca8896dfee109933617144a7254d0a6281c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 10 Mar 2024 04:33:06 +0000 Subject: [PATCH 37/87] Increment Version to 0.1.0a16 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 16ecc16b..c5a25f98 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 15 +VERSION_ALPHA = 16 # END_VERSION_BLOCK From 67e785561ffe1f812fcc67baae6cecd594ead833 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 10 Mar 2024 04:33:33 +0000 Subject: [PATCH 38/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 449d3cf0..7f13c7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a15](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a15) (2024-02-18) +## [0.1.0a16](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a16) (2024-03-10) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a13...0.1.0a15) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a15...0.1.0a16) + +**Merged pull requests:** + +- chore\(docs\): rename readme.md to README.md [\#230](https://github.com/OpenVoiceOS/ovos-utils/pull/230) ([mikejgray](https://github.com/mikejgray)) + +## [V0.1.0a15](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a15) (2024-02-18) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a13...V0.1.0a15) **Closed issues:** From e711ceeb3dcc589f69b61c49e004c623d6cef52e Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 9 Mar 2024 22:34:46 -0600 Subject: [PATCH 39/87] chore(docs): add a long description to PyPi (#229) * chore(docs): add a long description to PyPi Partially fufills https://github.com/OpenVoiceOS/ovos-core/issues/390 * Rename readme.md to README.md --------- Co-authored-by: JarbasAI <33701864+JarbasAl@users.noreply.github.com> --- setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 97e29527..1f926149 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,8 @@ def required(requirements_file): return [pkg for pkg in requirements if pkg.strip() and not pkg.startswith("#")] +with open("README.md", "r") as f: + long_description = f.read() setup( name='ovos_utils', @@ -66,8 +68,10 @@ def required(requirements_file): include_package_data=True, license='Apache', author='jarbasAI', - author_email='jarbasai@mailfence.com', - description='collection of simple utilities for use across the mycroft ecosystem', + author_email='jarbas@openvoiceos.com', + description='collection of simple utilities for use across the openvoiceos ecosystem', + long_description=long_description, + long_description_content_type="text/markdown", entry_points={ 'console_scripts': [ 'ovos-logs=ovos_utils.log_parser:ovos_logs' From 7650d62f1384de55a1e8419462f73878ca801d26 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 10 Mar 2024 04:35:00 +0000 Subject: [PATCH 40/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index c5a25f98..b13a88b3 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 16 +VERSION_ALPHA = 17 # END_VERSION_BLOCK From 57cecdc4cbd1884dbe56262d20aa7cb55df14e2b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 10 Mar 2024 04:35:28 +0000 Subject: [PATCH 41/87] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f13c7de..6572cf8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.1.0a16](https://github.com/OpenVoiceOS/ovos-utils/tree/0.1.0a16) (2024-03-10) +## [Unreleased](https://github.com/OpenVoiceOS/ovos-utils/tree/HEAD) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a15...0.1.0a16) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a16...HEAD) + +**Merged pull requests:** + +- chore\(docs\): add a long description to PyPi [\#229](https://github.com/OpenVoiceOS/ovos-utils/pull/229) ([mikejgray](https://github.com/mikejgray)) + +## [V0.1.0a16](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.1.0a16) (2024-03-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a15...V0.1.0a16) **Merged pull requests:** From 04fc16cd25d463d647b11e57c66d0a86a4c7c1e8 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 23 Mar 2024 14:39:18 -0500 Subject: [PATCH 42/87] feat: mac support for ram cache (#231) --- ovos_utils/file_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py index ace07c7a..4245a525 100644 --- a/ovos_utils/file_utils.py +++ b/ovos_utils/file_utils.py @@ -5,6 +5,7 @@ import tempfile from os import walk from os.path import dirname, splitext, join +from sys import platform from threading import RLock from typing import Optional, List @@ -56,12 +57,12 @@ def get_temp_path(*args) -> str: def get_cache_directory(folder: str) -> str: """ Get a temporary cache directory, preferably in RAM. - Note that Windows will not use RAM. + Note that only Linux use RAM. @param folder: base path to use for cache @return: valid cache path """ path = get_temp_path(folder) - if os.name != 'nt': + if platform == 'linux': try: from memory_tempfile import MemoryTempfile path = join(MemoryTempfile(fallback=True).gettempdir(), folder) From 50abd12f6d067e70e4a1259fda7e877cde2bef88 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 23 Mar 2024 19:39:31 +0000 Subject: [PATCH 43/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index b13a88b3..547c309a 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 17 +VERSION_ALPHA = 18 # END_VERSION_BLOCK From 3b4a9b6624d6f68a86f3c98aae5cd44afeb0c6ba Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 23 Mar 2024 19:39:56 +0000 Subject: [PATCH 44/87] Update Changelog --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6572cf8d..483da4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ ## [Unreleased](https://github.com/OpenVoiceOS/ovos-utils/tree/HEAD) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a16...HEAD) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V...HEAD) + +**Fixed bugs:** + +- feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) + +## [V](https://github.com/OpenVoiceOS/ovos-utils/tree/V) (2024-03-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a16...V) **Merged pull requests:** From 404b7fcbbba8570ce22f0dcf0adf691716c00fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20De=20Melo?= Date: Tue, 28 May 2024 23:49:28 +0200 Subject: [PATCH 45/87] Add Damerau-Levenshtein similarity matching (#248) --- ovos_utils/parse.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ovos_utils/parse.py b/ovos_utils/parse.py index cc385672..e3cd50d8 100644 --- a/ovos_utils/parse.py +++ b/ovos_utils/parse.py @@ -19,6 +19,7 @@ class MatchStrategy(IntEnum): PARTIAL_TOKEN_RATIO = auto() PARTIAL_TOKEN_SORT_RATIO = auto() PARTIAL_TOKEN_SET_RATIO = auto() + DAMERAU_LEVENSHTEIN_SIMILARITY = auto() def _validate_matching_strategy(strategy): @@ -37,6 +38,7 @@ def fuzzy_match(x, against, strategy=MatchStrategy.SIMPLE_RATIO): down to 0.0 for no match at all. """ strategy = _validate_matching_strategy(strategy) + LOG.debug(f"matching strategy: {strategy}") if strategy == MatchStrategy.RATIO: score = rapidfuzz.fuzz.ratio(x, against) / 100 elif strategy == MatchStrategy.PARTIAL_RATIO: @@ -51,6 +53,8 @@ def fuzzy_match(x, against, strategy=MatchStrategy.SIMPLE_RATIO): score = rapidfuzz.fuzz.partial_token_set_ratio(x, against) / 100 elif strategy == MatchStrategy.PARTIAL_TOKEN_RATIO: score = rapidfuzz.fuzz.partial_token_ratio(x, against) / 100 + elif strategy == MatchStrategy.DAMERAU_LEVENSHTEIN_SIMILARITY: + score = rapidfuzz.distance.DamerauLevenshtein.normalized_similarity(x, against) else: score = SequenceMatcher(None, x, against).ratio() From 930a7010a7c17557c6224a8b989d60388e5a44b9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 28 May 2024 21:49:42 +0000 Subject: [PATCH 46/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 547c309a..f1001ddc 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 18 +VERSION_ALPHA = 19 # END_VERSION_BLOCK From 4bc171af2b1ee5733a1654d9326e9b57a01f2583 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 28 May 2024 21:50:10 +0000 Subject: [PATCH 47/87] Update Changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 483da4b8..d312c0bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ [Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V...HEAD) +**Implemented enhancements:** + +- Add Damerau-Levenshtein similarity matching [\#248](https://github.com/OpenVoiceOS/ovos-utils/pull/248) ([femelo](https://github.com/femelo)) + **Fixed bugs:** - feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) @@ -28,10 +32,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a13...V0.1.0a15) -**Closed issues:** - -- Move mark1 module to PHAL Plugin [\#123](https://github.com/OpenVoiceOS/ovos-utils/issues/123) - **Merged pull requests:** - Option for systemd-timesyncd [\#200](https://github.com/OpenVoiceOS/ovos-utils/pull/200) ([builderjer](https://github.com/builderjer)) From c8391315316be44ec99870a40a32f4201d9ef9cb Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:43:42 +0100 Subject: [PATCH 48/87] deprecate/signal utils (#249) we already deprecated usage of this everywhere in ovos, only the utils remain in some circumstances, depending on workdir, python also gets confused with importing signal, as it tries to use the relative import for ovos_utils.signal, imports moved to where they are used to minimize this impact this also allows process_utils.py to be imported under windows as reported by @mikejgray --- ovos_utils/file_utils.py | 30 ++++++++++++++++++++++++++++++ ovos_utils/process_utils.py | 12 +++++++++--- ovos_utils/signal.py | 31 ++++++++++--------------------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py index 4245a525..0b02f6b1 100644 --- a/ovos_utils/file_utils.py +++ b/ovos_utils/file_utils.py @@ -16,6 +16,36 @@ from ovos_utils.log import LOG, log_deprecation +def ensure_directory_exists(directory, domain=None): + """ Create a directory and give access rights to all + + Args: + domain (str): The IPC domain. Basically a subdirectory to prevent + overlapping signal filenames. + + Returns: + str: a path to the directory + """ + if domain: + directory = os.path.join(directory, domain) + + # Expand and normalize the path + directory = os.path.normpath(directory) + directory = os.path.expanduser(directory) + + if not os.path.isdir(directory): + try: + save = os.umask(0) + os.makedirs(directory, 0o777) # give everyone rights to r/w here + except OSError: + LOG.warning("Failed to create: " + directory) + pass + finally: + os.umask(save) + + return directory + + def to_alnum(skill_id: str) -> str: """ Convert a skill id to only alphanumeric characters diff --git a/ovos_utils/process_utils.py b/ovos_utils/process_utils.py index 1a8c981b..aef04cd8 100644 --- a/ovos_utils/process_utils.py +++ b/ovos_utils/process_utils.py @@ -17,8 +17,6 @@ from collections import namedtuple from dataclasses import dataclass from enum import IntEnum -from signal import signal, SIGKILL, SIGINT, SIGTERM, \ - SIG_DFL, default_int_handler, SIG_IGN # signals from threading import Event from time import sleep, monotonic @@ -251,6 +249,9 @@ def __init__(self, sig_value, func): func: User supplied function that will act as the new signal handler. """ super(Signal, self).__init__() # python 3+ 'super().__init__() + + from signal import signal, SIG_DFL, default_int_handler, SIG_IGN + self.__sig_value = sig_value self.__user_func = func # store user passed function self.__previous_func = signal(sig_value, self) @@ -278,6 +279,8 @@ def __del__(self): Class destructor. Called during garbage collection. Resets the signal handler to the previous function. """ + + from signal import signal signal(self.__sig_value, self.__previous_func) @@ -330,6 +333,7 @@ def set_handlers(self): """ Trap both SIGINT and SIGTERM to gracefully clean up PID files """ + from signal import SIGINT, SIGTERM self.__handlers = {SIGINT: Signal(SIGINT, self.delete), SIGTERM: Signal(SIGTERM, self.delete)} @@ -348,7 +352,8 @@ def exists(self): if not os.path.isfile(self.path): return with open(self.path, 'r') as L: - try: + try: # TODO - make it work in windows ? + from signal import SIGKILL os.kill(int(L.read()), SIGKILL) except Exception as e: LOG.error(f"Failed to kill PID {L}: {e}") @@ -402,4 +407,5 @@ def reset_sigint_handler(): This fixes KeyboardInterrupt not getting raised when started via start-mycroft.sh """ + from signal import signal, SIGINT, default_int_handler signal(SIGINT, default_int_handler) diff --git a/ovos_utils/signal.py b/ovos_utils/signal.py index 36c29790..1f6d3fa4 100644 --- a/ovos_utils/signal.py +++ b/ovos_utils/signal.py @@ -3,9 +3,10 @@ import tempfile import time -from ovos_utils.log import LOG, log_deprecation +from ovos_utils.log import LOG, log_deprecation, deprecated +@deprecated("ovos_utils.signal module has been deprecated!", "0.2.0") def get_ipc_directory(domain=None, config=None): """Get the directory used for Inter Process Communication @@ -23,8 +24,8 @@ def get_ipc_directory(domain=None, config=None): if config is None: log_deprecation(f"Expected a dict config and got None.", "0.1.0") try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config() + from ovos_config.config import Configuration + config = Configuration() except ImportError: LOG.warning("Config not provided and ovos_config not available") config = dict() @@ -35,6 +36,7 @@ def get_ipc_directory(domain=None, config=None): return ensure_directory_exists(path, domain) +@deprecated("use 'from ovos_utils.file_utils import ensure_directory_exists' instead", "0.2.0") def ensure_directory_exists(directory, domain=None): """ Create a directory and give access rights to all @@ -45,26 +47,11 @@ def ensure_directory_exists(directory, domain=None): Returns: str: a path to the directory """ - if domain: - directory = os.path.join(directory, domain) - - # Expand and normalize the path - directory = os.path.normpath(directory) - directory = os.path.expanduser(directory) - - if not os.path.isdir(directory): - try: - save = os.umask(0) - os.makedirs(directory, 0o777) # give everyone rights to r/w here - except OSError: - LOG.warning("Failed to create: " + directory) - pass - finally: - os.umask(save) - - return directory + from ovos_utils.file_utils import ensure_directory_exists as _ede + return _ede(directory, domain) +@deprecated("ovos_utils.signal module has been deprecated!", "0.2.0") def create_file(filename): """ Create the file filename and create any directories needed @@ -79,6 +66,7 @@ def create_file(filename): f.write('') +@deprecated("ovos_utils.signal module has been deprecated!", "0.2.0") def create_signal(signal_name, config=None): """Create a named signal @@ -96,6 +84,7 @@ def create_signal(signal_name, config=None): return False +@deprecated("ovos_utils.signal module has been deprecated!", "0.2.0") def check_for_signal(signal_name, sec_lifetime=0, config=None): """See if a named signal exists From a0ca8667326163a7a1aa5d7fdef2e280e8ce9a1c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 2 Jun 2024 17:43:55 +0000 Subject: [PATCH 49/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index f1001ddc..ebb943c8 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 19 +VERSION_ALPHA = 20 # END_VERSION_BLOCK From 18940e2a5a1fa5e16d163d95a69dd445750d7b2c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 2 Jun 2024 17:44:26 +0000 Subject: [PATCH 50/87] Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d312c0bf..fd9cc004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) +**Merged pull requests:** + +- deprecate/signal utils [\#249](https://github.com/OpenVoiceOS/ovos-utils/pull/249) ([JarbasAl](https://github.com/JarbasAl)) + ## [V](https://github.com/OpenVoiceOS/ovos-utils/tree/V) (2024-03-10) [Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.1.0a16...V) From d2fc17d7b7f2041de5e913a9e20890b4a33d57cd Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 5 Jun 2024 00:23:30 +0100 Subject: [PATCH 51/87] fix/log_spam (#251) ``` 2024-06-04 23:54:39.764 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:39.764 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:39.931 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:39.931 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.072 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.072 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.072 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.073 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.073 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.073 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.075 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.075 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.308 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.322 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.325 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.328 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.331 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.336 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.339 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.343 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.346 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.350 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.353 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.356 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.359 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.363 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.366 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.369 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.372 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.376 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.379 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.382 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.386 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.389 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.392 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.395 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.399 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.402 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.405 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.408 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.412 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.421 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.423 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:40.426 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:40.436 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.360 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:41.401 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.404 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.407 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.410 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.413 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.417 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.420 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.423 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.426 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.429 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.432 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.435 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.438 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.441 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.444 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.447 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.451 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 5 2024-06-04 23:54:41.962 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.234 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.362 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.753 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.754 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.755 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.755 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.755 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.761 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:42.762 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:43.162 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:43.466 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:43.875 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:44.161 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:44.162 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:44.162 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:44.163 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:44.163 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 2024-06-04 23:54:44.164 - skills - ovos_utils.parse:fuzzy_match:41 - DEBUG - matching strategy: 1 ``` --- ovos_utils/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/parse.py b/ovos_utils/parse.py index e3cd50d8..545a5fe9 100644 --- a/ovos_utils/parse.py +++ b/ovos_utils/parse.py @@ -38,7 +38,7 @@ def fuzzy_match(x, against, strategy=MatchStrategy.SIMPLE_RATIO): down to 0.0 for no match at all. """ strategy = _validate_matching_strategy(strategy) - LOG.debug(f"matching strategy: {strategy}") + # LOG.debug(f"matching strategy: {strategy}") if strategy == MatchStrategy.RATIO: score = rapidfuzz.fuzz.ratio(x, against) / 100 elif strategy == MatchStrategy.PARTIAL_RATIO: From 4f9ff34459ef1e9efaa499b9df3974fdac6b1438 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 4 Jun 2024 23:23:43 +0000 Subject: [PATCH 52/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index ebb943c8..eea34318 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 20 +VERSION_ALPHA = 21 # END_VERSION_BLOCK From 4994fe816eba9cc632d31131cebd93878a1adfeb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 4 Jun 2024 23:24:13 +0000 Subject: [PATCH 53/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd9cc004..fa8bc6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ **Fixed bugs:** +- fix/log\_spam [\#251](https://github.com/OpenVoiceOS/ovos-utils/pull/251) ([JarbasAl](https://github.com/JarbasAl)) - feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) **Merged pull requests:** From 3ddfe5ab9f4dd4576bba64826f0a79a8c6db7570 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 13 Jun 2024 01:20:14 +0100 Subject: [PATCH 54/87] fix/dont_lie_about_being_a_uri (#246) * Increment Version to * fix/dont_lie_about_being_a_uri consumers of the MediaEntry.from_dict method need to start using dict2entry instead relates to https://github.com/OpenVoiceOS/ovos-ocp-audio-plugin/issues/114 * fix typing * fix tests * revert automations mess up :sweat_smile: * backwards compat --------- Co-authored-by: JarbasAl --- ovos_utils/ocp.py | 143 ++++++++++++++++++++++++------- test/unittests/test_events.py | 2 +- test/unittests/test_ocp_media.py | 2 +- 3 files changed, 113 insertions(+), 34 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 393e7129..22cb4dbd 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -226,18 +226,16 @@ def as_dict(self) -> dict: return orjson.loads(orjson.dumps(self).decode("utf-8")) @staticmethod - def from_dict(track: dict): - if track.get("playlist"): - kwargs = {k: v for k, v in track.items() - if k in inspect.signature(Playlist).parameters} - playlist = Playlist(**kwargs) - for e in track["playlist"]: - playlist.add_entry(e) - return playlist - else: - kwargs = {k: v for k, v in track.items() - if k in inspect.signature(MediaEntry).parameters} - return MediaEntry(**kwargs) + def from_dict(track: dict) -> 'MediaEntry': + if "uri" not in track: + LOG.error("track dictionary does not contain 'uri', it is not a valid MediaEntry") + # raise ValueError("track dictionary does not contain 'uri', it is not a valid MediaEntry") + LOG.warning("DEPRECATED: use dict2entry() for Playlists and PluginStreams," + " MediaEntry.from_dict is only for regular media, will start throwing ValueError in 0.1.0") + return dict2entry(track) + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(MediaEntry).parameters} + return MediaEntry(**kwargs) @property def mimetype(self) -> Optional[Tuple[Optional[str], Optional[str]]]: @@ -254,6 +252,63 @@ def __eq__(self, other): return other == self.infocard +@dataclass +class PluginStream: + stream: str + extractor_id: str + title: str = "" + artist: str = "" + match_confidence: int = 0 # 0 - 100 + skill_id: str = OCP_ID + playback: PlaybackType = PlaybackType.UNDEFINED + status: TrackState = TrackState.DISAMBIGUATION + media_type: MediaType = MediaType.GENERIC + length: int = 0 # in seconds + image: str = "" + skill_icon: str = "" + + @property + def infocard(self) -> dict: + """ + Return dict data used for a UI display + (model shared with MediaEntry) + """ + return { + "duration": self.length, + "track": self.title, + "image": self.image, + "album": self.skill_id, + "source": self.skill_icon, + "uri": f"{self.extractor_id}//{self.stream}" + } + + @property + def as_media_entry(self) -> MediaEntry: + kwargs = {k: v for k, v in self.as_dict.items() + if k in inspect.signature(MediaEntry).parameters} + # TODO - in a couple major versions this should be deprecated + kwargs["uri"] = f"{self.extractor_id}//{self.stream}" + return MediaEntry(**kwargs) + + @property + def as_dict(self) -> dict: + """ + Return a dict representation of this MediaEntry + """ + # orjson handles dataclasses directly + return orjson.loads(orjson.dumps(self).decode("utf-8")) + + @staticmethod + def from_dict(track: dict) -> 'PluginStream': + if "extractor_id" not in track: + raise ValueError("track dictionary does not contain 'extractor_id', it is not a valid PluginStream") + if "stream" not in track: + raise ValueError("track dictionary does not contain 'stream', it is not a valid PluginStream") + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(PluginStream).parameters} + return PluginStream(**kwargs) + + @dataclass class Playlist(list): title: str = "" @@ -284,8 +339,15 @@ def infocard(self) -> dict: } @staticmethod - def from_dict(track: dict): - return MediaEntry.from_dict(track) + def from_dict(track: dict) -> 'Playlist': + if "playlist" not in track: + raise ValueError("track dictionary does not contain 'playlist' entries, it is not a valid Playlist") + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(Playlist).parameters} + playlist = Playlist(**kwargs) + for e in track.get("playlist", []): + playlist.add_entry(e) + return playlist @property def as_dict(self) -> dict: @@ -305,20 +367,20 @@ def as_dict(self) -> dict: return data @property - def entries(self) -> List[MediaEntry]: + def entries(self) -> List[Union[MediaEntry, PluginStream]]: """ Return a list of MediaEntry objects in the playlist """ entries = [] for e in self: if isinstance(e, dict): - e = MediaEntry.from_dict(e) - if isinstance(e, MediaEntry): + e = dict2entry(e) + if isinstance(e, (MediaEntry, PluginStream)): entries.append(e) return entries @property - def current_track(self) -> Optional[MediaEntry]: + def current_track(self) -> Optional[Union[MediaEntry, PluginStream]]: """ Return the current MediaEntry or None if the playlist is empty """ @@ -327,7 +389,7 @@ def current_track(self) -> Optional[MediaEntry]: self._validate_position() track = self[self.position] if isinstance(track, dict): - track = MediaEntry.from_dict(track) + track = dict2entry(track) return track @property @@ -371,7 +433,7 @@ def sort_by_conf(self): key=lambda k: k.match_confidence if isinstance(k, (MediaEntry, Playlist)) else k.get("match_confidence", 0), reverse=True) - def add_entry(self, entry: MediaEntry, index: int = -1) -> None: + def add_entry(self, entry: Union[MediaEntry, PluginStream], index: int = -1) -> None: """ Add an entry at the requested index @param entry: MediaEntry to add to playlist @@ -383,9 +445,9 @@ def add_entry(self, entry: MediaEntry, index: int = -1) -> None: f"playlist only has {len(self)} entries") if isinstance(entry, dict): - entry = MediaEntry.from_dict(entry) + entry = dict2entry(entry) - assert isinstance(entry, (MediaEntry, Playlist)) + assert isinstance(entry, (MediaEntry, Playlist, PluginStream)) if index == -1: index = len(self) @@ -395,7 +457,7 @@ def add_entry(self, entry: MediaEntry, index: int = -1) -> None: self.insert(index, entry) - def remove_entry(self, entry: Union[int, dict, MediaEntry]) -> None: + def remove_entry(self, entry: Union[int, dict, MediaEntry, PluginStream]) -> None: """ Remove the requested entry from the playlist or raise a ValueError @param entry: index or MediaEntry to remove from the playlist @@ -404,8 +466,8 @@ def remove_entry(self, entry: Union[int, dict, MediaEntry]) -> None: self.pop(entry) return if isinstance(entry, dict): - entry = MediaEntry.from_dict(entry) - assert isinstance(entry, MediaEntry) + entry = dict2entry(entry) + assert isinstance(entry, (MediaEntry, PluginStream)) for idx, e in enumerate(self.entries): if e == entry: self.pop(idx) @@ -413,7 +475,7 @@ def remove_entry(self, entry: Union[int, dict, MediaEntry]) -> None: else: raise ValueError(f"entry not in playlist: {entry}") - def replace(self, new_list: List[Union[dict, MediaEntry]]) -> None: + def replace(self, new_list: List[Union[dict, MediaEntry, PluginStream]]) -> None: """ Replace the contents of this Playlist with new_list @param new_list: list of MediaEntry or dict objects to set this list to @@ -430,24 +492,28 @@ def set_position(self, idx: int): self.position = idx self._validate_position() - def goto_track(self, track: Union[MediaEntry, dict]) -> None: + def goto_track(self, track: Union[MediaEntry, dict, PluginStream]) -> None: """ Go to the requested track in the playlist @param track: MediaEntry to find and go to in the playlist """ if isinstance(track, dict): - track = MediaEntry.from_dict(track) + track = dict2entry(track) - assert isinstance(track, (MediaEntry, Playlist)) + assert isinstance(track, (MediaEntry, Playlist, PluginStream)) if isinstance(track, MediaEntry): requested_uri = track.uri + elif isinstance(track, PluginStream): + requested_uri = track.stream else: requested_uri = track.title for idx, t in enumerate(self): if isinstance(t, MediaEntry): pl_entry_uri = t.uri + elif isinstance(t, PluginStream): + pl_entry_uri = t.stream else: pl_entry_uri = t.title @@ -480,14 +546,27 @@ def _validate_position(self) -> None: def __contains__(self, item): if isinstance(item, dict): - item = MediaEntry.from_dict(item) - if isinstance(item, MediaEntry): - for e in self.entries: + item = dict2entry(item) + for e in self.entries: + if isinstance(item, PluginStream) and isinstance(e, PluginStream): + if e.stream == item.stream and e.extractor_id == item.extractor_id: + return True + elif isinstance(item, MediaEntry) and isinstance(e, MediaEntry): if e.uri == item.uri: return True return False +def dict2entry(track: dict) -> Union[PluginStream, MediaEntry, Playlist]: + if track.get("playlist"): + return Playlist.from_dict(track) + elif track.get("extractor_id"): + return PluginStream.from_dict(track) + elif track.get("uri"): + return MediaEntry.from_dict(track) + raise ValueError("track dictionary is not a valid MediaEntry, Playlist or PluginStream") + + if __name__ == "__main__": m = MediaEntry("1") m2 = MediaEntry("2") diff --git a/test/unittests/test_events.py b/test/unittests/test_events.py index cc0709fa..18c5a5e1 100644 --- a/test/unittests/test_events.py +++ b/test/unittests/test_events.py @@ -155,7 +155,7 @@ def test_event_container(self): self.assertEqual(len(bus.ee.listeners("once_event")), 1) new_event = container.events[-1] self.assertEqual(new_event[0], "once_event") - self.assertNotEquals(new_event[1], handler) + self.assertNotEqual(new_event[1], handler) self.assertEqual(len(inspect.signature(new_event[1]).parameters), 1) # Test iterate events diff --git a/test/unittests/test_ocp_media.py b/test/unittests/test_ocp_media.py index 3a2bd165..0a6c18ff 100644 --- a/test/unittests/test_ocp_media.py +++ b/test/unittests/test_ocp_media.py @@ -70,7 +70,7 @@ def test_from_dict(self): new_entry = MediaEntry.from_dict(dict_data) self.assertEqual(from_dict, new_entry) - self.assertIsInstance(MediaEntry.from_dict({}), MediaEntry) + self.assertIsInstance(MediaEntry.from_dict({"uri": "xxx"}), MediaEntry) class TestPlaylist(unittest.TestCase): From f7fe7e56742595a25178eed3ec183ffbe7cf93ce Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jun 2024 00:20:31 +0000 Subject: [PATCH 55/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index eea34318..9353387e 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 21 +VERSION_ALPHA = 22 # END_VERSION_BLOCK From f5db421e0d97fda1fb20c1a8694321dacac12f98 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jun 2024 00:21:06 +0000 Subject: [PATCH 56/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa8bc6d7..f9045062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ **Implemented enhancements:** - Add Damerau-Levenshtein similarity matching [\#248](https://github.com/OpenVoiceOS/ovos-utils/pull/248) ([femelo](https://github.com/femelo)) +- fix/dont\_lie\_about\_being\_a\_uri [\#246](https://github.com/OpenVoiceOS/ovos-utils/pull/246) ([JarbasAl](https://github.com/JarbasAl)) **Fixed bugs:** From 1dbb34873bdc55cb3d0ef4d997bdfc5e07f472ba Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 13 Jun 2024 01:26:06 +0100 Subject: [PATCH 57/87] hotfix/skip_release allow pypi packages until neon automation is fixed --- .github/workflows/publish_alpha.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml index 3c0ef9d1..d17dc6e4 100644 --- a/.github/workflows/publish_alpha.yml +++ b/.github/workflows/publish_alpha.yml @@ -34,20 +34,6 @@ jobs: runs-on: ubuntu-latest needs: update_version steps: - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: V${{ needs.update_version.outputs.version }} - release_name: Release ${{ needs.update_version.outputs.version }} - body: | - Changes in this Release - ${{ needs.update_version.outputs.changelog }} - draft: false - prerelease: true - commitish: dev - name: Checkout Repository uses: actions/checkout@v2 with: From 81b02ca8eee019b4a8b24fc0ef08a8258436a002 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jun 2024 00:26:49 +0000 Subject: [PATCH 58/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 9353387e..b4ad4b96 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 22 +VERSION_ALPHA = 23 # END_VERSION_BLOCK From d87b7490c5d7496fa8b1fe28297fafb1b4ba6491 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:23:35 +0100 Subject: [PATCH 59/87] fix/log_level_cfg (#252) * fix/log_level_cfg react to changes in log level from mycroft.conf * dict_hash * ensure log level --- ovos_utils/log.py | 51 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 86c30048..70cbe7ca 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -12,6 +12,7 @@ # import functools import inspect +import json import logging import os import sys @@ -103,7 +104,8 @@ def init(cls, config=None): cls.base_path = config.get("path") or xdg_path cls.max_bytes = config.get("max_bytes", 50000000) cls.backup_count = config.get("backup_count", 3) - cls.level = config.get("level") or LOG.level + level = config.get("level") or LOG.level + cls.set_level(level) cls.diagnostic_mode = config.get("diagnostic", False) @classmethod @@ -191,16 +193,38 @@ def exception(cls, *args, **kwargs): cls._get_real_logger().exception(*args, **kwargs) +def _monitor_log_level(): + _logs_conf = get_logs_config(LOG.name) + hax = hash(json.dumps(_logs_conf, sort_keys=True, indent=2)) + if hax != _monitor_log_level.config_hash: + _monitor_log_level.config_hash = hax + LOG.init(_logs_conf) + LOG.info("updated LOG level") + + +_monitor_log_level.config_hash = None + + def init_service_logger(service_name): - # this is makes all logs from this service be configured to write to service_name.log file - # if this is not called in every __main__.py entrypoint logs will be written - # to a generic OVOS.log file shared across all services + _logs_conf = get_logs_config(service_name) + _monitor_log_level.config_hash = hash(json.dumps(_logs_conf, sort_keys=True, indent=2)) + LOG.name = service_name + LOG.init(_logs_conf) # setup the LOG instance try: - from ovos_config.config import read_mycroft_config - _cfg = read_mycroft_config() + from ovos_config import Configuration + Configuration.set_config_watcher(_monitor_log_level) except ImportError: - LOG.warning("ovos_config not available. Falling back to defaults") - _cfg = dict() + LOG.warning("Can not monitor config LOG level changes") + + +def get_logs_config(service_name=None, _cfg=None) -> dict: + if _cfg is None: + try: + from ovos_config import Configuration + _cfg = Configuration() + except ImportError: + LOG.warning("ovos_config not available. Falling back to defaults") + _cfg = {} # First try and get the "logging" section log_config = _cfg.get("logging") @@ -213,15 +237,14 @@ def init_service_logger(service_name): # where per-service "logs" are not defined _logs_conf = log_config.get("logs") or _logs_conf # Now get our config by service name - _cfg = log_config.get(service_name) or log_config - # and if "logs" is redefined in "logging." use that - _logs_conf = _cfg.get("logs") or _logs_conf + if service_name: + _cfg = log_config.get(service_name) or log_config + # and if "logs" is redefined in "logging." use that + _logs_conf = _cfg.get("logs") or _logs_conf # Grab the log level from whatever section we found, defaulting to INFO _log_level = _cfg.get("log_level", "INFO") - # and write it into the "logs" config _logs_conf["level"] = _log_level - LOG.name = service_name - LOG.init(_logs_conf) # setup the LOG instance + return _logs_conf def log_deprecation(log_message: str = "DEPRECATED", From 58e8e557acbb3d3e6000d74a8aef9add3e721146 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 18 Jun 2024 23:23:52 +0000 Subject: [PATCH 60/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index b4ad4b96..2351f3e3 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 23 +VERSION_ALPHA = 24 # END_VERSION_BLOCK From 7dfbbef2c0bdb911392374c95d2d03e0aa9c5a57 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 18 Jun 2024 23:24:19 +0000 Subject: [PATCH 61/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9045062..c1391278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ **Fixed bugs:** +- fix/log\_level\_cfg [\#252](https://github.com/OpenVoiceOS/ovos-utils/pull/252) ([JarbasAl](https://github.com/JarbasAl)) - fix/log\_spam [\#251](https://github.com/OpenVoiceOS/ovos-utils/pull/251) ([JarbasAl](https://github.com/JarbasAl)) - feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) From a50c301def6d9d66b805bbdb26d0d9981c0a15c0 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:26:51 +0100 Subject: [PATCH 62/87] new_util/get_sound_duration (#254) --- ovos_utils/sound.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ovos_utils/sound.py b/ovos_utils/sound.py index d211b4d6..efe51ff7 100644 --- a/ovos_utils/sound.py +++ b/ovos_utils/sound.py @@ -1,6 +1,10 @@ import os import subprocess +import wave from copy import deepcopy +from os.path import isfile +from typing import Optional + from distutils.spawn import find_executable from ovos_utils.log import LOG @@ -115,3 +119,36 @@ def play_audio(uri, play_cmd=None, environment=None): LOG.error(f"Failed to play: {play_cmd}") LOG.exception(e) return None + + +def get_sound_duration(path: str, base_dir: Optional[str] = "") -> float: + """return sound duration, in seconds""" + if base_dir and path.startswith("snd/"): + resolved_path = f"{base_dir}/{path}" + if isfile(resolved_path): + path = resolved_path + + if not isfile(path): + raise FileNotFoundError(f"could not resolve sound file: {path}") + + if path.endswith(".wav"): + with wave.open(path, 'r') as f: + frames = f.getnframes() + rate = f.getframerate() + return frames / float(rate) + media_info = find_executable("mediainfo") + if media_info: + args = (media_info, path) + popen = subprocess.Popen(args, stdout=subprocess.PIPE) + popen.wait() + output = popen.stdout.read().decode("utf-8").split("Duration")[1].split("\n")[0] + output = "".join([c for c in output if c.isdigit()]) + return int(output) / 1000 + ffprobe = find_executable("ffprobe") + if ffprobe: + args = (ffprobe, "-show_entries", "format=duration", "-i", path) + popen = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + popen.wait() + output = popen.stdout.read().decode("utf-8") + return float(output.split("duration=")[-1].split("\n")[0]) + raise RuntimeError("Failed to determine sound length, please install mediainfo or ffprobe") From 65a47427b0425daf687aaef9574bf58a21a40624 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 19 Jun 2024 17:27:08 +0000 Subject: [PATCH 63/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 2351f3e3..cfd7c550 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 24 +VERSION_ALPHA = 25 # END_VERSION_BLOCK From 12808d1dd2c4846e58829d476c24bb22be7ee7cf Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 19 Jun 2024 17:27:33 +0000 Subject: [PATCH 64/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1391278..d881741a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Implemented enhancements:** +- new\_util/get\_sound\_duration [\#254](https://github.com/OpenVoiceOS/ovos-utils/pull/254) ([JarbasAl](https://github.com/JarbasAl)) - Add Damerau-Levenshtein similarity matching [\#248](https://github.com/OpenVoiceOS/ovos-utils/pull/248) ([femelo](https://github.com/femelo)) - fix/dont\_lie\_about\_being\_a\_uri [\#246](https://github.com/OpenVoiceOS/ovos-utils/pull/246) ([JarbasAl](https://github.com/JarbasAl)) From b1519c43bbf4e1030e1d21590ac85f30e6ab85c8 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 21 Jun 2024 02:22:23 +0100 Subject: [PATCH 65/87] fix/sound_duration (#255) when using mediainfo the parsing would fail for files > 1min --- ovos_utils/sound.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ovos_utils/sound.py b/ovos_utils/sound.py index efe51ff7..bf87b648 100644 --- a/ovos_utils/sound.py +++ b/ovos_utils/sound.py @@ -120,7 +120,6 @@ def play_audio(uri, play_cmd=None, environment=None): LOG.exception(e) return None - def get_sound_duration(path: str, base_dir: Optional[str] = "") -> float: """return sound duration, in seconds""" if base_dir and path.startswith("snd/"): @@ -136,14 +135,6 @@ def get_sound_duration(path: str, base_dir: Optional[str] = "") -> float: frames = f.getnframes() rate = f.getframerate() return frames / float(rate) - media_info = find_executable("mediainfo") - if media_info: - args = (media_info, path) - popen = subprocess.Popen(args, stdout=subprocess.PIPE) - popen.wait() - output = popen.stdout.read().decode("utf-8").split("Duration")[1].split("\n")[0] - output = "".join([c for c in output if c.isdigit()]) - return int(output) / 1000 ffprobe = find_executable("ffprobe") if ffprobe: args = (ffprobe, "-show_entries", "format=duration", "-i", path) @@ -151,4 +142,25 @@ def get_sound_duration(path: str, base_dir: Optional[str] = "") -> float: popen.wait() output = popen.stdout.read().decode("utf-8") return float(output.split("duration=")[-1].split("\n")[0]) + media_info = find_executable("mediainfo") + if media_info: + args = (media_info, path) + popen = subprocess.Popen(args, stdout=subprocess.PIPE) + popen.wait() + output = popen.stdout.read().decode("utf-8").split("Duration")[1].split("\n")[0].split(":")[-1] + t = 0 + if " h" in output: + h, output = output.split(" h") + t += int(h) * 60 * 60 + if " min" in output: + m, output = output.split(" min") + t += int(m) * 60 + if " s" in output: + m, output = output.split(" s") + t += int(m) + if " ms" in output: + m, output = output.split(" ms") + t += int(m) / 1000 + return t raise RuntimeError("Failed to determine sound length, please install mediainfo or ffprobe") + From 92461fd16f30ed996422edbb7f92ed7f90d1ceae Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Jun 2024 01:22:38 +0000 Subject: [PATCH 66/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index cfd7c550..b2a4919a 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 25 +VERSION_ALPHA = 26 # END_VERSION_BLOCK From 9b56eac37f3000d0a8cf5302bbe4cd4bbcc92d5d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Jun 2024 01:23:06 +0000 Subject: [PATCH 67/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d881741a..cebe888e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ **Fixed bugs:** +- fix/sound\_duration [\#255](https://github.com/OpenVoiceOS/ovos-utils/pull/255) ([JarbasAl](https://github.com/JarbasAl)) - fix/log\_level\_cfg [\#252](https://github.com/OpenVoiceOS/ovos-utils/pull/252) ([JarbasAl](https://github.com/JarbasAl)) - fix/log\_spam [\#251](https://github.com/OpenVoiceOS/ovos-utils/pull/251) ([JarbasAl](https://github.com/JarbasAl)) - feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) From e6b416e2effe712aef50a5ec2cf8c9ca9d219333 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:02:33 +0100 Subject: [PATCH 68/87] fix/ocp_playlist (#256) * fix/ocp_playlist fix Playlist objects * fix --- ovos_utils/ocp.py | 27 ++++++++++++++++++++------- test/unittests/test_ocp_media.py | 6 ++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 22cb4dbd..9151e526 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -5,6 +5,7 @@ from typing import Optional, Tuple, List, Union import orjson + from ovos_utils.log import LOG, deprecated OCP_ID = "ovos.common_play" @@ -312,16 +313,29 @@ def from_dict(track: dict) -> 'PluginStream': @dataclass class Playlist(list): title: str = "" + artist: str = "" position: int = 0 - length: int = 0 # in seconds image: str = "" match_confidence: int = 0 # 0 - 100 skill_id: str = OCP_ID skill_icon: str = "" + playback: PlaybackType = PlaybackType.UNDEFINED + media_type: MediaType = MediaType.GENERIC def __init__(self, *args, **kwargs): - super().__init__(**kwargs) - list.__init__(self, *args) + super().__init__() + for k, v in kwargs.items(): + if hasattr(self, k): + self.__setattr__(k, v) + if len(args) == 1 and isinstance(args[0], list): + args = args[0] + for e in args: + self.add_entry(e) + + @property + def length(self): + """calc the length value based on all entries""" + return sum([e.length for e in self.entries]) @property def infocard(self) -> dict: @@ -343,7 +357,7 @@ def from_dict(track: dict) -> 'Playlist': if "playlist" not in track: raise ValueError("track dictionary does not contain 'playlist' entries, it is not a valid Playlist") kwargs = {k: v for k, v in track.items() - if k in inspect.signature(Playlist).parameters} + if k in inspect.signature(Playlist).parameters} playlist = Playlist(**kwargs) for e in track.get("playlist", []): playlist.add_entry(e) @@ -592,6 +606,5 @@ def dict2entry(track: dict) -> Union[PluginStream, MediaEntry, Playlist]: p.goto_start() print(p.position) p.set_position(1) - print(p) # Playlist(title='My Jams', position=1, length=0, image='', match_confidence=0, skill_id='ovos.common_play', skill_icon='') - - + print( + p) # Playlist(title='My Jams', position=1, length=0, image='', match_confidence=0, skill_id='ovos.common_play', skill_icon='') diff --git a/test/unittests/test_ocp_media.py b/test/unittests/test_ocp_media.py index 0a6c18ff..b94dbad2 100644 --- a/test/unittests/test_ocp_media.py +++ b/test/unittests/test_ocp_media.py @@ -76,7 +76,8 @@ def test_from_dict(self): class TestPlaylist(unittest.TestCase): def test_properties(self): # Empty Playlist - pl = Playlist() + pl = Playlist(title="empty playlist") + self.assertEqual(pl.title, "empty playlist") self.assertEqual(pl.position, 0) self.assertEqual(pl.entries, []) self.assertIsNone(pl.current_track) @@ -84,7 +85,8 @@ def test_properties(self): self.assertTrue(pl.is_last_track) # Playlist of dicts - pl = Playlist(valid_search_results) + pl = Playlist(valid_search_results, title="my playlist") + self.assertEqual(pl.title, "my playlist") self.assertEqual(pl.position, 0) self.assertEqual(len(pl), len(valid_search_results)) self.assertEqual(len(pl.entries), len(valid_search_results)) From 1dfc636fcf5c78ae8962966fa345e2d3d7e9dff4 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Jun 2024 14:02:47 +0000 Subject: [PATCH 69/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index b2a4919a..74be6fd2 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 26 +VERSION_ALPHA = 27 # END_VERSION_BLOCK From 3fa1504841b4b6bc49e51a9b2c6b9120db3de203 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Jun 2024 14:03:12 +0000 Subject: [PATCH 70/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cebe888e..6911dbde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ **Fixed bugs:** +- fix/ocp\_playlist [\#256](https://github.com/OpenVoiceOS/ovos-utils/pull/256) ([JarbasAl](https://github.com/JarbasAl)) - fix/sound\_duration [\#255](https://github.com/OpenVoiceOS/ovos-utils/pull/255) ([JarbasAl](https://github.com/JarbasAl)) - fix/log\_level\_cfg [\#252](https://github.com/OpenVoiceOS/ovos-utils/pull/252) ([JarbasAl](https://github.com/JarbasAl)) - fix/log\_spam [\#251](https://github.com/OpenVoiceOS/ovos-utils/pull/251) ([JarbasAl](https://github.com/JarbasAl)) From a05ab7bce24d4ff68c7943650e9d41274ec6d5fa Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:20:37 +0100 Subject: [PATCH 71/87] feat/ocp_stream_utils (#257) * feat/ocp_stream_utils helper methods to parse the ocp stream from PluginStream objects * more tests * more tests --- ovos_utils/ocp.py | 16 +++++++ test/unittests/test_ocp_media.py | 75 ++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 9151e526..0768c264 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -268,6 +268,22 @@ class PluginStream: image: str = "" skill_icon: str = "" + def extract_uri(self, video=True) -> str: + from ovos_plugin_manager.ocp import load_stream_extractors + xtract = load_stream_extractors() + meta = xtract.extract_stream(f"{self.extractor_id}//{self.stream}", + video=video) + return meta["uri"] + + def extract_media_entry(self, video=True) -> MediaEntry: + from ovos_plugin_manager.ocp import load_stream_extractors + xtract = load_stream_extractors() + meta = xtract.extract_stream(f"{self.extractor_id}//{self.stream}", + video=video) + kwargs = {k: v for k, v in meta.items() + if k in inspect.signature(MediaEntry).parameters} + return MediaEntry(**kwargs) + @property def infocard(self) -> dict: """ diff --git a/test/unittests/test_ocp_media.py b/test/unittests/test_ocp_media.py index b94dbad2..00f7304c 100644 --- a/test/unittests/test_ocp_media.py +++ b/test/unittests/test_ocp_media.py @@ -2,7 +2,7 @@ from ovos_utils.ocp import MediaEntry, Playlist, MediaType, PlaybackType, TrackState -valid_search_results = [ +dict_search_results = [ {'media_type': MediaType.MUSIC, 'playback': PlaybackType.AUDIO, 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', @@ -17,6 +17,7 @@ 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', 'skill_icon': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', 'uri': 'https://freemusicarchive.org/track/05_-_Quantum_Jazz_-_Passing_Fields/stream/', + 'skill_id': 'skill-free_music_archive.neongeckocom', 'title': 'Passing Fields', 'artist': 'Quantum Jazz', 'match_confidence': 65}, @@ -24,16 +25,48 @@ 'playback': PlaybackType.AUDIO, 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', 'skill_icon': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'skill_id': 'skill-free_music_archive.neongeckocom', 'uri': 'https://freemusicarchive.org/track/04_-_Quantum_Jazz_-_All_About_The_Sun/stream/', 'title': 'All About The Sun', 'artist': 'Quantum Jazz', 'match_confidence': 65} ] +search_results = [ + MediaEntry(media_type=MediaType.MUSIC, + playback=PlaybackType.AUDIO, + image='https://freemusicarchive.org/legacy/fma-smaller.jpg', + uri='https://freemusicarchive.org/track/07_-_Quantum_Jazz_-_Orbiting_A_Distant_Planet/stream/', + skill_icon='https://freemusicarchive.org/legacy/fma-smaller.jpg', + title='Orbiting A Distant Planet', + artist='Quantum Jazz', + skill_id='skill-free_music_archive.neongeckocom', + match_confidence=65 + ), + MediaEntry(media_type=MediaType.MUSIC, + playback=PlaybackType.AUDIO, + image='https://freemusicarchive.org/legacy/fma-smaller.jpg', + uri='https://freemusicarchive.org/track/05_-_Quantum_Jazz_-_Passing_Fields/stream/', + skill_icon='https://freemusicarchive.org/legacy/fma-smaller.jpg', + title='Passing Fields', + artist='Quantum Jazz', + skill_id='skill-free_music_archive.neongeckocom', + match_confidence=65), + MediaEntry(media_type=MediaType.MUSIC, + playback=PlaybackType.AUDIO, + image='https://freemusicarchive.org/legacy/fma-smaller.jpg', + uri='https://freemusicarchive.org/track/04_-_Quantum_Jazz_-_All_About_The_Sun/stream/', + skill_icon='https://freemusicarchive.org/legacy/fma-smaller.jpg', + title='All About The Sun', + artist='Quantum Jazz', + skill_id='skill-free_music_archive.neongeckocom', + match_confidence=65) +] + class TestMediaEntry(unittest.TestCase): def test_init(self): - data = valid_search_results[0] + data = dict_search_results[0] # Test MediaEntry init entry = MediaEntry(**data) @@ -52,8 +85,17 @@ def test_init(self): new_entry = MediaEntry(**data) self.assertEqual(entry, new_entry) + def test_as_dict(self): + for idx, track in enumerate(search_results): + for k, v in track.as_dict.items(): + if k in dict_search_results[idx]: + self.assertEqual(v, dict_search_results[idx][k]) + + for idx, track in enumerate(dict_search_results): + self.assertEqual(MediaEntry.from_dict(track), search_results[idx]) + def test_from_dict(self): - dict_data = valid_search_results[1] + dict_data = dict_search_results[1] from_dict = MediaEntry.from_dict(dict_data) self.assertIsInstance(from_dict, MediaEntry) from_init = MediaEntry(dict_data["uri"], dict_data["title"], @@ -61,6 +103,7 @@ def test_from_dict(self): match_confidence=dict_data["match_confidence"], playback=PlaybackType.AUDIO, skill_icon=dict_data["skill_icon"], + skill_id=dict_data["skill_id"], media_type=dict_data["media_type"], artist=dict_data["artist"]) self.assertEqual(from_init, from_dict) @@ -85,17 +128,33 @@ def test_properties(self): self.assertTrue(pl.is_last_track) # Playlist of dicts - pl = Playlist(valid_search_results, title="my playlist") + pl = Playlist(dict_search_results, title="my playlist") self.assertEqual(pl.title, "my playlist") self.assertEqual(pl.position, 0) - self.assertEqual(len(pl), len(valid_search_results)) - self.assertEqual(len(pl.entries), len(valid_search_results)) + self.assertEqual(len(pl), len(dict_search_results)) + self.assertEqual(len(pl.entries), len(dict_search_results)) + for entry in pl.entries: + self.assertIsInstance(entry, MediaEntry) + self.assertIsInstance(pl.current_track, MediaEntry) + self.assertTrue(pl.is_first_track) + self.assertFalse(pl.is_last_track) + + # Playlist of MediaEntry + pl = Playlist(search_results, title="Test Jazz") + self.assertEqual(pl.title, "Test Jazz") + self.assertEqual(pl.position, 0) + self.assertEqual(len(pl), len(search_results)) + self.assertEqual(len(pl.entries), len(search_results)) for entry in pl.entries: self.assertIsInstance(entry, MediaEntry) self.assertIsInstance(pl.current_track, MediaEntry) self.assertTrue(pl.is_first_track) self.assertFalse(pl.is_last_track) + self.assertListEqual(pl.entries, search_results) + for idx, e in enumerate(pl.as_dict["playlist"]): + self.assertEqual(MediaEntry.from_dict(e), search_results[idx]) + def test_goto_start(self): # TODO pass @@ -150,7 +209,7 @@ def test_validate_position(self): self.assertEqual(pl.position, 0) # Test playlist of len 1 - pl = Playlist([valid_search_results[0]]) + pl = Playlist([dict_search_results[0]]) pl.position = 0 pl._validate_position() self.assertEqual(pl.position, 0) @@ -159,7 +218,7 @@ def test_validate_position(self): self.assertEqual(pl.position, 0) # Test playlist of len>1 - pl = Playlist(valid_search_results) + pl = Playlist(dict_search_results) pl.position = 0 pl._validate_position() self.assertEqual(pl.position, 0) From 0ea660e732e51756366fd9831ec4f09020a36dfd Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Jun 2024 17:20:53 +0000 Subject: [PATCH 72/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 74be6fd2..e8383036 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 27 +VERSION_ALPHA = 28 # END_VERSION_BLOCK From 121391b889fb8626beafc99922a2741a5b0f28e7 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Jun 2024 17:21:19 +0000 Subject: [PATCH 73/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6911dbde..3f9272d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Implemented enhancements:** +- feat/ocp\_stream\_utils [\#257](https://github.com/OpenVoiceOS/ovos-utils/pull/257) ([JarbasAl](https://github.com/JarbasAl)) - new\_util/get\_sound\_duration [\#254](https://github.com/OpenVoiceOS/ovos-utils/pull/254) ([JarbasAl](https://github.com/JarbasAl)) - Add Damerau-Levenshtein similarity matching [\#248](https://github.com/OpenVoiceOS/ovos-utils/pull/248) ([femelo](https://github.com/femelo)) - fix/dont\_lie\_about\_being\_a\_uri [\#246](https://github.com/OpenVoiceOS/ovos-utils/pull/246) ([JarbasAl](https://github.com/JarbasAl)) From ce642ff5860d3a0e6f9a7bbaf0f4bd97b700d170 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:01:52 +0100 Subject: [PATCH 74/87] fix/detect_ovos_gui_app (#258) * fix/detect_ovos_gui_app account for the ovos maintained mycroft-gui fork when checking for running gui processes * add deprecation warning drop bigscreen * add deprecation warning drop bigscreen --- ovos_utils/gui.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ovos_utils/gui.py b/ovos_utils/gui.py index c53705b8..f6a9d2ae 100644 --- a/ovos_utils/gui.py +++ b/ovos_utils/gui.py @@ -6,10 +6,10 @@ from ovos_utils.system import is_installed, has_screen, is_process_running _default_gui_apps = ( - "mycroft-gui-app", + "ovos-gui-app", "ovos-shell", - "mycroft-embedded-shell", - "plasmashell" + "mycroft-gui-app", + "mycroft-embedded-shell" ) @@ -33,7 +33,13 @@ def is_gui_running(applications: List[str] = _default_gui_apps) -> bool: Return true if a GUI application is running @param applications: list of applications to check for """ - return any((is_process_running(app) for app in applications)) + deprecated = any((is_process_running(app) for app in applications + if app.startswith("mycroft-"))) + if deprecated: + LOG.warning("you are running a deprecated mycroft-gui version, " + "please move to a OVOS maintained version") + return True + return deprecated or any((is_process_running(app) for app in applications)) def is_gui_connected(bus=None) -> bool: From 2101d14f019ef185909e4e1a869630b5023e0c8c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 5 Jul 2024 19:02:07 +0000 Subject: [PATCH 75/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index e8383036..f7df90a6 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 28 +VERSION_ALPHA = 29 # END_VERSION_BLOCK From 70c46a3517e22a98b2abd809e39abe054135b39e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 5 Jul 2024 19:02:35 +0000 Subject: [PATCH 76/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9272d1..1cf790f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ **Fixed bugs:** +- fix/detect\_ovos\_gui\_app [\#258](https://github.com/OpenVoiceOS/ovos-utils/pull/258) ([JarbasAl](https://github.com/JarbasAl)) - fix/ocp\_playlist [\#256](https://github.com/OpenVoiceOS/ovos-utils/pull/256) ([JarbasAl](https://github.com/JarbasAl)) - fix/sound\_duration [\#255](https://github.com/OpenVoiceOS/ovos-utils/pull/255) ([JarbasAl](https://github.com/JarbasAl)) - fix/log\_level\_cfg [\#252](https://github.com/OpenVoiceOS/ovos-utils/pull/252) ([JarbasAl](https://github.com/JarbasAl)) From b90285cc2301b7b2dc3c0f89557bf4efa997c006 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 3 Aug 2024 19:29:35 +0100 Subject: [PATCH 77/87] fix/playlist_deserialization (#262) * fix/playlist_deserialization * rm debug print --- ovos_utils/ocp.py | 13 ++++++++----- test/unittests/test_ocp_media.py | 3 +++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 0768c264..194da332 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -342,7 +342,10 @@ def __init__(self, *args, **kwargs): super().__init__() for k, v in kwargs.items(): if hasattr(self, k): - self.__setattr__(k, v) + try: + self.__setattr__(k, v) + except AttributeError: + continue if len(args) == 1 and isinstance(args[0], list): args = args[0] for e in args: @@ -351,7 +354,8 @@ def __init__(self, *args, **kwargs): @property def length(self): """calc the length value based on all entries""" - return sum([e.length for e in self.entries]) + # -1 is for live streams + return max(-1, sum([e.length for e in self.entries])) @property def infocard(self) -> dict: @@ -372,9 +376,8 @@ def infocard(self) -> dict: def from_dict(track: dict) -> 'Playlist': if "playlist" not in track: raise ValueError("track dictionary does not contain 'playlist' entries, it is not a valid Playlist") - kwargs = {k: v for k, v in track.items() - if k in inspect.signature(Playlist).parameters} - playlist = Playlist(**kwargs) + + playlist = Playlist(**track) for e in track.get("playlist", []): playlist.add_entry(e) return playlist diff --git a/test/unittests/test_ocp_media.py b/test/unittests/test_ocp_media.py index 00f7304c..13c6fa18 100644 --- a/test/unittests/test_ocp_media.py +++ b/test/unittests/test_ocp_media.py @@ -155,6 +155,9 @@ def test_properties(self): for idx, e in enumerate(pl.as_dict["playlist"]): self.assertEqual(MediaEntry.from_dict(e), search_results[idx]) + # test serialize/deserialize + self.assertEqual(Playlist.from_dict(pl.as_dict), pl) + def test_goto_start(self): # TODO pass From 22d442b03f2c9be78a7a4c354c8a1e8ff82526f0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 3 Aug 2024 18:29:48 +0000 Subject: [PATCH 78/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index f7df90a6..89e3016e 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 29 +VERSION_ALPHA = 30 # END_VERSION_BLOCK From c3a52d0927a46aa6021aceaca78a47326d1378e6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 3 Aug 2024 18:30:15 +0000 Subject: [PATCH 79/87] Update Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf790f4..04bd6268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ **Fixed bugs:** +- fix/playlist\_deserialization [\#262](https://github.com/OpenVoiceOS/ovos-utils/pull/262) ([JarbasAl](https://github.com/JarbasAl)) - fix/detect\_ovos\_gui\_app [\#258](https://github.com/OpenVoiceOS/ovos-utils/pull/258) ([JarbasAl](https://github.com/JarbasAl)) - fix/ocp\_playlist [\#256](https://github.com/OpenVoiceOS/ovos-utils/pull/256) ([JarbasAl](https://github.com/JarbasAl)) - fix/sound\_duration [\#255](https://github.com/OpenVoiceOS/ovos-utils/pull/255) ([JarbasAl](https://github.com/JarbasAl)) @@ -20,6 +21,10 @@ - fix/log\_spam [\#251](https://github.com/OpenVoiceOS/ovos-utils/pull/251) ([JarbasAl](https://github.com/JarbasAl)) - feat: mac support for ram cache [\#231](https://github.com/OpenVoiceOS/ovos-utils/pull/231) ([mikejgray](https://github.com/mikejgray)) +**Closed issues:** + +- "systemctl restart" not working when using with ovos-phal-plugin-system [\#259](https://github.com/OpenVoiceOS/ovos-utils/issues/259) + **Merged pull requests:** - deprecate/signal utils [\#249](https://github.com/OpenVoiceOS/ovos-utils/pull/249) ([JarbasAl](https://github.com/JarbasAl)) From 8e5d4aabee5195ae01dad5e4b3afa98ece78f70f Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:41:22 +0100 Subject: [PATCH 80/87] fix/plugin_stream_extraction (#263) fix update method type check validate that required sei are available ensure metadata is not dropped by the plugin --- ovos_utils/ocp.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ovos_utils/ocp.py b/ovos_utils/ocp.py index 194da332..27a70385 100644 --- a/ovos_utils/ocp.py +++ b/ovos_utils/ocp.py @@ -177,7 +177,7 @@ def update(self, entry: dict, skipkeys: list = None, newonly: bool = False): @param newonly: if True, only adds new keys; existing keys are unchanged """ skipkeys = skipkeys or [] - if isinstance(entry, MediaEntry): + if isinstance(entry, (MediaEntry, PluginStream)): entry = entry.as_dict entry = entry or {} for k, v in entry.items(): @@ -278,8 +278,17 @@ def extract_uri(self, video=True) -> str: def extract_media_entry(self, video=True) -> MediaEntry: from ovos_plugin_manager.ocp import load_stream_extractors xtract = load_stream_extractors() + if self.extractor_id not in xtract.supported_seis: + raise ImportError(f"stream extractor not installed, extractor_id: {self.extractor_id}\navailable plugins: {list(xtract.extractors)}") + meta = xtract.extract_stream(f"{self.extractor_id}//{self.stream}", video=video) + p = meta.get("playback", self.playback) + if p == PlaybackType.UNDEFINED: + meta["playback"] = PlaybackType.VIDEO if video else PlaybackType.AUDIO + for k, v in self.as_dict.items(): + if not meta.get(k): + meta[k] = v kwargs = {k: v for k, v in meta.items() if k in inspect.signature(MediaEntry).parameters} return MediaEntry(**kwargs) From 9f7ed6afd2f6ab96b6b8ec7da66257f61666bd40 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 3 Aug 2024 19:41:36 +0000 Subject: [PATCH 81/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 89e3016e..975172ee 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 30 +VERSION_ALPHA = 31 # END_VERSION_BLOCK From d0d55877e2ab34b45665209fcd702fad9f10ca33 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 3 Aug 2024 19:42:04 +0000 Subject: [PATCH 82/87] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04bd6268..5edd1162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ **Fixed bugs:** +- fix/plugin\_stream\_extraction [\#263](https://github.com/OpenVoiceOS/ovos-utils/pull/263) ([JarbasAl](https://github.com/JarbasAl)) - fix/playlist\_deserialization [\#262](https://github.com/OpenVoiceOS/ovos-utils/pull/262) ([JarbasAl](https://github.com/JarbasAl)) - fix/detect\_ovos\_gui\_app [\#258](https://github.com/OpenVoiceOS/ovos-utils/pull/258) ([JarbasAl](https://github.com/JarbasAl)) - fix/ocp\_playlist [\#256](https://github.com/OpenVoiceOS/ovos-utils/pull/256) ([JarbasAl](https://github.com/JarbasAl)) From e4a3ff7e16c4c0843e537947a03c6f6614304732 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:20:59 -0700 Subject: [PATCH 83/87] log module unit test coverage (#260) * Add docstrings and type annotations to `log` module Add test coverage for log rotation Outline test coverage for added log module functions Relates to #2 #239 #233 #250 #253 * Update logs to resolve order-related failure * Add test coverage for `get_logs_config` Refactor variable in `get_logs_config` for clarity Refactor `get_logs_config` for more predictable handling of an empty service name * Add test coverage for `get_log_path` * Add test coverage for `get_log_paths` Update `get_log_paths` to check all log names with `-` normalized to `_` (appeared to be the original intent) Add `enclosure` and `admin` service logs used by Neon and legacy Mycroft/OVOS setups * Add test coverage for `get_available_logs` * Add test coverage for `_monitor_log_level` * Add test of `get_config` call count * Update `get_log_paths` to reference config directly instead of test some well-known values Update unit test for `get_log_paths` * Remove unused `ALL_SERVICES` variable * Remove unused import --------- Co-authored-by: Daniel McKnight --- ovos_utils/log.py | 72 ++++++----- test/unittests/test_log.py | 194 +++++++++++++++++++++++++++++- test/unittests/test_logs/real.log | 0 3 files changed, 235 insertions(+), 31 deletions(-) create mode 100644 test/unittests/test_logs/real.log diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 70cbe7ca..6f4c69a0 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -21,17 +21,6 @@ from pathlib import Path from typing import Optional, List, Set -ALL_SERVICES = {"bus", - "audio", - "skills", - "voice", - "gui", - "ovos", - "phal", - "phal-admin", - "hivemind", - "hivemind-voice-sat"} - class LOG: """ @@ -205,11 +194,16 @@ def _monitor_log_level(): _monitor_log_level.config_hash = None -def init_service_logger(service_name): +def init_service_logger(service_name: str): + """ + Initialize `LOG` for the specified service + @param service_name: Name of service to configure `LOG` for + """ _logs_conf = get_logs_config(service_name) - _monitor_log_level.config_hash = hash(json.dumps(_logs_conf, sort_keys=True, indent=2)) + _monitor_log_level.config_hash = hash(json.dumps(_logs_conf, sort_keys=True, + indent=2)) LOG.name = service_name - LOG.init(_logs_conf) # setup the LOG instance + LOG.init(_logs_conf) # set up the LOG instance try: from ovos_config import Configuration Configuration.set_config_watcher(_monitor_log_level) @@ -217,7 +211,14 @@ def init_service_logger(service_name): LOG.warning("Can not monitor config LOG level changes") -def get_logs_config(service_name=None, _cfg=None) -> dict: +def get_logs_config(service_name: Optional[str] = None, + _cfg: Optional[dict] = None) -> dict: + """ + Get logging configuration for the specified service + @param service_name: Name of service to get logging configuration for + @param _cfg: Configuration to parse + @return: dict logging configuration for the specified service + """ if _cfg is None: try: from ovos_config import Configuration @@ -227,20 +228,23 @@ def get_logs_config(service_name=None, _cfg=None) -> dict: _cfg = {} # First try and get the "logging" section - log_config = _cfg.get("logging") + logging_conf = _cfg.get("logging") # For compatibility we try to get the "logs" from the root level # and default to empty which is used in case there is no logging # section _logs_conf = _cfg.get("logs") or {} - if log_config: # We found a logging section + if logging_conf: # We found a logging section # if "logs" is defined in "logging" use that as the default # where per-service "logs" are not defined - _logs_conf = log_config.get("logs") or _logs_conf + _logs_conf = logging_conf.get("logs") or _logs_conf # Now get our config by service name if service_name: - _cfg = log_config.get(service_name) or log_config - # and if "logs" is redefined in "logging." use that - _logs_conf = _cfg.get("logs") or _logs_conf + _cfg = logging_conf.get(service_name) or logging_conf + else: + # No service name specified, use `logging` configuration + _cfg = logging_conf + # and if "logs" is redefined in "logging." use that + _logs_conf = _cfg.get("logs") or _logs_conf # Grab the log level from whatever section we found, defaulting to INFO _log_level = _cfg.get("log_level", "INFO") _logs_conf["level"] = _log_level @@ -326,6 +330,7 @@ def get_log_path(service: str, directories: Optional[List[str]] = None) \ Returns: path to log directory for service + (returned path may be `None` if `directories` is specified) """ if directories: for directory in directories: @@ -342,9 +347,9 @@ def get_log_path(service: str, directories: Optional[List[str]] = None) \ xdg_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER", "mycroft") return os.path.join(xdg_state_home(), xdg_base) - config = Configuration().get("logging", dict()).get("logs", dict()) + config = get_logs_config(service_name=service) # service specific config or default config location - path = config.get(service, {}).get("path") or config.get("path") + path = config.get("path") # default xdg location if not path: path = os.path.join(xdg_state_home(), get_xdg_base()) @@ -352,7 +357,7 @@ def get_log_path(service: str, directories: Optional[List[str]] = None) \ return path -def get_log_paths() -> Set[str]: +def get_log_paths(config: Optional[dict] = None) -> Set[str]: """ Get all log paths for all service logs Different services may have different log paths @@ -361,9 +366,20 @@ def get_log_paths() -> Set[str]: set of paths to log directories """ paths = set() - ALL_SERVICES.union({s.replace("-", "_") for s in ALL_SERVICES}) - for service in ALL_SERVICES: - paths.add(get_log_path(service)) + if not config: + try: + from ovos_config import Configuration + config = Configuration() + except ImportError: + LOG.warning("ovos_config not available. Falling back to defaults") + config = dict() + + for name, service_config in config.get("logging", {}).items(): + if not isinstance(service_config, dict) or name == "logs": + continue + if service_config.get("path"): + paths.add(service_config.get("path")) + paths.add(get_log_path("")) return paths @@ -375,7 +391,7 @@ def get_available_logs(directories: Optional[List[str]] = None) -> List[str]: directories: (optional) list of directories to check for service Returns: - list of log files + list of log file basenames (i.e. "audio", "skills") """ directories = directories or get_log_paths() return [Path(f).stem for path in directories diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 751f96ae..f816cf1b 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -17,6 +17,7 @@ def tearDownClass(cls) -> None: def test_log(self): import ovos_utils.log + importlib.reload(ovos_utils.log) from ovos_utils.log import LOG # Default log config self.assertEqual(LOG.base_path, "stdout") @@ -60,9 +61,76 @@ def test_log(self): self.assertEqual(len(lines), 1) self.assertTrue(lines[0].endswith("This will print\n")) - def test_init_service_logger(self): - from ovos_utils.log import init_service_logger - # TODO + # Init with backup + test_config['max_bytes'] = 2 + test_config['backup_count'] = 1 + test_config['level'] = 'INFO' + LOG.init(test_config) + LOG.name = "rotate" + LOG.info("first") + LOG.info("second") + LOG.debug("third") + log_1 = join(LOG.base_path, f"{LOG.name}.log.1") + log = join(LOG.base_path, f"{LOG.name}.log") + + # Log rotated once + with open(log_1) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("first\n")) + with open(log) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("second\n")) + + LOG.info("fourth") + # Log rotated again + with open(log_1) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("second\n")) + with open(log) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("fourth\n")) + + # Multiple log rotations within a short period of time + for i in range(100): + LOG.info(str(i)) + with open(log_1) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("98\n")) + with open(log) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("99\n")) + + @patch("ovos_utils.log.get_logs_config") + @patch("ovos_config.Configuration.set_config_watcher") + def test_init_service_logger(self, set_config_watcher, log_config): + from ovos_utils.log import init_service_logger, LOG + + # Test log init with default config + log_config.return_value = dict() + LOG.level = "ERROR" + init_service_logger("default") + from ovos_utils.log import LOG + set_config_watcher.assert_called_once() + self.assertEqual(LOG.name, "default") + self.assertEqual(LOG.level, "ERROR") + + # Test log init with config + set_config_watcher.reset_mock() + log_config.return_value = {"path": self.test_dir, + "level": "DEBUG"} + init_service_logger("configured") + from ovos_utils.log import LOG + set_config_watcher.assert_called_once() + self.assertEqual(LOG.name, "configured") + self.assertEqual(LOG.level, "DEBUG") + LOG.debug("This will print") + self.assertTrue(isfile(join(self.test_dir, "configured.log"))) @patch("ovos_utils.log.LOG.create_logger") def test_log_deprecation(self, create_logger): @@ -115,3 +183,123 @@ def _deprecated_function(test_arg): log_msg = log_warning.call_args[0][0] self.assertIn('version=1.0.0', log_msg, log_msg) self.assertIn('test deprecation', log_msg, log_msg) + + @patch("ovos_utils.log.get_logs_config") + @patch("ovos_utils.log.LOG") + def test_monitor_log_level(self, log, get_config): + from ovos_utils.log import _monitor_log_level + + log.name = "TEST" + get_config.return_value = {"changed": False} + + _monitor_log_level() + get_config.assert_called_once_with("TEST") + log.init.assert_called_once_with(get_config.return_value) + log.info.assert_called_once() + + # Callback with no change + _monitor_log_level() + self.assertEqual(get_config.call_count, 2) + log.init.assert_called_once_with(get_config.return_value) + log.info.assert_called_once() + + # Callback with change + get_config.return_value["changed"] = True + _monitor_log_level() + self.assertEqual(get_config.call_count, 3) + self.assertEqual(log.init.call_count, 2) + log.init.assert_called_with(get_config.return_value) + + def test_get_logs_config(self): + from ovos_utils.log import get_logs_config + valid_config = {"level": "DEBUG", + "path": self.test_dir, + "max_bytes": 1000, + "backup_count": 2, + "diagnostic": False} + valid_config_2 = {"max_bytes": 100000, + "diagnostic": True} + logs_config = {"path": self.test_dir, + "max_bytes": 1000, + "backup_count": 2, + "diagnostic": False} + legacy_config = {"log_level": "DEBUG", + "logs": logs_config} + + logging_config = {"logging": {"log_level": "DEBUG", + "logs": logs_config, + "test_service": {"log_level": "WARNING", + "logs": valid_config_2} + } + } + + # Test original config with `logs` section and no `logging` section + self.assertEqual(get_logs_config("", legacy_config), valid_config) + + # Test `logging.logs` config with no service config + self.assertEqual(get_logs_config("service", logging_config), valid_config) + + # Test `logging.logs` config with `logging.` overrides + expected_config = {**valid_config_2, **{"level": "WARNING"}} + self.assertEqual(get_logs_config("test_service", logging_config), + expected_config) + + # Test `logs` config with `logging.` overrides + logging_config["logs"] = logging_config["logging"].pop("logs") + self.assertEqual(get_logs_config("test_service", logging_config), + expected_config) + + # Test `logging.` config with no `logs` or `logging.logs` + logging_config["logging"].pop("log_level") + logging_config.pop("logs") + self.assertEqual(get_logs_config("test_service", logging_config), + expected_config) + + @patch("ovos_utils.log.get_logs_config") + def test_get_log_path(self, get_config): + from ovos_utils.log import get_log_path + + real_log_path = join(dirname(__file__), "test_logs") + test_paths = [self.test_dir, dirname(__file__), real_log_path] + + # Test with multiple populated directories + self.assertEqual(get_log_path("real", test_paths), real_log_path) + self.assertIsNone(get_log_path("fake", test_paths)) + + # Test path from configuration + get_config.return_value = {"path": self.test_dir} + self.assertEqual(get_log_path("test"), self.test_dir) + get_config.assert_called_once_with(service_name="test") + + @patch('ovos_config.Configuration') + def test_get_log_paths(self, config): + from ovos_utils.log import get_log_paths + + config_no_modules = {"logging": {"logs": {"path": "default_path"}}} + + # Test default config path from Configuration (no module overrides) + config.return_value = config_no_modules + self.assertEqual(get_log_paths(), {"default_path"}) + + # Test services with different configured paths + config_multi_modules = {"logging": {"logs": {"path": "default_path"}, + "module_1": {"path": "path_1"}, + "module_2": {"path": "path_2"}, + "module_3": {"path": "path_1"}}} + self.assertEqual(get_log_paths(config_multi_modules), + {"default_path", "path_1", "path_2"}) + + + @patch('ovos_utils.log.get_log_paths') + def test_get_available_logs(self, get_log_paths): + from ovos_utils.log import get_available_logs + + # Test with specified directories containing logs and other files + real_log_path = join(dirname(__file__), "test_logs") + get_log_paths.return_value = [dirname(__file__), real_log_path] + self.assertEqual(get_available_logs(), ["real"]) + + # Test with no log directories + self.assertEqual(get_available_logs([dirname(__file__)]), []) + get_log_paths.return_value = [] + self.assertEqual(get_available_logs(), []) diff --git a/test/unittests/test_logs/real.log b/test/unittests/test_logs/real.log new file mode 100644 index 00000000..e69de29b From bd975489f5872b5a1867746149a9608063785580 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 2 Sep 2024 15:21:14 +0000 Subject: [PATCH 84/87] Increment Version to --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 975172ee..49ff7a97 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 31 +VERSION_ALPHA = 32 # END_VERSION_BLOCK From 7f7c9d0aca67544fa40158c0c838ce141e9580f9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 2 Sep 2024 15:21:45 +0000 Subject: [PATCH 85/87] Update Changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5edd1162..8291a017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,9 +25,12 @@ **Closed issues:** - "systemctl restart" not working when using with ovos-phal-plugin-system [\#259](https://github.com/OpenVoiceOS/ovos-utils/issues/259) +- add some trivial unit test coverage that log config changes are reacted to [\#253](https://github.com/OpenVoiceOS/ovos-utils/issues/253) +- log.py - New functions are missing test coverage [\#239](https://github.com/OpenVoiceOS/ovos-utils/issues/239) **Merged pull requests:** +- log module unit test coverage [\#260](https://github.com/OpenVoiceOS/ovos-utils/pull/260) ([NeonDaniel](https://github.com/NeonDaniel)) - deprecate/signal utils [\#249](https://github.com/OpenVoiceOS/ovos-utils/pull/249) ([JarbasAl](https://github.com/JarbasAl)) ## [V](https://github.com/OpenVoiceOS/ovos-utils/tree/V) (2024-03-10) From d2613b0cb38cbac8b209541fd9da19b1e7e7c707 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 2 Sep 2024 15:22:35 +0000 Subject: [PATCH 86/87] Increment Version to --- ovos_utils/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 49ff7a97..516076b6 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -1,7 +1,7 @@ # The following lines are replaced during the release process. # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 1 +VERSION_MINOR = 2 VERSION_BUILD = 0 -VERSION_ALPHA = 32 +VERSION_ALPHA = 0 # END_VERSION_BLOCK From 9a47ad10c3c4eb67331d1808da2992f2021986e1 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:44:40 +0100 Subject: [PATCH 87/87] Update version.py fix automation mess up --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 516076b6..316a5fd0 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -1,7 +1,7 @@ # The following lines are replaced during the release process. # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 2 +VERSION_MINOR = 1 VERSION_BUILD = 0 VERSION_ALPHA = 0 # END_VERSION_BLOCK