diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000..12daf2c --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,40 @@ +# This workflow will generate a release distribution and upload it to PyPI + +name: Publish Build and GitHub Release +on: + push: + branches: + - master + +jobs: + tag_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Get Version + run: | + VERSION=$(python setup.py --version) + echo "VERSION=${VERSION}" >> $GITHUB_ENV + - uses: ncipollo/release-action@v1 + with: + token: ${{secrets.GITHUB_TOKEN}} + tag: ${{env.VERSION}} + build_and_publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + + - name: Build Distribution Packages + run: | + python setup.py bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/publish_test_build.yml b/.github/workflows/publish_test_build.yml new file mode 100644 index 0000000..33c3b4c --- /dev/null +++ b/.github/workflows/publish_test_build.yml @@ -0,0 +1,39 @@ +# This workflow will generate a distribution and upload it to PyPI + +name: Publish Alpha Build +on: + push: + branches: + - dev + paths-ignore: + - 'version.py' + +jobs: + build_and_publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: Increment Version + run: | + VER=$(python setup.py --version) + python version_bump.py + - name: Push Version Change + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Increment Version + - name: Build Distribution Packages + run: | + python setup.py bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/pull_master.yml b/.github/workflows/pull_master.yml new file mode 100644 index 0000000..d62646b --- /dev/null +++ b/.github/workflows/pull_master.yml @@ -0,0 +1,19 @@ +# This workflow will generate a PR for changes in dev into master for a skill + +name: Pull to Master +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + pull_changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: pull-request-action + uses: repo-sync/pull-request@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + pr_reviewer: 'neonreviewers' \ No newline at end of file diff --git a/.github/workflows/skill_tests.yml b/.github/workflows/skill_tests.yml new file mode 100644 index 0000000..f03c60b --- /dev/null +++ b/.github/workflows/skill_tests.yml @@ -0,0 +1,67 @@ +# This workflow will run unit tests + +name: Test Skill +on: + pull_request: + workflow_dispatch: + +jobs: + build_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: Build Distribution Packages + run: | + python setup.py bdist_wheel + neon_core: + strategy: + matrix: + python-version: [ 3.7, 3.8, 3.9, '3.10' ] + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v2 + - name: Set up python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + sudo apt update + sudo apt install -y gcc libfann-dev swig libssl-dev portaudio19-dev git libpulse-dev + pip install --upgrade pip + pip install pytest mock git+https://github.com/NeonGeckoCom/NeonCore#egg=neon_core + pip install -r requirements/requirements.txt + pip install -r requirements/test_requirements.txt + - name: Test Skill + run: | + pytest test/test_skill.py + ovos-core: + strategy: + matrix: + python-version: [ 3.8 ] + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v2 + - name: Set up python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + sudo apt install -y gcc libfann-dev swig libssl-dev portaudio19-dev git + pip install --upgrade pip + pip install ovos-core[skills] pytest mock + pip install -r requirements/requirements.txt + pip install -r requirements/test_requirements.txt + - name: Test Skill + run: | + pytest test/test_skill.py \ No newline at end of file diff --git a/.github/workflows/update_skill_json.yml b/.github/workflows/update_skill_json.yml new file mode 100644 index 0000000..5dc9797 --- /dev/null +++ b/.github/workflows/update_skill_json.yml @@ -0,0 +1,33 @@ +# This workflow will run unit tests + +name: Update skill.json +on: + push: + +jobs: + update_skill_json: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + with: + path: action/skill/ + - name: Set up python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Dependencies + run: | + sudo apt update + sudo apt install -y gcc git libpulse-dev + pip install --upgrade pip + pip install neon-utils\~=0.17 ovos-skills-manager + - name: Get Updated skill.json + run: | + python action/skill/scripts/update_skill_json.py + - name: Push skill.json Change + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Update skill.json + repository: action/skill/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f21b54 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/venv/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..cc28779 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# BSD-3 License + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 2307d99..eed73de 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ -# malls-parser-skill -Skill for parsing store name and location or a requested product/store +# {Malls parser skill neon} +## Summary +Skill for mall parsing + +## Description +Skill parses mall web page and returns user name, location and work hours of requested shop, store + +## Examples +- where is apple? +- where can I find ABC stores? + +## Contact Support +Use the [link](https://neongecko.com/ContactUs) or [submit an issue on GitHub](https://help.github.com/en/articles/creating-an-issue) + +## Credits + +[NeonGeckoCom](https://github.com/NeonGeckoCom) +[NeonMariia](https://github.com/neonmariia) + +## Category +**Information** diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e6acc2f --- /dev/null +++ b/__init__.py @@ -0,0 +1,381 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from neon_utils.skills.neon_skill import NeonSkill, LOG +from mycroft.skills.core import intent_file_handler +from .request_handling import RequestHandler +from .request_handling import existing_lang_check, get_shop_data,\ + shop_selection_by_floors,\ + location_format,\ + curent_time_extraction +import re + + + +class DirectorySkill(NeonSkill): + + def __init__(self): + super(DirectorySkill, self).__init__(name="DirectorySkill") + self.url = "https://www.alamoanacenter.com/en/directory/" + + + def initialize(self): + # When first run or prompt not dismissed, wait for load and prompt user + if self.settings.get('prompt_on_start'): + self.bus.once('mycroft.ready', self._start_mall_parser_prompt) + + @intent_file_handler("run_mall_parser.intent") + def start_mall_parser_intent(self, message): + LOG.info(message.data) + self._start_mall_parser_prompt(message) + return + + # @property + def mall_link(self): + mall_link = 'https://www.alamoanacenter.com/' + return self.settings.get("mall_link") or mall_link + + def user_request_handling(self, message): + """ + Checks user language existence on mall's web-page + using existing_lang_check() function. + Returns: + None, None: if message is empty + None, None: if language is not supported + user_request, link (str, str): if language exists + answer) + """ + LOG.info(f"Message is {message.data}") + if message.data == {} or message is None: + return None, None + else: + request_lang = self.lang.split('-')[0] + user_request = message.data['shop'] + LOG.info(f"{self.mall_link()}") + LOG.info(str(request_lang)) + LOG.info(user_request) + found, link = existing_lang_check(request_lang, self.mall_link()) + if found: + link = self.mall_link()+request_lang+'/directory/' + LOG.info('new link: '+ link) + return user_request, link + else: + self.speak_dialog("no_lang") + return None, None + + def start_again(self): + """ + Asks yes/no question whether user wants to + get another shop info, after Neon gave the + information about previously selected shop. + If user's answer 'yes': asks what shop is + needed. Returns user's answer. + If 'no', speaks corresponding dialog. + If some other answer, speaks corresponding + dialog + Returns: + None (if no shop in request, if user's + answer is 'no', if user gives some other + answer) + """ + start_again = self.ask_yesno("ask_more") + if start_again == "yes": + another_shop = self.get_response('another_shop') + if another_shop is not None: + LOG.info(f'another shop {another_shop}') + return another_shop + elif start_again == "no": + self.speak_dialog('no_shop_request') + else: + self.speak_dialog('unexpected_error') + return None + + def speak_shops(self, shop_info): + """ + Speaks shop info that was found. + Substitutes time format for better pronunciation. + speak_dialog('found_shop', {"name": shop['name'], "hours": hours, "location": location}) + Shows shop label image in gui. + Args: + shop_info (list): found shops on user's + request + """ + for shop in shop_info: + LOG.info(shop) + location = location_format(shop['location']) + hours = re.sub('(\d+)am.+(\d+)pm', r'from \1 A M to \2 P M', shop['hours']) + self.speak_dialog('found_shop', {"name": shop['name'], "hours": hours, "location": location}) + LOG.info({"name": shop['name'], "hours": hours, "location": location}) + self.gui.show_image(shop['logo'], caption=f'{hours} {location}', title=shop['name']) + + def location_selection(self, shop_info): + """ + If there are several shops in found shops list + and user wants to get shop info on the certain + floor. If shop on that floor exists speaks + this shop info. Else speaks all shops info. + Args: + shop_info (list): found shops on user's + request + Returns: + 3, None (to ask for another shop info) + """ + LOG.info(f"Shop by location selection {shop_info}") + floor = self.get_response('which_floor') + shops = shop_selection_by_floors(floor, shop_info) + if shops: + self.speak_shops(shops) + else: + self.speak_dialog('no_shop_on_level') + self.speak_shops(shop_info) + return 3, None + + def open_shops_search(self, shop_info, day_time, hour, min): + """ + Selects open shops. Collects the list of + open shops else return empty list. + Args: + shop_info (list): found shops on user's + request + Returns: + shop_info (list): open shops + """ + open_shops = [] + LOG.info(f"User's time {day_time, hour, min}") + for shop in shop_info: + parse_time = re.findall(r'(\d+)+[am|pm]', shop['hours']) + LOG.info(f'Parsed time {parse_time}') + open_time = int(parse_time[0]) + close_time = int(parse_time[1]) + if open_time <= hour < close_time: + open_shops.append(shop) + elif day_time[1] == 'am' and open_time <= hour: + open_shops.append(shop) + return open_shops + + + def time_calculation(self, shop_info, open, day_time, hour, min): + # add logic if shop opens and closes not at am-pm time period + # change to speak dialog + """ + Calculates time difference between user's current time + and shop working hours. + If 'open' argument is True: + If user one hour or less before closing: speaks how + many minutes left. Speaks shop info. + Else speaks corresponging dialog. + Speaks shop info. + If 'open' argument is False: + Speaks corresponding dialog. + If user is one hour or less before opening hours + speaks how much time is left for waiting. + If user's time is 'am' and user is before opening + hours, speaks how many hours and minutes left + waiting. + If user's time is evening (pm) speaks when the shop + opens in the morning. + Speaks shop info. + Args: + shop_info (list): found shops on user's request + open (boolean): True - if shop is open + day_time (str): user's current day time (am|pm) + hour (int): user's current hour + min (int): user's current minute + Returns: + 3, None (to ask for another shop info) + Examples: + work time 9am-10pm + user's time 8am + Prompt: 'Shop is closed now. Opens in 1 hour' + """ + for shop in shop_info: + work_time = shop['hours'] + normalized_time = re.findall(r'(\d+)[am|pm]', work_time) + open_time = int(normalized_time[0]) + close_time = int(normalized_time[1]) + LOG.info(f'work_time {work_time}') + shop_name = shop['name'] + parse_time = work_time.split('-') + LOG.info(f'parse_time {parse_time}') + # time left + wait_h = open_time - hour - 1 + wait_min = 60 - min + if open: + if day_time[1] == 'pm' and 0 < (close_time - hour) <= 1: + LOG.info(f'{shop_name} closes in {wait_min} minutes.') + self.speak_dialog('closing_minutes', {"shop_name": shop_name, "wait_min": wait_min}) + else: + LOG.info(f'{shop_name} is open.') + self.speak_dialog('open_now', {'shop_name': shop_name}) + LOG.info([shop]) + self.speak_shops([shop]) + else: + if day_time[1] == 'am' and hour < open_time: + if wait_h == 0: + LOG.info(f'{shop_name} is closed now. Opens in {wait_min} minutes') + self.speak_dialog('opening_minutes', {"shop_name": shop_name, "wait_min": wait_min}) + else: + LOG.info(f'{shop_name} is closed now. Opens in {wait_h} hour and {wait_min} minutes') + self.speak_dialog('opening_hours', {"shop_name": shop_name, "wait_h": wait_h, "wait_min": wait_min}) + elif hour >= close_time: + LOG.info(f'{shop_name} is closed now. Shop opens at {open_time}') + self.speak_dialog('closed_now', {'shop_name': shop_name, 'open_time': open_time}) + LOG.info([shop]) + self.speak_shops([shop]) + return 3, None + + def shops_by_time_selection(self, shop_info): + """ + If user chose to select shops by time or + use like default selection. Gets user's + current time. Selects open shops. + Args: + shop_info (list): found shops on user's + request + Returns: + time_calculation function with True + in 'open' argument. + time_calculation function with False + in 'open' argument. (if list + of open shops is 0) + + """ + LOG.info(f"Shop by time selection {shop_info}") + day_time, hour, min = curent_time_extraction() + # day_time, hour, min = ['11:15', 'pm'], 11, 15 + open_shops = self.open_shops_search(shop_info, day_time, hour, min) + if len(open_shops) >= 1: + return self.time_calculation(open_shops, True, day_time, hour, min) + else: + return self.time_calculation(shop_info, False, day_time, hour, min) + + def find_shop(self, user_request, mall_link): + """ + When the intent is matched, user_request + variable contains the name of the shop. + The matching function get_shop_data() is + used to find the shop name in cache or + on the mall page. + If user's request is not None this function + can return several shops, one shop or empty + list. + If no shop was found asks user to repeat. + returns 1, user_request to continue the + execution loop in self.execute(). + If there are several shops asks user what way + of sorting to choose: time, level, nothing. + If 'time' - finds open shops. If open shops + list is not empty speaks open shops, else + tells time difference between user and shops' + work hours. + If 'location' - asks what level user is interested + in. If shops were found speaks shops' info, + else tells that there is no shop on that level + and speaks all found shops. + If 'no' - sorts by time. + If nothing matched in the answer - sorts by time. + If there was one shop found speaks this + shop info. Returns 3, None to stop current + shop search. + Location and time sorting functions return + 3, None to stop current shop search. + """ + LOG.info(f'user_request {user_request}') + LOG.info(f'mall_link {mall_link}') + if user_request is not None: + self.speak_dialog("start_parsing") + LOG.info(f"I am parsing shops and malls for your request") + file_path = self.file_system.path + LOG.info(f'file_path {file_path}') + shop_info = get_shop_data(mall_link, user_request, file_path) + LOG.info(f"I found {len(shop_info)} shops") + LOG.info(f"shop list: {shop_info}") + if len(shop_info) == 0: + user_request = self.get_response('shop_not_found') + return 1, user_request + elif len(shop_info) > 1: + self.speak_dialog('more_than_one') + # ask for the way of selection: time, location, nothing + sorting_selection = self.get_response('choose_selection') + if sorting_selection: + LOG.info(f'Users answer on sorting options: {sorting_selection}') + if self.voc_match(sorting_selection, "time"): + LOG.info('Time sorting selected') + return self.shops_by_time_selection(shop_info) + elif self.voc_match(sorting_selection, "location"): + LOG.info('Location sorting selected') + return self.location_selection(shop_info) + elif self.voc_match(sorting_selection, "no"): + LOG.info('No sorting selected. Sorting by time on default.') + return self.shops_by_time_selection(shop_info) + else: + LOG.info('Nothing matched. Sorting by time on default.') + return self.shops_by_time_selection(shop_info) + else: + LOG.info(f"found shop {shop_info}") + self.speak_shops(shop_info) + return 3, None + + def execute(self, user_request, mall_link): + count = 0 + LOG.info('Start execute') + while count < 3 and user_request is not None and mall_link is not None: + new_count, user_request = self.find_shop(user_request, mall_link) + count = count + new_count + user_request = self.start_again() + LOG.info(str(user_request)) + if user_request is not None: + LOG.info('New execution') + self.execute(user_request, mall_link) + else: + return None + + def _start_mall_parser_prompt(self, message): + if self.neon_in_request(message): + LOG.info('Prompting Mall parsing start') + self.make_active() + if message is not None: + LOG.info('new message'+str(message)) + user_request, mall_link = self.user_request_handling(message) + LOG.info(mall_link) + if user_request is not None: + if self.execute(user_request, mall_link) is not None: + LOG.info('executed') + return + else: + self.speak_dialog('finished') + else: + self.speak_dialog('finished') + else: + return + + + +def create_skill(): + return DirectorySkill() diff --git a/locale/en-us/dialog/en/all_locations.dialog b/locale/en-us/dialog/en/all_locations.dialog new file mode 100644 index 0000000..fbffb2a --- /dev/null +++ b/locale/en-us/dialog/en/all_locations.dialog @@ -0,0 +1 @@ +Here are all possible shop locations. \ No newline at end of file diff --git a/locale/en-us/dialog/en/another_shop.dialog b/locale/en-us/dialog/en/another_shop.dialog new file mode 100644 index 0000000..be4fbfd --- /dev/null +++ b/locale/en-us/dialog/en/another_shop.dialog @@ -0,0 +1 @@ +What shop you are looking for? \ No newline at end of file diff --git a/locale/en-us/dialog/en/ask_more.dialog b/locale/en-us/dialog/en/ask_more.dialog new file mode 100644 index 0000000..b2b8db3 --- /dev/null +++ b/locale/en-us/dialog/en/ask_more.dialog @@ -0,0 +1 @@ +Do you want to get another shop info? \ No newline at end of file diff --git a/locale/en-us/dialog/en/choose_selection.dialog b/locale/en-us/dialog/en/choose_selection.dialog new file mode 100644 index 0000000..23ac161 --- /dev/null +++ b/locale/en-us/dialog/en/choose_selection.dialog @@ -0,0 +1 @@ +Do you want to select store by work hours or location? \ No newline at end of file diff --git a/locale/en-us/dialog/en/closed_now.dialog b/locale/en-us/dialog/en/closed_now.dialog new file mode 100644 index 0000000..d095784 --- /dev/null +++ b/locale/en-us/dialog/en/closed_now.dialog @@ -0,0 +1 @@ +{{shop_name}} is closed now. Opens at {{open_time}}. \ No newline at end of file diff --git a/locale/en-us/dialog/en/closing_minutes.dialog b/locale/en-us/dialog/en/closing_minutes.dialog new file mode 100644 index 0000000..46d9f8b --- /dev/null +++ b/locale/en-us/dialog/en/closing_minutes.dialog @@ -0,0 +1 @@ +{{shop_name}} is open now. Closes in {{wait_min}} minutes. \ No newline at end of file diff --git a/locale/en-us/dialog/en/finished.dialog b/locale/en-us/dialog/en/finished.dialog new file mode 100644 index 0000000..17ed73d --- /dev/null +++ b/locale/en-us/dialog/en/finished.dialog @@ -0,0 +1 @@ +Finished. Goodbye! \ No newline at end of file diff --git a/locale/en-us/dialog/en/found_shop.dialog b/locale/en-us/dialog/en/found_shop.dialog new file mode 100644 index 0000000..d667791 --- /dev/null +++ b/locale/en-us/dialog/en/found_shop.dialog @@ -0,0 +1 @@ +I found {{name}} with hours {{hours}}. You can find this store on {{location}}. \ No newline at end of file diff --git a/locale/en-us/dialog/en/more_than_one.dialog b/locale/en-us/dialog/en/more_than_one.dialog new file mode 100644 index 0000000..70532f4 --- /dev/null +++ b/locale/en-us/dialog/en/more_than_one.dialog @@ -0,0 +1 @@ +I found more than one shop in your request. \ No newline at end of file diff --git a/locale/en-us/dialog/en/no_lang.dialog b/locale/en-us/dialog/en/no_lang.dialog new file mode 100644 index 0000000..d9d0b6e --- /dev/null +++ b/locale/en-us/dialog/en/no_lang.dialog @@ -0,0 +1 @@ +Sorry there is no mall info in your language. \ No newline at end of file diff --git a/locale/en-us/dialog/en/no_shop_on_level.dialog b/locale/en-us/dialog/en/no_shop_on_level.dialog new file mode 100644 index 0000000..69b5d6d --- /dev/null +++ b/locale/en-us/dialog/en/no_shop_on_level.dialog @@ -0,0 +1 @@ +There is no requested shop on your level. Here are all possible locations. \ No newline at end of file diff --git a/locale/en-us/dialog/en/no_shop_request.dialog b/locale/en-us/dialog/en/no_shop_request.dialog new file mode 100644 index 0000000..6ca56f4 --- /dev/null +++ b/locale/en-us/dialog/en/no_shop_request.dialog @@ -0,0 +1 @@ +Okay. I will stop mall parsing. \ No newline at end of file diff --git a/locale/en-us/dialog/en/open_now.dialog b/locale/en-us/dialog/en/open_now.dialog new file mode 100644 index 0000000..3421d4c --- /dev/null +++ b/locale/en-us/dialog/en/open_now.dialog @@ -0,0 +1 @@ +{{shop_name}} is open. \ No newline at end of file diff --git a/locale/en-us/dialog/en/opening_hours.dialog b/locale/en-us/dialog/en/opening_hours.dialog new file mode 100644 index 0000000..3bcb8ce --- /dev/null +++ b/locale/en-us/dialog/en/opening_hours.dialog @@ -0,0 +1 @@ +{{shop_name}} is closed now. Opens in {{wait_h}} hour and {{wait_min}} minutes' \ No newline at end of file diff --git a/locale/en-us/dialog/en/opening_minutes.dialog b/locale/en-us/dialog/en/opening_minutes.dialog new file mode 100644 index 0000000..ce55fcc --- /dev/null +++ b/locale/en-us/dialog/en/opening_minutes.dialog @@ -0,0 +1 @@ +{{shop_name}} is closed now. Opens in {{wait_min}} minutes. \ No newline at end of file diff --git a/locale/en-us/dialog/en/shop_by_floor.dialog b/locale/en-us/dialog/en/shop_by_floor.dialog new file mode 100644 index 0000000..97c1ac0 --- /dev/null +++ b/locale/en-us/dialog/en/shop_by_floor.dialog @@ -0,0 +1 @@ +Do you want to get shop info by floor? \ No newline at end of file diff --git a/locale/en-us/dialog/en/shop_not_found.dialog b/locale/en-us/dialog/en/shop_not_found.dialog new file mode 100644 index 0000000..859f286 --- /dev/null +++ b/locale/en-us/dialog/en/shop_not_found.dialog @@ -0,0 +1,2 @@ +Sorry, this store doesn't exist in this mall. I am not sure I've understood what you said. Repeat, please. +Sorry, I can not find this shop. I am not sure I've understood what you said. Repeat, please. \ No newline at end of file diff --git a/locale/en-us/dialog/en/start_parsing.dialog b/locale/en-us/dialog/en/start_parsing.dialog new file mode 100644 index 0000000..4071f9b --- /dev/null +++ b/locale/en-us/dialog/en/start_parsing.dialog @@ -0,0 +1 @@ +I am parsing shops and malls for your request. \ No newline at end of file diff --git a/locale/en-us/dialog/en/stop.dialog b/locale/en-us/dialog/en/stop.dialog new file mode 100644 index 0000000..2874c27 --- /dev/null +++ b/locale/en-us/dialog/en/stop.dialog @@ -0,0 +1 @@ +Do you want to exit the mall directory? \ No newline at end of file diff --git a/locale/en-us/dialog/en/unexpected_error.dialog b/locale/en-us/dialog/en/unexpected_error.dialog new file mode 100644 index 0000000..aed994a --- /dev/null +++ b/locale/en-us/dialog/en/unexpected_error.dialog @@ -0,0 +1 @@ +Sorry, I'm not sure what you mean. \ No newline at end of file diff --git a/locale/en-us/dialog/en/which_floor.dialog b/locale/en-us/dialog/en/which_floor.dialog new file mode 100644 index 0000000..4333de5 --- /dev/null +++ b/locale/en-us/dialog/en/which_floor.dialog @@ -0,0 +1,3 @@ +Which floor are you staying on? +On which floor are you? +what is your floor? \ No newline at end of file diff --git a/locale/en-us/run_mall_parser.intent b/locale/en-us/run_mall_parser.intent new file mode 100644 index 0000000..ccbf4d9 --- /dev/null +++ b/locale/en-us/run_mall_parser.intent @@ -0,0 +1,8 @@ +where (is|are|can i find) {shop} +where (is|are) {shop} located +i am looking for {shop} +location of {shop} +where {shop} +show {shop} +show me {shop} +find {shop} \ No newline at end of file diff --git a/locale/en-us/vocab/en/location.voc b/locale/en-us/vocab/en/location.voc new file mode 100644 index 0000000..c96c5a1 --- /dev/null +++ b/locale/en-us/vocab/en/location.voc @@ -0,0 +1,3 @@ +location +floor +level \ No newline at end of file diff --git a/locale/en-us/vocab/en/no.voc b/locale/en-us/vocab/en/no.voc new file mode 100644 index 0000000..f5b08b0 --- /dev/null +++ b/locale/en-us/vocab/en/no.voc @@ -0,0 +1,5 @@ +not +no +nope +none +don't \ No newline at end of file diff --git a/locale/en-us/vocab/en/time.voc b/locale/en-us/vocab/en/time.voc new file mode 100644 index 0000000..48d1c38 --- /dev/null +++ b/locale/en-us/vocab/en/time.voc @@ -0,0 +1,4 @@ +time +hour +hours +working hours \ No newline at end of file diff --git a/locale/en-us/vocab/en/yes.voc b/locale/en-us/vocab/en/yes.voc new file mode 100644 index 0000000..d9ea0c3 --- /dev/null +++ b/locale/en-us/vocab/en/yes.voc @@ -0,0 +1,4 @@ +yes +yeah +yep +aha diff --git a/request_handling.py b/request_handling.py new file mode 100644 index 0000000..049e56a --- /dev/null +++ b/request_handling.py @@ -0,0 +1,255 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from urllib.error import HTTPError +import requests +import bs4 +from neon_utils.skills.neon_skill import LOG +import urllib.request + +import lingua_franca +from lingua_franca.format import pronounce_number +lingua_franca.load_language('en') + +import re +import os +import json + +from datetime import datetime + + +class RequestHandler(): + + caching_file = '' + +def find_cached_stores(user_request: str, url, file_path): + """ + Check shop name existence in cache keys + Args: + user_request (str): shop from user's message + Returns: + if file is empty -> None, {} + if shop wasn't found -> None, read data + if shop found -> store_info (list), read data + Examples: + [ + {"name": "ABS stores", "time": "8am-10pm", "location": "1 level"}, + {"name": "ABS stores", "time": "8am-10pm", "location": "2 level"} + ] + """ + caching_file = file_path+'/cached_stores.json' + if os.path.isfile(caching_file) == False: + LOG.info("Cache file doesn't exist") + caching_stores_in_mall(file_path, url) + return find_cached_stores(user_request, url, file_path) + else: + with open(caching_file, 'r', encoding='utf-8') as readfile: + data = json.load(readfile) + found_key = [key for key in data.keys() + if key.lower() in user_request.lower() + or user_request.lower() in key.lower()] + LOG.info(f'found key {found_key}') + if len(found_key) >=1 : + store_name = str(found_key[0]) + LOG.info(f'Shop exists {data[store_name]}') + return data[store_name], data + else: + LOG.info("Shop doesn't exist in cache") + return None, data + +def caching_stores_in_mall(file_path, url): + """ + Creates caching file in the current class. + Creates empty dictionary for cache. Parses + all shops info. Creates dict key from shop + name. Value list of dicts with current shop + info. + If shop name already exists in created dict + append current shop dict to existing + list. + Writes created dict to created JSON file. + Args: + file_path (str): new file path + url (str): malls url + Examples: + {"ABS stores": [ + {"name": "ABS stores", "time": "8am-10pm", "location": "1 level"}, + {"name": "ABS stores", "time": "8am-10pm", "location": "2 level"} + ]} + """ + caching_file = file_path+'/cached_stores.json' + LOG.info(f'caching_file {caching_file}') + shop_cache = {} + soup = parse(url) + for shop in soup.find_all(attrs={"class": "directory-tenant-card"}): + logo = shop.find_next("img").get('src') + info = shop.find_next(attrs={"class": "tenant-info-container"}) + name = info.find_next(attrs={"class": "tenant-info-row"}).text.strip().strip('\n') + hours = info.find_next(attrs={"class": "tenant-hours-container"}).text.strip('\n') + location = info.find_next(attrs={"tenant-location-container"}).text.strip('\n') + shop_data = {'name': name, 'hours': hours, 'location': location, 'logo': logo} + if name in shop_cache.keys(): + shop_cache[name].append(shop_data) + else: + shop_cache[name] = [shop_data] + with open(caching_file, + 'w+') as outfile: + json.dump(shop_cache, outfile, ensure_ascii=False) + os.chmod(caching_file, 777) + LOG.info("Created mall's cache") + +def existing_lang_check(user_lang: str, url): + """ + Check existence of user's language + on the mall web-page + Args: + user_lang (str): user's lang in ISO 639-1 + Returns: + bool: True if lang exists + """ + link = url+user_lang+'/directory/' + response = requests.get(link) + if response.status_code == 200: + LOG.info('This language is supported') + return True, link + else: + LOG.info('This language is not supported') + return False, link + +def curent_time_extraction(): + """ + Defines current time in utc timezone + Format: hour:minutes part of day (1:23 pm) + + Returns: + day_time (list): contains splited time + numerals and part of the day + day_time -> ['07:19', 'am'] + hour (int): current hour + min (int): current minute + """ + now = datetime.now().time().strftime("%I:%M %p") + # now = datetime.today().strftime("%H:%M %p") + LOG.info(f'now {now}') + day_time = now.lower().split(' ') + exact_time = day_time[0].split(':') + hour, min = int(exact_time[0]), int(exact_time[1]) + return day_time, hour, min + +def location_format(location): + """ + Finds all digits in store's location and + formats them to numeral words. + Args: + location (str): location info + from shops info + Returns: + if digits were found: + pronounced (str): utterance with + pronounced digits + else: + location (str): not changed utterance + Examples: + 'level 1' -> 'level one' + """ + floor = re.findall(r'\d+', location) + if len(floor) > 0: + floor = floor[0] + num = pronounce_number(int(floor), ordinals=False) + pronounced = re.sub(r'\d+', num, location) + return pronounced + else: + return location + +def shop_selection_by_floors(user_request, found_shops): + """ + If there are several shops in found shops list + and user agrees to select shop by floor. + Finds all digits in store's location and + formats them to ordinal and cardinal numerals. + Matches formated numerals with user's request. + If shop was found appends it to the new found + list. + Args: + user_request (str): floor from user + found_shops (list): found shops on user's + request + Returns: + shops_by_floor (list): shops that was found by floor + """ + shops_by_floor = [] + for shop in found_shops: + numbers = re.findall(r'\d+', shop['location']) + if len(numbers) > 0: + numbers = numbers[0] + num = pronounce_number(int(numbers), ordinals=False) + num_ordinal = pronounce_number(int(numbers), ordinals=True) + if num in user_request or num_ordinal in user_request: + shops_by_floor.append(shop) + return shops_by_floor + +def parse(url): + headers = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36' + } + request = urllib.request.Request(url, + headers=headers) + try: + with urllib.request.urlopen(request) as page: + soup = bs4.BeautifulSoup(page.read(), features='lxml') + return soup + except HTTPError: + LOG.info("Failed url parsing") + + +def get_shop_data(url, user_request, file_path): + """ + Check existence of user's request store in cache + if shop was found returns list with shop info, + else does parsing of mall's web-page. + Matches the name of existing stores with user's + request. If store was found, returns list with + stores' info and does caching, else returns empty + list. + on the mall web-page + Args: + url (str): mall link from hardcoded in init.py + user_request (str): utterance from stt parsing + Returns: + : found_shops (list): found shops' info + """ + # search for store existence in cache + LOG.info(file_path) + found_shops, data = find_cached_stores(user_request, url, file_path) + LOG.info(found_shops) + if found_shops: + LOG.info(f"found_shops: {found_shops}") + return found_shops + else: + return [] + diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..5a2a1bd --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,6 @@ +numpy +neon-utils~=1.0 +bs4 +requests +ovos-lingua-franca +datetime diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/requirements/test_requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/scripts/update_skill_json.py b/scripts/update_skill_json.py new file mode 100644 index 0000000..9e56817 --- /dev/null +++ b/scripts/update_skill_json.py @@ -0,0 +1,57 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import json +from os.path import dirname, join +from pprint import pprint +from neon_utils.packaging_utils import build_skill_spec + +skill_dir = dirname(dirname(__file__)) + + +def get_skill_json(): + print(f"skill_dir={skill_dir}") + skill_json = join(skill_dir, "skill.json") + skill_spec = build_skill_spec(skill_dir) + pprint(skill_spec) + try: + with open(skill_json) as f: + current = json.load(f) + except Exception as e: + print(e) + current = None + if current != skill_spec: + print("Skill Updated. Writing skill.json") + with open(skill_json, 'w+') as f: + json.dump(skill_spec, f, indent=4) + else: + print("No changes to skill.json") + + +if __name__ == "__main__": + get_skill_json() diff --git a/settingsmeta.yml b/settingsmeta.yml new file mode 100644 index 0000000..cb6b7e8 --- /dev/null +++ b/settingsmeta.yml @@ -0,0 +1,9 @@ + +skillMetadata: + sections: + - name: Mall Parsing + fields: + - name: prompt_on_start + type: checkbox + label: Start Mall Parsing + value: "true" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..10759cd --- /dev/null +++ b/setup.py @@ -0,0 +1,100 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from setuptools import setup +from os import getenv, path, walk + +SKILL_NAME = "skill-directory" +SKILL_PKG = SKILL_NAME.replace('-', '_') +# skill_id=package_name:SkillClass +PLUGIN_ENTRY_POINT = f'{SKILL_NAME}.neongeckocom={SKILL_PKG}:DirectorySkill' + + +def get_requirements(requirements_filename: str): + requirements_file = path.join(path.abspath(path.dirname(__file__)), + requirements_filename) + with open(requirements_file, 'r', encoding='utf-8') as r: + requirements = r.readlines() + requirements = [r.strip() for r in requirements if r.strip() + and not r.strip().startswith("#")] + + for i in range(0, len(requirements)): + r = requirements[i] + if "@" in r: + parts = [p.lower() if p.strip().startswith("git+http") else p + for p in r.split('@')] + r = "@".join(parts) + if getenv("GITHUB_TOKEN"): + if "github.com" in r: + requirements[i] = \ + r.replace("github.com", + f"{getenv('GITHUB_TOKEN')}@github.com") + return requirements + + +def find_resource_files(): + resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex") + base_dir = path.dirname(__file__) + package_data = ["skill.json"] + for res in resource_base_dirs: + if path.isdir(path.join(base_dir, res)): + for (directory, _, files) in walk(path.join(base_dir, res)): + if files: + package_data.append( + path.join(directory.replace(base_dir, "").lstrip('/'), + '*')) + return package_data + + +with open("README.md", "r") as f: + long_description = f.read() + +with open("./version.py", "r", encoding="utf-8") as v: + for line in v.readlines(): + if line.startswith("__version__"): + if '"' in line: + version = line.split('"')[1] + else: + version = line.split("'")[1] + +setup( + name=f"neon-{SKILL_NAME}", + version=version, + url=f'https://github.com/NeonGeckoCom/{SKILL_NAME}', + license='BSD-3-Clause', + install_requires=get_requirements("requirements/requirements.txt"), + author='Neongecko', + author_email='developers@neon.ai', + long_description=long_description, + long_description_content_type="text/markdown", + package_dir={SKILL_PKG: ""}, + packages=[SKILL_PKG], + package_data={SKILL_PKG: find_resource_files()}, + include_package_data=True, + entry_points={"ovos.plugin.skill": PLUGIN_ENTRY_POINT} +) diff --git a/skill.json b/skill.json new file mode 100644 index 0000000..1394d21 --- /dev/null +++ b/skill.json @@ -0,0 +1,49 @@ +{ + "title": "{Malls parser skill neon}", + "url": "https://github.com/NeonGeckoCom/skill-directory", + "summary": "Skill for mall parsing", + "short_description": "Skill for mall parsing", + "description": "Skill parses mall web page and returns user name, location and work hours of requested shop, store", + "examples": [ + "where is apple?", + "where can i find abc stores?" + ], + "desktopFile": false, + "warning": "", + "systemDeps": false, + "requirements": { + "python": [ + "bs4", + "datetime", + "neon-utils~=1.0", + "numpy", + "ovos-lingua-franca", + "requests" + ], + "system": {}, + "skill": [] + }, + "incompatible_skills": [], + "platforms": [ + "i386", + "x86_64", + "ia64", + "arm64", + "arm" + ], + "branch": "master", + "license": "BSD-3-Clause", + "icon": "https://0000.us/klatchat/app/files/neon_images/icons/neon_skill.png", + "category": "Information", + "categories": [ + "Information" + ], + "tags": [], + "credits": [ + "NeonGeckoCom", + "NeonMariia" + ], + "skillname": "skill-directory", + "authorname": "NeonGeckoCom", + "foldername": null +} \ No newline at end of file diff --git a/test/test_skill.py b/test/test_skill.py new file mode 100644 index 0000000..411ad8f --- /dev/null +++ b/test/test_skill.py @@ -0,0 +1,119 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import shutil +import unittest + +from os import mkdir +from os.path import dirname, join, exists +from mock import Mock +from ovos_utils.messagebus import FakeBus + +from mycroft.skills.skill_loader import SkillLoader + +from mycroft_bus_client import Message + + +class TestSkill(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + bus = FakeBus() + bus.run_in_thread() + skill_loader = SkillLoader(bus, dirname(dirname(__file__))) + skill_loader.load() + cls.skill = skill_loader.instance + + # Define a directory to use for testing + cls.test_fs = join(dirname(__file__), "skill") + if not exists(cls.test_fs): + mkdir(cls.test_fs) + + # Override the configuration and fs paths to use the test directory + cls.skill.settings_write_path = cls.test_fs + cls.skill.file_system.path = cls.test_fs + cls.skill._init_settings() + cls.skill.initialize() + + # Override speak and speak_dialog to test passed arguments + cls.skill.speak = Mock() + cls.skill.speak_dialog = Mock() + + # TODO: Put any skill method overrides here + + def setUp(self): + self.skill.speak.reset_mock() + self.skill.speak_dialog.reset_mock() + + # TODO: Put any cleanup here that runs before each test case + + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(cls.test_fs) + + def test_en_skill_init(self): + self.skill.ask_yesno = Mock(return_value="yes") + self.skill.gui._pages2uri = Mock() + self.skill._start_mall_parser_prompt( + Message('test', {'utterance': 'find ABC stores', + 'shop': 'ABC stores', + 'lang': 'en-us'}, + {'context_key': 'MallParsing'}) + ) + + message = Message('test', {'utterance': 'find ABC stores', + 'shop': 'ABC stores', + 'lang': 'en-us'}, + {'context_key': 'MallParsing'}) + self.skill.user_request_handling(message) + + def test_en_time_extraction(self): + shop_info = [{'name': 'ABC Stores', 'hours': '9am – 9pm', 'location': 'Street Level 1, near Centerstage', 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937914061-abcstores.png'}, + {'name': 'ABC Stores', 'hours': '10am – 8pm', 'location': 'Street Level 1, in the Ewa Wing', 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937946329-abcstores.png'}] + day_time, hour, min = ['10:15', 'am'], 10, 15 + result_shops = self.skill.open_shops_search(shop_info, day_time, hour, min) + self.assertEqual(shop_info, result_shops) + + day_time, hour, min = ['9:15', 'am'], 9, 15 + result_shops = self.skill.open_shops_search(shop_info, day_time, hour, min) + self.assertEqual(shop_info[0], result_shops[0]) + + # def test_en_time_extraction(self): + # shop_info = [{'name': 'ABC Stores', 'hours': '9am – 9pm', 'location': 'Street Level 1, near Centerstage', 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937914061-abcstores.png'}, + # {'name': 'ABC Stores', 'hours': '10am – 8pm', 'location': 'Street Level 1, in the Ewa Wing', 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937946329-abcstores.png'}, + # {'name': 'ABC Stores', 'hours': '10am – 9pm', 'location': 'Street Level 1, in the Ewa Wing', 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937946329-abcstores.png'}] + + # day_time, hour, min = ['9:15', 'pm'], 9, 15 + # print(self.skill.time_calculation(shop_info, False, day_time, hour, min)) + + # day_time, hour, min = ['8:15', 'pm'], 8, 15 + # print(self.skill.time_calculation(shop_info, False, day_time, hour, min)) + + +if __name__ == '__main__': + unittest.main() diff --git a/version.py b/version.py new file mode 100644 index 0000000..caf633f --- /dev/null +++ b/version.py @@ -0,0 +1,29 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +__version__ = "0.0.1a3" diff --git a/version_bump.py b/version_bump.py new file mode 100644 index 0000000..5a01c55 --- /dev/null +++ b/version_bump.py @@ -0,0 +1,54 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import fileinput +from os.path import join, dirname + +with open(join(dirname(__file__), "version.py"), "r", encoding="utf-8") as v: + for line in v.readlines(): + if line.startswith("__version__"): + if '"' in line: + version = line.split('"')[1] + else: + version = line.split("'")[1] + +if "a" not in version: + parts = version.split('.') + parts[-1] = str(int(parts[-1]) + 1) + version = '.'.join(parts) + version = f"{version}a0" +else: + post = version.split("a")[1] + new_post = int(post) + 1 + version = version.replace(f"a{post}", f"a{new_post}") + +for line in fileinput.input(join(dirname(__file__), "version.py"), inplace=True): + if line.startswith("__version__"): + print(f"__version__ = \"{version}\"") + else: + print(line.rstrip('\n'))