diff --git a/__init__.py b/__init__.py index e6acc2f..7f3f0f6 100644 --- a/__init__.py +++ b/__init__.py @@ -27,41 +27,141 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from neon_utils.skills.neon_skill import NeonSkill, LOG +from neon_utils.skills.neon_skill import LOG +from neon_utils.skills.kiosk_skill import KioskSkill + +from time import time +from neon_utils.message_utils import get_message_user + 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 +from .request_handling import existing_lang_check, get_store_data + +from .time_processing import left_lime_calculation, \ + time_refactoring, \ + open_stores_search + +from .location_processing import store_selection_by_floors, \ + location_format + import re +from lingua_franca.format import nice_duration +from datetime import datetime +import pytz -class DirectorySkill(NeonSkill): +class DirectorySkill(KioskSkill): def __init__(self): super(DirectorySkill, self).__init__(name="DirectorySkill") self.url = "https://www.alamoanacenter.com/en/directory/" + self._speak_timeout = 60 + @property + def request_lang(self): + return self.lang.split('-')[0] - 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 + @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): + @property + def timeout_seconds(self) -> int: + """ + Time in seconds to wait for a user response before timing out + """ + return 60 + + @property + def greeting_dialog(self) -> str: + """ + Specify a dialog to speak on interaction start + """ + return 'greeting' + + @property + def goodbye_dialog(self) -> str: + """ + Specify a dialog to speak when an interaction ends cleanly + """ + return 'goodbye' + + @property + def timeout_dialog(self) -> str: + """ + Specify a dialog to speak when an interaction ends after some timeout + """ + return 'timeout' + + @property + def error_dialog(self) -> str: + """ + Specify a dialog to speak on unhandled errors + """ + return 'unexpected_error' + + def setup_new_interaction(self, message) -> bool: + """ + Override to include skill-specific actions on first user interaction. + This is the first action that could prompt user to input language, etc. + :param message: Message associated with start request + :returns: True if user interaction is supported + """ + if message: + return True + + def handle_new_interaction(self, message): + """ + Override to interact with the user after the greeting message has been + spoken. + :param message: Message associated with start request + """ + self.speak_dialog('how_can_i_help_you') + + def handle_user_utterance(self, message): + """ + Handle any input from a user interacting with the kiosk. + :param message: Message associated with user utterance + """ + answer = message.data['utterances'][0] + user_request, mall_link = self.user_request_handling(message, answer) + if user_request and mall_link: + LOG.info(mall_link) + if self.execute(user_request, mall_link) is not None: + LOG.info('executed') + return + else: + self.end_interaction(message) + else: + self.end_interaction(message) + + @intent_file_handler("start_mall_directory") + def start_interaction(self, message): + super().start_interaction(message) + + def _extract_store_name(self, utt: str): + """ + Patch the regex bug and try extracting a store name from the utterance + :param utt: string utterance + :return: extracted store name string if found in utterance + """ + rx_file = self.find_resource('store.rx', 'regex') + if rx_file and utt: + with open(rx_file) as f: + for pat in f.readlines(): + pat = pat.strip() + LOG.info(f"Regex pattern: {pat}") + res = re.sub(pat, '\\4', str(utt)) + LOG.info(f"Matched store {res}") + if res: + try: + return res + except IndexError: + pass + return None + + def user_request_handling(self, message, answer): """ Checks user language existence on mall's web-page using existing_lang_check() function. @@ -72,18 +172,22 @@ def user_request_handling(self, message): answer) """ LOG.info(f"Message is {message.data}") - if message.data == {} or message is None: + LOG.info(f"User's greeting answer {answer}") + # extracting store name from user's answer on greeting prompt + user_request = self._extract_store_name(answer) + LOG.info(f"Store in request {user_request}") + + if not user_request: + self.speak('store_not_found') 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(f"{self.mall_link}") + LOG.info(str(self.request_lang)) LOG.info(user_request) - found, link = existing_lang_check(request_lang, self.mall_link()) + found, link = existing_lang_check(self.request_lang, self.mall_link) if found: - link = self.mall_link()+request_lang+'/directory/' - LOG.info('new link: '+ link) + link = self.mall_link + self.request_lang + '/directory/' + LOG.info('new link: ' + link) return user_request, link else: self.speak_dialog("no_lang") @@ -92,261 +196,249 @@ def user_request_handling(self, message): 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 + get another store info, after Neon gave the + information about previously selected store. + If user's answer 'yes': asks what store 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 + None (if no store 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 + another_store = self.get_response('another_store') + if another_store is not None: + LOG.info(f'another store {another_store}') + return another_store elif start_again == "no": - self.speak_dialog('no_shop_request') + self.speak_dialog('no_store_request') else: + self.speak_dialog('unexpected_error') return None - def speak_shops(self, shop_info): + def speak_stores(self, store_info): """ - Speaks shop info that was found. + Speaks store 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. + speak_dialog('found_store', {"name": store['name'], "hours": hours, "location": location}) + Shows store label image in gui. Args: - shop_info (list): found shops on user's + store_info (list): found stores 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']) + for store in store_info: + LOG.info(store) + location = location_format(store['location']) + hours = re.sub('(\d+)\:*(\d*)am.+(\d+)pm', r'from \1 \2 A M to \3 P M', store['hours']) + LOG.info(f'changed hours {hours}') + self.speak_dialog('found_store', {"name": store['name'], "hours": hours, "location": location}) + LOG.info({"name": store['name'], "hours": hours, "location": location}) + if store['logo'] and store['logo'].startswith('http'): + self.gui.show_image(store['logo'], + caption=f'{hours} {location}', + title=store['name'], + fill="PreserveAspectFit", + background_color="#FFFFFF") - def location_selection(self, shop_info): + def location_selection(self, store_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. + If there are several stores in found stores list + and user wants to get store info on the certain + floor. If store on that floor exists speaks + this store info. Else speaks all stores info. Args: - shop_info (list): found shops on user's + store_info (list): found stores on user's request Returns: - 3, None (to ask for another shop info) + 3, None (to ask for another store info) """ - LOG.info(f"Shop by location selection {shop_info}") + LOG.info(f"store by location selection {store_info}") floor = self.get_response('which_floor') - shops = shop_selection_by_floors(floor, shop_info) - if shops: - self.speak_shops(shops) + stores = store_selection_by_floors(floor, store_info) + if stores: + self.speak_stores(stores) else: - self.speak_dialog('no_shop_on_level') - self.speak_shops(shop_info) + self.speak_dialog('no_store_on_level') + self.speak_stores(store_info) return 3, None - - def open_shops_search(self, shop_info, day_time, hour, min): + + def stores_by_time_selection(self, store_info): """ - Selects open shops. Collects the list of - open shops else return empty list. - Args: - shop_info (list): found shops on user's + If user chose to select stores by time or + use like default selection. Gets user's + current time. Selects open stores. + Args: + store_info (list): found stores 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 + Returns: + time_calculation function with True + in 'open' argument. + time_calculation function with False + in 'open' argument. (if list + of open stores is 0) + + """ + LOG.info(f"store by time selection {store_info}") + # user's time + now = datetime.utcnow().replace(tzinfo=pytz.utc).strftime("%H:%M") + # splitting user's time on hour and mins + now_h_m = now.split(':') + LOG.info(f'users_time {now_h_m}') + now_h = int(now_h_m[0]) + now_m = int(now_h_m[1]) + # open stores search + open_stores = open_stores_search(store_info, now_h, now_m) + if len(open_stores) >= 1: + return self.time_calculation(open_stores, True, now_h, now_m) + else: + return self.time_calculation(store_info, False, now_h, now_m) + + def time_calculation(self, store_info, open, now_h, now_m): """ Calculates time difference between user's current time - and shop working hours. + and store 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: + many minutes left. Speaks store info. + Else speaks corresponging dialog. + Speaks store info. + If 'closed' 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 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 + hours, speaks how many hours and minutes left waiting. - If user's time is evening (pm) speaks when the shop + If user's time is evening (pm) speaks when the store opens in the morning. - Speaks shop info. + Speaks store 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 + store_info (list): found stores on user's request + open (boolean): True - if store is open + now_h (int): user's current hour + now_m (int): user's current minute Returns: - 3, None (to ask for another shop info) + 3, None (to ask for another store 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 + Prompt: 'store is closed now. Opens in 1 hour' + """ + for store in store_info: + work_time = store['hours'].split(' – ') + store_name = store['name'] + + LOG.info(f'Store work time {work_time}') + open_time = time_refactoring(work_time[0]) + close_time = time_refactoring(work_time[1]) + LOG.info(f'Normalixed time {open_time, close_time}') + 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}) + duration = None + if 0 <= (close_time[0] - now_h) <= 1: + LOG.info(f'hour waiting {close_time[0] - now_h}') + duration = left_lime_calculation(now_h, now_m, close_time[0], close_time[1]) + if duration: + formated_duration = nice_duration(duration, lang=str(self.request_lang), speech=True) + LOG.info(f'{store_name} closes in {formated_duration}.') + self.speak_dialog('time_before_closing', {"store_name": store_name, "duration": formated_duration}) else: - LOG.info(f'{shop_name} is open.') - self.speak_dialog('open_now', {'shop_name': shop_name}) - LOG.info([shop]) - self.speak_shops([shop]) + LOG.info(f'{store_name} is open.') + self.speak_dialog('open_now', {'store_name': store_name}) + self.speak_stores([store]) 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]) + LOG.info(f'{store_name} is closed.') + if (close_time[0] - now_h) >= 0: + duration = left_lime_calculation(now_h, now_m, close_time[1], close_time[0]) + formated_duration = nice_duration(duration, lang=str(self.request_lang), speech=True) + LOG.info(f'{store_name} is closed now. store opens in {formated_duration}') + self.speak_dialog('waiting_for_opening', {"store_name": store_name, 'duration': formated_duration}) + else: + open_time = work_time.split(' – ')[0] + open_time = re.sub('(\d+)\:*(\d*)am', r'\1 \2 A M', open_time) + LOG.info(f'{store_name} is closed now. store opens at {open_time}') + self.speak_dialog('closed_now', {'store_name': store_name, 'open_time': open_time}) + self.speak_stores([store]) 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): + def find_store(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 + variable contains the name of the store. + The matching function get_store_data() is + used to find the store 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 + can return several stores, one store or empty list. - If no shop was found asks user to repeat. + If no store 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 + If there are several stores 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' + If 'time' - finds open stores. If open stores + list is not empty speaks open stores, else + tells time difference between user and stores' 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. + in. If stores were found speaks stores' info, + else tells that there is no store on that level + and speaks all found stores. 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. + If there was one store found speaks this + store info. Returns 3, None to stop current + store search. Location and time sorting functions return - 3, None to stop current shop search. + 3, None to stop current store 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") + LOG.info(f"I am parsing stores 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') + store_info = get_store_data(mall_link, user_request, file_path) + LOG.info(f"I found {len(store_info)} stores") + LOG.info(f"store list: {store_info}") + if len(store_info) == 0: + user_request = self.get_response('store_not_found', {"store_name": user_request}) return 1, user_request - elif len(shop_info) > 1: + elif len(store_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) + return self.stores_by_time_selection(store_info) elif self.voc_match(sorting_selection, "location"): LOG.info('Location sorting selected') - return self.location_selection(shop_info) + return self.location_selection(store_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) + return self.stores_by_time_selection(store_info) else: LOG.info('Nothing matched. Sorting by time on default.') - return self.shops_by_time_selection(shop_info) + return self.stores_by_time_selection(store_info) else: - LOG.info(f"found shop {shop_info}") - self.speak_shops(shop_info) + LOG.info(f"found store {store_info}") + self.speak_stores(store_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) + new_count, user_request = self.find_store(user_request, mall_link) count = count + new_count user_request = self.start_again() LOG.info(str(user_request)) @@ -356,26 +448,5 @@ def execute(self, 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/another_shop.dialog b/locale/en-us/dialog/en/another_shop.dialog deleted file mode 100644 index be4fbfd..0000000 --- a/locale/en-us/dialog/en/another_shop.dialog +++ /dev/null @@ -1 +0,0 @@ -What shop you are looking for? \ No newline at end of file diff --git a/locale/en-us/dialog/en/another_store.dialog b/locale/en-us/dialog/en/another_store.dialog new file mode 100644 index 0000000..b9c794a --- /dev/null +++ b/locale/en-us/dialog/en/another_store.dialog @@ -0,0 +1,2 @@ +What store are you looking for? + diff --git a/locale/en-us/dialog/en/ask_more.dialog b/locale/en-us/dialog/en/ask_more.dialog index b2b8db3..05d8228 100644 --- a/locale/en-us/dialog/en/ask_more.dialog +++ b/locale/en-us/dialog/en/ask_more.dialog @@ -1 +1 @@ -Do you want to get another shop info? \ No newline at end of file +Do you want to get info for another store? diff --git a/locale/en-us/dialog/en/choose_selection.dialog b/locale/en-us/dialog/en/choose_selection.dialog index 23ac161..56419a0 100644 --- a/locale/en-us/dialog/en/choose_selection.dialog +++ b/locale/en-us/dialog/en/choose_selection.dialog @@ -1 +1 @@ -Do you want to select store by work hours or location? \ No newline at end of file +Do you want to select a result by open hours or location? diff --git a/locale/en-us/dialog/en/closed_now.dialog b/locale/en-us/dialog/en/closed_now.dialog index d095784..197d364 100644 --- a/locale/en-us/dialog/en/closed_now.dialog +++ b/locale/en-us/dialog/en/closed_now.dialog @@ -1 +1 @@ -{{shop_name}} is closed now. Opens at {{open_time}}. \ No newline at end of file +{{store_name}} is closed now. It opens at {{open_time}}. diff --git a/locale/en-us/dialog/en/closing_minutes.dialog b/locale/en-us/dialog/en/closing_minutes.dialog index 46d9f8b..05eee31 100644 --- a/locale/en-us/dialog/en/closing_minutes.dialog +++ b/locale/en-us/dialog/en/closing_minutes.dialog @@ -1 +1 @@ -{{shop_name}} is open now. Closes in {{wait_min}} minutes. \ No newline at end of file +{{store_name}} is open now. It closes in {{wait_min}} minutes. diff --git a/locale/en-us/dialog/en/finished.dialog b/locale/en-us/dialog/en/finished.dialog index 17ed73d..5566371 100644 --- a/locale/en-us/dialog/en/finished.dialog +++ b/locale/en-us/dialog/en/finished.dialog @@ -1 +1 @@ -Finished. Goodbye! \ No newline at end of file +Have a good day! diff --git a/locale/en-us/dialog/en/found_shop.dialog b/locale/en-us/dialog/en/found_store.dialog similarity index 100% rename from locale/en-us/dialog/en/found_shop.dialog rename to locale/en-us/dialog/en/found_store.dialog diff --git a/locale/en-us/dialog/en/goodbye.dialog b/locale/en-us/dialog/en/goodbye.dialog new file mode 100644 index 0000000..79f7f7d --- /dev/null +++ b/locale/en-us/dialog/en/goodbye.dialog @@ -0,0 +1 @@ +Ok. See you later! \ No newline at end of file diff --git a/locale/en-us/dialog/en/greeting.dialog b/locale/en-us/dialog/en/greeting.dialog new file mode 100644 index 0000000..05a682b --- /dev/null +++ b/locale/en-us/dialog/en/greeting.dialog @@ -0,0 +1 @@ +Hello! \ No newline at end of file diff --git a/locale/en-us/dialog/en/how_can_i_help_you.dialog b/locale/en-us/dialog/en/how_can_i_help_you.dialog new file mode 100644 index 0000000..c1c20e4 --- /dev/null +++ b/locale/en-us/dialog/en/how_can_i_help_you.dialog @@ -0,0 +1 @@ +How can I help you? \ 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 index 70532f4..aec13a4 100644 --- a/locale/en-us/dialog/en/more_than_one.dialog +++ b/locale/en-us/dialog/en/more_than_one.dialog @@ -1 +1 @@ -I found more than one shop in your request. \ No newline at end of file +I found more than one store location. diff --git a/locale/en-us/dialog/en/no_shop_on_level.dialog b/locale/en-us/dialog/en/no_shop_on_level.dialog deleted file mode 100644 index 69b5d6d..0000000 --- a/locale/en-us/dialog/en/no_shop_on_level.dialog +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 6ca56f4..0000000 --- a/locale/en-us/dialog/en/no_shop_request.dialog +++ /dev/null @@ -1 +0,0 @@ -Okay. I will stop mall parsing. \ No newline at end of file diff --git a/locale/en-us/dialog/en/no_store_on_level.dialog b/locale/en-us/dialog/en/no_store_on_level.dialog new file mode 100644 index 0000000..e0dfce1 --- /dev/null +++ b/locale/en-us/dialog/en/no_store_on_level.dialog @@ -0,0 +1 @@ +I did not find that store on your level. Here are all possible locations. diff --git a/locale/en-us/dialog/en/no_store_request.dialog b/locale/en-us/dialog/en/no_store_request.dialog new file mode 100644 index 0000000..fa054b1 --- /dev/null +++ b/locale/en-us/dialog/en/no_store_request.dialog @@ -0,0 +1 @@ +Okay! diff --git a/locale/en-us/dialog/en/open_now.dialog b/locale/en-us/dialog/en/open_now.dialog index 3421d4c..180a428 100644 --- a/locale/en-us/dialog/en/open_now.dialog +++ b/locale/en-us/dialog/en/open_now.dialog @@ -1 +1,2 @@ -{{shop_name}} is open. \ No newline at end of file +{{store_name}} is open. + diff --git a/locale/en-us/dialog/en/opening_hours.dialog b/locale/en-us/dialog/en/opening_hours.dialog deleted file mode 100644 index 3bcb8ce..0000000 --- a/locale/en-us/dialog/en/opening_hours.dialog +++ /dev/null @@ -1 +0,0 @@ -{{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 deleted file mode 100644 index ce55fcc..0000000 --- a/locale/en-us/dialog/en/opening_minutes.dialog +++ /dev/null @@ -1 +0,0 @@ -{{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 deleted file mode 100644 index 97c1ac0..0000000 --- a/locale/en-us/dialog/en/shop_by_floor.dialog +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 859f286..0000000 --- a/locale/en-us/dialog/en/shop_not_found.dialog +++ /dev/null @@ -1,2 +0,0 @@ -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_mall_directory.intent b/locale/en-us/dialog/en/start_mall_directory.intent new file mode 100644 index 0000000..867d0c0 --- /dev/null +++ b/locale/en-us/dialog/en/start_mall_directory.intent @@ -0,0 +1,5 @@ +start mall directory skill +start mall skill +start mall parsing +start kiosk skill +kiosk skill diff --git a/locale/en-us/dialog/en/start_parsing.dialog b/locale/en-us/dialog/en/start_parsing.dialog index 4071f9b..15fc6b6 100644 --- a/locale/en-us/dialog/en/start_parsing.dialog +++ b/locale/en-us/dialog/en/start_parsing.dialog @@ -1 +1 @@ -I am parsing shops and malls for your request. \ No newline at end of file +Let me look that up for you. diff --git a/locale/en-us/dialog/en/stop.dialog b/locale/en-us/dialog/en/stop.dialog index 2874c27..b1c96ba 100644 --- a/locale/en-us/dialog/en/stop.dialog +++ b/locale/en-us/dialog/en/stop.dialog @@ -1 +1 @@ -Do you want to exit the mall directory? \ No newline at end of file +Are you finished getting store information for now? diff --git a/locale/en-us/dialog/en/store_by_floor.dialog b/locale/en-us/dialog/en/store_by_floor.dialog new file mode 100644 index 0000000..ede6d37 --- /dev/null +++ b/locale/en-us/dialog/en/store_by_floor.dialog @@ -0,0 +1 @@ +Do you want to get store info by floor? diff --git a/locale/en-us/dialog/en/store_not_found.dialog b/locale/en-us/dialog/en/store_not_found.dialog new file mode 100644 index 0000000..261cf75 --- /dev/null +++ b/locale/en-us/dialog/en/store_not_found.dialog @@ -0,0 +1,3 @@ +Sorry, I can't find {{store_name}}, but maybe I misunderstood, could you ask again please? +{{store_name}} isn't here, could you please repeat your query in case I misunderstood? +I apologize, I can't find a listing for {{store_name}} here. Can you please say that again? diff --git a/locale/en-us/dialog/en/time_before_closing.dialog b/locale/en-us/dialog/en/time_before_closing.dialog new file mode 100644 index 0000000..5a9974b --- /dev/null +++ b/locale/en-us/dialog/en/time_before_closing.dialog @@ -0,0 +1 @@ +{{store_name}} is open now. Closes in {{duration}}.' diff --git a/locale/en-us/dialog/en/timeout.dialog b/locale/en-us/dialog/en/timeout.dialog new file mode 100644 index 0000000..f16450a --- /dev/null +++ b/locale/en-us/dialog/en/timeout.dialog @@ -0,0 +1 @@ +Sorry, but waiting time is over. \ No newline at end of file diff --git a/locale/en-us/dialog/en/waiting_for_opening.dialog b/locale/en-us/dialog/en/waiting_for_opening.dialog new file mode 100644 index 0000000..38bf2d3 --- /dev/null +++ b/locale/en-us/dialog/en/waiting_for_opening.dialog @@ -0,0 +1,2 @@ +{{store_name}} is closed now. Opens in {{duration}}.' + diff --git a/locale/en-us/dialog/en/which_floor.dialog b/locale/en-us/dialog/en/which_floor.dialog index 4333de5..c3edcfa 100644 --- a/locale/en-us/dialog/en/which_floor.dialog +++ b/locale/en-us/dialog/en/which_floor.dialog @@ -1,3 +1,2 @@ -Which floor are you staying on? +Which floor are you 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 index ccbf4d9..6d68908 100644 --- a/locale/en-us/run_mall_parser.intent +++ b/locale/en-us/run_mall_parser.intent @@ -1,8 +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 +where (is|are|can i find) {store} +where (is|are) {store} located +i am looking for {store} +location of {store} +where {store} +show {store} +show me {store} +find {store} diff --git a/location_processing.py b/location_processing.py new file mode 100644 index 0000000..cd71314 --- /dev/null +++ b/location_processing.py @@ -0,0 +1,56 @@ +#location operations +from lingua_franca.format import pronounce_number +import re + + +def location_format(location): + """ + Finds all digits in store's location and + formats them to numeral words. + Args: + location (str): location info + from stores 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 store_selection_by_floors(user_request, found_stores): + """ + If there are several stores in found stores list + and user agrees to select store 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 store was found appends it to the new found + list. + Args: + user_request (str): floor from user + found_stores (list): found stores on user's + request + Returns: + stores_by_floor (list): stores that was found by floor + """ + stores_by_floor = [] + for store in found_stores: + numbers = re.findall(r'\d+', store['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: + stores_by_floor.append(store) + return stores_by_floor \ No newline at end of file diff --git a/regex/store.rx b/regex/store.rx new file mode 100644 index 0000000..9028dcd --- /dev/null +++ b/regex/store.rx @@ -0,0 +1 @@ +(where|find|show|i)?\s(is|are|need|can\s*i?\s?find*|me)?\s?(find)?\s?(\w+\s?\w*\s*)(situated)? \ No newline at end of file diff --git a/request_handling.py b/request_handling.py index 049e56a..d9edf3f 100644 --- a/request_handling.py +++ b/request_handling.py @@ -32,30 +32,43 @@ 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 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 + +#caching stores' info def find_cached_stores(user_request: str, url, file_path): """ - Check shop name existence in cache keys + Check store name existence in cache keys Args: - user_request (str): shop from user's message + user_request (str): store 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 + if store wasn't found -> None, read data + if store found -> store_info (list), read data Examples: [ {"name": "ABS stores", "time": "8am-10pm", "location": "1 level"}, @@ -76,21 +89,21 @@ def find_cached_stores(user_request: str, url, file_path): 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]}') + LOG.info(f'Store exists {data[store_name]}') return data[store_name], data else: - LOG.info("Shop doesn't exist in cache") + LOG.info("Store 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 + all stores info. Creates dict key from store + name. Value list of dicts with current store info. - If shop name already exists in created dict - append current shop dict to existing + If store name already exists in created dict + append current store dict to existing list. Writes created dict to created JSON file. Args: @@ -104,115 +117,26 @@ def caching_stores_in_mall(file_path, url): """ caching_file = file_path+'/cached_stores.json' LOG.info(f'caching_file {caching_file}') - shop_cache = {} + store_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"}) + for store in soup.find_all(attrs={"class": "directory-tenant-card"}): + logo = store.find_next("img").get('src') + info = store.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) + store_data = {'name': name, 'hours': hours, 'location': location, 'logo': logo} + if name in store_cache.keys(): + store_cache[name].append(store_data) else: - shop_cache[name] = [shop_data] + store_cache[name] = [store_data] with open(caching_file, 'w+') as outfile: - json.dump(shop_cache, outfile, ensure_ascii=False) + json.dump(store_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 - +# mall link parsing 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' @@ -227,10 +151,10 @@ def parse(url): LOG.info("Failed url parsing") -def get_shop_data(url, user_request, file_path): +def get_store_data(url, user_request, file_path): """ Check existence of user's request store in cache - if shop was found returns list with shop info, + if store was found returns list with store 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 @@ -241,15 +165,15 @@ def get_shop_data(url, user_request, file_path): url (str): mall link from hardcoded in init.py user_request (str): utterance from stt parsing Returns: - : found_shops (list): found shops' info + : found_stores (list): found stores' 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 + found_stores, data = find_cached_stores(user_request, url, file_path) + LOG.info(found_stores) + if found_stores: + LOG.info(f"found_stores: {found_stores}") + return found_stores else: return [] diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5a2a1bd..bf588e7 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,7 @@ numpy -neon-utils~=1.0 +neon-utils[network]==1.1.3a6 bs4 requests ovos-lingua-franca datetime +ovos_utils~=0.0,>=0.0.27a4 diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt index 55b033e..615fac9 100644 --- a/requirements/test_requirements.txt +++ b/requirements/test_requirements.txt @@ -1 +1,2 @@ +mock pytest \ No newline at end of file diff --git a/skill.json b/skill.json index 1394d21..55a585c 100644 --- a/skill.json +++ b/skill.json @@ -1,6 +1,6 @@ { "title": "{Malls parser skill neon}", - "url": "https://github.com/NeonGeckoCom/skill-directory", + "url": "https://github.com/NeonGeckoCom/mall_guide_skill", "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", @@ -18,6 +18,7 @@ "neon-utils~=1.0", "numpy", "ovos-lingua-franca", + "ovos_utils~=0.0,>=0.0.27a4", "requests" ], "system": {}, @@ -43,7 +44,7 @@ "NeonGeckoCom", "NeonMariia" ], - "skillname": "skill-directory", + "skillname": "mall_guide_skill", "authorname": "NeonGeckoCom", "foldername": null } \ No newline at end of file diff --git a/test/test_skill.py b/test/test_skill.py index 411ad8f..7b0138e 100644 --- a/test/test_skill.py +++ b/test/test_skill.py @@ -35,9 +35,11 @@ from ovos_utils.messagebus import FakeBus from mycroft.skills.skill_loader import SkillLoader - from mycroft_bus_client import Message +import lingua_franca +lingua_franca.load_language('en') + class TestSkill(unittest.TestCase): @@ -53,66 +55,78 @@ def setUpClass(cls) -> None: 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() + # 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): + + class MockGui: + def __init__(self): + self._data = dict() + self.show_image = Mock() + + def __setitem__(self, key, value): + self._data[key] = value + + def __getitem__(self, item): + return self._data[item] + + @staticmethod + def clear(): + pass + self.skill.speak.reset_mock() self.skill.speak_dialog.reset_mock() - # TODO: Put any cleanup here that runs before each test case + mock_gui = MockGui() + self.skill.gui = mock_gui + @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', + 'store': 'ABC stores', 'lang': 'en-us'}, {'context_key': 'MallParsing'}) - self.skill.user_request_handling(message) + answer = 'where is abc stores' + self.skill.user_request_handling(message, answer) + - 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) + def test_en_time_calculation(self): + store_info = [{'name': 'ABC Stores', 'hours': '9:30am – 9pm', 'location': 'Street Level 1, near Centerstage', + 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937914061-abcstores.png'}, + {'name': 'ABC Stores', 'hours': '10:30am – 8:30pm', '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', 'am'], 9, 15 - result_shops = self.skill.open_shops_search(shop_info, day_time, hour, min) - self.assertEqual(shop_info[0], result_shops[0]) + now_hour, now_min = 9, 30 + open = True - # 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'}] + result = self.skill.time_calculation(store_info, open, now_hour, now_min) + self.assertEqual(result, (3, None)) - # day_time, hour, min = ['9:15', 'pm'], 9, 15 - # print(self.skill.time_calculation(shop_info, False, day_time, hour, min)) + def test_en_stores_by_time_selection(self): + store_info = [{'name': 'ABC Stores', 'hours': '9:30am – 9pm', 'location': 'Street Level 1, near Centerstage', + 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937914061-abcstores.png'}, + {'name': 'ABC Stores', 'hours': '10:30am – 8:30pm', 'location': 'Street Level 1, in the Ewa Wing', + 'logo': 'https://gizmostorageprod.blob.core.windows.net/tenant-logos/1615937946329-abcstores.png'}] - # day_time, hour, min = ['8:15', 'pm'], 8, 15 - # print(self.skill.time_calculation(shop_info, False, day_time, hour, min)) + result = self.skill.stores_by_time_selection(store_info) + self.assertEqual(result, (3, None)) if __name__ == '__main__': diff --git a/time_processing.py b/time_processing.py new file mode 100644 index 0000000..8deb8a3 --- /dev/null +++ b/time_processing.py @@ -0,0 +1,77 @@ +# time operations +from datetime import datetime +from neon_utils.skills.neon_skill import LOG +import re + +def change_format(time): + """ + Changing format from 12h to 24h. + Returns: + formated_time (list): [int(hour), int(min)] + """ + in_time = datetime.strptime(time, "%I:%M %p") + out_time = datetime.strftime(in_time, "%H:%M") + hour_min = out_time.split(':') + formated_time = [int(hour_min[0]), int(hour_min[1])] + return formated_time + +def time_refactoring(time_str): + """ + Adding space detween time and am|pm. + If working time doesn't have mins add them. + Args: + time_str (list): store's work time + Returns: + open_time (list): open_h (int), open_m (int), + close_time (list): close_h (int), close_m (int), + """ + if ':' not in time_str: + time_refactored = re.sub(r'(\d+)(am|pm)', '\g<1>:00 \g<2>', time_str) + else: + time_refactored = re.sub(r'(am|pm)', ' \g<1>', time_str) + new_format = change_format(time_refactored) + return new_format + +def left_lime_calculation(user_h, user_m, work_h, work_m): + if work_m < user_m: + wait_min = user_m - work_m + else: + wait_min = work_m - user_m + wait_h_opening = work_h - user_h + LOG.info(f'Hour difference {wait_h_opening}') + duration = wait_h_opening * 3600 + wait_min * 60 + return duration + +def open_stores_search(store_info, now_h, now_m): + """ + Selects open stores. Collects the list of + open stores else return empty list. + Args: + store_info (list): found stores on user's + request + now (str): current user's time + Returns: + store_info (list): open stores + """ + open_stores = [] + LOG.info(f" user's current time {now_h, now_m}") + + for store in store_info: + # formating store's work hours + time_splited = store['hours'].split(' – ') + open = time_refactoring(time_splited[0]) + close = time_refactoring(time_splited[1]) + LOG.info(f'formated_work_time {open, close}') + + if now_h == open[0]: + if now_m >= open[1]: + open_stores.append(store) + elif now_h > open[0]: + open_stores.append(store) + elif now_h == close[0]: + if now_m < close[1]: + open_stores.append(store) + elif now_h < close[0]: + open_stores.append(store) + LOG.info(f'open stores {open_stores}') + return open_stores \ No newline at end of file