├── src ├── tests │ ├── __init__.py │ ├── base_request.json │ └── test_skill.py └── skill_cosmo_quest │ ├── __init__.py │ ├── button_menu.py │ ├── graphite_statistics.py │ ├── sessions.py │ ├── utils.py │ ├── audios.py │ ├── phrases.py │ └── application.py ├── .gitignore ├── skill_config.cfg ├── Pipfile ├── README.md ├── setup.py ├── Makefile ├── License.txt └── Pipfile.lock /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | 4 | config.ini 5 | .mypy_cache 6 | .pytest_cache 7 | .make-install* 8 | old -------------------------------------------------------------------------------- /skill_config.cfg: -------------------------------------------------------------------------------- 1 | [main] 2 | host=0.0.0.0 3 | port=1254 4 | session_life_time_sec=3600 5 | audio_files_path=https://resources-vc.go.mail.ru/external_skills/skill_cosmo_quest/{file_name} 6 | 7 | [graphite] 8 | host=127.0.0.1 9 | prefix=cosmo_quest_skill 10 | interval=1 -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pytest = "*" 8 | pytest-asyncio = "*" 9 | isort = "*" 10 | flake8 = "*" 11 | mypy = "*" 12 | black = "*" 13 | 14 | [packages] 15 | aiohttp = "*" 16 | graphyte = "*" 17 | 18 | [requires] 19 | python_version = "3.6" 20 | 21 | [pipenv] 22 | allow_prereleases = true 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Скилл космический квест 2 | 3 | #### Триггеры для вызова скилла 4 | космический квест, космос 5 | 6 | #### Описание скилла 7 | Космическое путешествие, где вас ждут пять экспедиций по Солнечной системе! Вы исследуете планеты, астероиды и кометы, а также узнаете много нового о космосе. 8 | 9 | #### Краткое описание скилла 10 | Интерактивный квест по космосу 11 | 12 | #### !!!При установке необходимо указать шаблон URL аудиофайлов audio_files_path в конфиге!!! 13 | 14 | #### Install: 15 | 16 | make install 17 | 18 | #### Run: 19 | 20 | make run 21 | 22 | #### Tests: 23 | 24 | make test 25 | 26 | #### flake, mypy: 27 | 28 | make flake 29 | make mypy 30 | 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from setuptools import find_packages, setup 6 | 7 | VERSION = os.getenv("CI_COMMIT_TAG") 8 | if not VERSION: 9 | VERSION = "0.0.1" 10 | 11 | # --- > 12 | setup( 13 | name="skill-space-quest", 14 | version=VERSION, 15 | package_dir={"skill_cosmo_quest": "src/skill_cosmo_quest"}, 16 | python_requires=">=3.6", 17 | packages=find_packages(where="src", include=["skill_cosmo_quest"]), 18 | url="https://gitlab.com/mailru-voice/external_skills/skill_cosmo_quest", 19 | license="MIT", 20 | author="n.andreev", 21 | author_email="nickandreevart@gmail.com", 22 | description="skill-cosmo-quest", 23 | ) 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: \ 2 | all install run 3 | 4 | all: .make-install 5 | 6 | install: .make-install 7 | 8 | .make-install-pipenv: 9 | @if ! which pipenv &> /dev/null; then \ 10 | pip install pipenv; \ 11 | fi 12 | @touch $@ 13 | 14 | .make-install: Pipfile .make-install-pipenv 15 | pipenv install -d 16 | @touch $@ 17 | 18 | run: .make-install 19 | python3 -m src.skill_cosmo_quest.application 20 | 21 | flake: 22 | flake8 src/skill_cosmo_quest 23 | flake8 src/tests 24 | 25 | mypy: 26 | mypy src/skill_cosmo_quest 27 | 28 | isort: 29 | isort setup.py 30 | isort src/skill_cosmo_quest 31 | isort src/tests 32 | 33 | test: 34 | pytest -q src/tests --log-format="%(asctime)s %(levelname)s %(message)s" --log-date-format="%Y-%m-%d %H:%M:%S" 35 | -------------------------------------------------------------------------------- /src/tests/base_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "client_id": "MailRu-VC/1.0", 4 | "locale": "ru_RU", 5 | "timezone": "Europe/Moscow", 6 | "interfaces": { 7 | "screen": {} 8 | }, 9 | "_city_ru": "Москва" 10 | }, 11 | "request": { 12 | "command": "включи скилл новогодний квест", 13 | "original_utterance": "включи скилл новогодний квест", 14 | "type": "SimpleUtterance", 15 | "nlu": { 16 | "tokens": [ 17 | "включи", 18 | "скилл", 19 | "новогодний", 20 | "квест" 21 | ], 22 | "entities": [] 23 | } 24 | }, 25 | "session": { 26 | "session_id": "111", 27 | "user_id": "222", 28 | "skill_id": "333", 29 | "new": false, 30 | "message_id": 1 31 | }, 32 | "version": "1.0" 33 | } -------------------------------------------------------------------------------- /src/skill_cosmo_quest/button_menu.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | 4 | class ButtonsMenuBuilder: 5 | def __init__(self, state_name=None, buttons=None) -> None: 6 | self.state_name: str = state_name 7 | self.buttons: List[Dict] = [] 8 | if buttons is not None: 9 | for button in buttons: 10 | self.add_button(**button) 11 | 12 | def add_button(self, text, callback_data={}) -> None: 13 | payload = { 14 | "state": self.state_name, 15 | "text": text, 16 | "callback_data": callback_data, 17 | } 18 | self.buttons.append({"title": text, "payload": payload}) 19 | 20 | def get_to_send(self): 21 | if len(self.buttons) == 0: 22 | return None 23 | 24 | return self.buttons 25 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | Skill Cosmo quest 3 | 4 | Copyright (c) 2021-present, Mail.Ru, LLC. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/graphite_statistics.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Dict 4 | 5 | import graphyte # type: ignore 6 | 7 | 8 | class GraphiteSender: 9 | def __init__(self, host, prefix) -> None: 10 | self.sender: graphyte.Sender = graphyte.Sender(host, prefix=prefix) 11 | self.is_runned: bool = False 12 | self.start_collect() 13 | 14 | def start_collect(self): 15 | self.metrics: Dict = {} 16 | 17 | def inc(self, name): 18 | logging.info(f"Add metrics: {name}") 19 | if name in self.metrics: 20 | self.metrics[name] += 1 21 | else: 22 | self.metrics[name] = 1 23 | 24 | def send_metrics(self): 25 | for name, value in self.metrics.items(): 26 | self.sender.send(name, value) 27 | 28 | async def send_task_async(self): 29 | self.start_collect() 30 | await asyncio.sleep(self.interval) 31 | self.send_metrics() 32 | if self.is_runned: 33 | self.loop.create_task(self.send_task_async()) 34 | 35 | def add_loop_task(self, loop, interval): 36 | if self.is_runned: 37 | raise Exception("Graphite client exception", "Send task is already runned") 38 | 39 | self.loop = loop 40 | self.is_runned = True 41 | self.interval = interval 42 | self.loop.create_task(self.send_task_async()) 43 | 44 | def stop_loop_task(self): 45 | self.is_runned = False 46 | -------------------------------------------------------------------------------- /src/tests/test_skill.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | import skill_cosmo_quest 8 | import skill_cosmo_quest.application 9 | import skill_cosmo_quest.phrases 10 | import skill_cosmo_quest.quest 11 | 12 | base_req_file_name = Path(__file__).parent / "base_request.json" 13 | 14 | 15 | class FakeRequest: 16 | def __init__(self, json): 17 | self._json = json 18 | 19 | async def json(self): 20 | await asyncio.sleep(0.001) 21 | return self._json 22 | 23 | 24 | @pytest.mark.asyncio 25 | @pytest.mark.parametrize( 26 | "phrase_text, new_session, answer", 27 | [ 28 | ("космический квест", True, skill_cosmo_quest.phrases.HELLO_PHRASES[0][0]), 29 | ("да", False, "001"), 30 | ("тренировка началась", False, "003"), 31 | ("Готово", False, "005"), 32 | ], 33 | ) 34 | async def test_skill(phrase_text, new_session, answer): 35 | 36 | req = {} 37 | with open(base_req_file_name) as f: 38 | req = json.load(f) 39 | 40 | req["request"]["command"] = phrase_text 41 | req["session"]["new"] = new_session 42 | 43 | resp = await skill_cosmo_quest.application.marusya_cosmo_quest(FakeRequest(req)) 44 | if new_session: 45 | assert json.loads(resp.text)["response"]["text"] in answer 46 | else: 47 | skill_cosmo_quest.quest.get_stage_by_id(answer).texts[0] in json.loads( 48 | resp.text 49 | )["response"]["text"] 50 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/sessions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import time 4 | 5 | sessions: dict = dict() 6 | 7 | 8 | class UserSession: 9 | def __init__(self, session_id): 10 | self.session_id = session_id 11 | self.set_info({}) 12 | self.update() 13 | 14 | def set_info(self, info): 15 | self.info = info 16 | 17 | def update(self): 18 | self.last_time = time.time() 19 | 20 | def remove(self): 21 | global sessions 22 | sessions.pop(self.session_id, None) 23 | 24 | 25 | def get_session(session_id): 26 | global sessions 27 | user_session = sessions.get(session_id, None) 28 | if user_session is None: 29 | user_session = UserSession(session_id) 30 | sessions[session_id] = user_session 31 | return user_session 32 | 33 | 34 | def init(new_session_life_time=60000): 35 | global session_life_time 36 | session_life_time = new_session_life_time 37 | 38 | 39 | async def remove_old_sessions(): 40 | global sessions, session_life_time 41 | current_time = time.time() 42 | remove_time = current_time - session_life_time 43 | delete_list = [] 44 | counter = 0 45 | for key, value in sessions.items(): 46 | if value.last_time < remove_time: 47 | delete_list.append(key) 48 | counter += 1 49 | if counter % 1000 == 0: 50 | await asyncio.sleep(1) 51 | 52 | logging.info(f"{len(delete_list)} sessions from {counter} for remove") 53 | 54 | for delete_session in delete_list: 55 | del sessions[delete_session] 56 | logging.info(f"Session timeout: {delete_session}") 57 | 58 | 59 | event_loop = None 60 | 61 | 62 | async def task_async(): 63 | global session_life_time, event_loop 64 | await remove_old_sessions() 65 | await asyncio.sleep(session_life_time) 66 | event_loop.create_task(task_async()) 67 | 68 | 69 | def add_loop_task(loop): 70 | global event_loop 71 | event_loop = loop 72 | loop.create_task(task_async()) 73 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | 4 | def set_response( 5 | response, text_and_tts=None, text=None, tts=None, buttons=None, speak_text=True 6 | ) -> None: 7 | if text_and_tts is not None: 8 | text = text_and_tts[0] 9 | tts = text_and_tts[1] 10 | 11 | if (tts is None) and speak_text: 12 | tts = text 13 | 14 | if text is not None: 15 | response["response"]["text"] = text 16 | 17 | if tts is not None: 18 | """ 19 | response["response"]["tts_type"] = "ssml" 20 | response["response"][ 21 | "ssml" 22 | ] = f'\n{tts}\n' 23 | """ 24 | response["response"]["tts"] = tts 25 | 26 | if buttons is not None: 27 | response["response"]["buttons"] = buttons 28 | 29 | 30 | def is_button_request(request): 31 | return request["request"]["type"] == "ButtonPressed" 32 | 33 | 34 | def get_button_info(request) -> Dict: 35 | return request["request"]["payload"] 36 | 37 | 38 | def get_request_text(request) -> Optional[str]: 39 | if request["request"]["type"] == "SimpleUtterance": 40 | return request["request"]["command"] 41 | elif is_button_request(request): 42 | return get_button_info(request)["text"] 43 | else: 44 | return None 45 | 46 | 47 | NOT_LETTERS = [",", ".", "!", "?"] 48 | 49 | 50 | def prepare_phrase(phrase) -> str: 51 | result = "" 52 | for symbol in phrase: 53 | if symbol not in NOT_LETTERS: 54 | result += symbol 55 | return result.replace("ё", "е").lower() 56 | 57 | 58 | def prepare_phrases_list(prepare_list) -> List[str]: 59 | result = [] 60 | for phrase in prepare_list: 61 | result.append(prepare_phrase(phrase)) 62 | return result 63 | 64 | 65 | def get_prepared_text(request) -> Optional[str]: 66 | result = None 67 | result = request["request"].get("command", None) 68 | if result is None: 69 | if request["request"]["type"] == "SimpleUtterance": 70 | result = request["request"].get("command", None) 71 | elif is_button_request(request): 72 | result = get_button_info(request)["text"] 73 | 74 | if result is not None: 75 | result = prepare_phrase(result) 76 | return result 77 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/audios.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | from typing import Dict 3 | 4 | 5 | class VkAudio: 6 | def __init__(self, id): 7 | self.id = id 8 | 9 | def get_tag(self): 10 | return f'' 11 | 12 | 13 | class UrlAudio: 14 | def __init__(self, id): 15 | self.id = id 16 | 17 | def get_tag(self): 18 | return f'' 19 | 20 | 21 | vk_audios: Dict[str, VkAudio] = {} 22 | 23 | 24 | def get_vk(name): 25 | return vk_audios[name] 26 | 27 | 28 | url_audios: Dict[str, UrlAudio] = {} 29 | 30 | audios_tags = {} 31 | 32 | 33 | def get_url(audio_name): 34 | global AUDIO_FILES_PATH 35 | return url_audios[audio_name] 36 | 37 | 38 | def add_file(name, file_name): 39 | global AUDIO_FILES_PATH 40 | url_audio = UrlAudio( 41 | AUDIO_FILES_PATH.format(file_name=urllib.parse.quote(file_name)) 42 | ) 43 | url_audios[name] = url_audio 44 | 45 | 46 | def init(audio_files_template): 47 | global AUDIO_FILES_PATH 48 | AUDIO_FILES_PATH = audio_files_template 49 | add_file("audio_1", "01 Do you hear me v4.mp3") 50 | add_file("audio_2", "02 Metronome.mp3") 51 | add_file("audio_3", "03 Metonome and helicopter.mp3") 52 | add_file("audio_4", "04 Takeoff v1.mp3") 53 | add_file("audio_5", "05 Superengine v3.mp3") 54 | add_file("audio_6", "06 Positive v2.mp3") 55 | add_file("audio_7", "07 Landing.mp3") 56 | add_file("audio_8", "08 Boarding.mp3") 57 | add_file("audio_9", "09 Probe multiple.mp3") 58 | add_file("audio_10", "10 Probe single.mp3") 59 | add_file("audio_11", "11 Dance Music v2.mp3") 60 | add_file("audio_12", "12 Engines v2.mp3") 61 | add_file("audio_13", "13 Shoots.mp3") 62 | add_file("audio_14", "14 Atmospheric drop.mp3") 63 | add_file("audio_15", "15 Parachute.mp3") 64 | add_file("audio_16", "16.1 Falling v4 (impact).mp3") 65 | add_file("audio_16_t", "16.2 Falling v5 (trees).mp3") 66 | add_file("audio_17", "17 Helicopters.mp3") 67 | add_file("audio_18", "18 Knocking v3.mp3") 68 | add_file("audio_19", "19 Hatch Opening v2.mp3") 69 | add_file("audio_20", "20 Welcome home.mp3") 70 | add_file("audio_21", "21 Alarm v2.mp3") 71 | add_file("audio_22", "22 Air Licking v3.mp3") 72 | add_file("audio_23", "23 Welding.mp3") 73 | add_file("audio_24", "24 Morse code v2.mp3") 74 | add_file("audio_25", "25 Space Suit.mp3") 75 | add_file("audio_26", "26 Helmet v2.mp3") 76 | add_file("audio_27", "27 Hatch closing.mp3") 77 | add_file("audio_28", "28 Carabiner.mp3") 78 | add_file("audio_29", "29 Mission control center speaking v2.mp3") 79 | add_file("audio_30", "30 Water Landing v3.mp3") 80 | 81 | for key, value in url_audios.items(): 82 | audios_tags[key] = value.get_tag() 83 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/phrases.py: -------------------------------------------------------------------------------- 1 | from . import button_menu, utils 2 | 3 | NOT_EXIT_PHRASES = utils.prepare_phrases_list( 4 | [ 5 | "да", 6 | "давай", 7 | "конечно", 8 | "конечно же", 9 | "ага", 10 | "возможно", 11 | "определенно да", 12 | "точно", 13 | "так", 14 | "именно так", 15 | "без сомнений", 16 | "абсолютно точно", 17 | "верно", 18 | "ну да", 19 | "скорее всего", 20 | "наверное", 21 | "естественно", 22 | "а то", 23 | "как же иначе", 24 | "без вариантов", 25 | "только так", 26 | "сто процентов", 27 | "по-другому и быть не может", 28 | "вероятно", 29 | "хорошо", 30 | "так точно", 31 | "начнем", 32 | "да, давай", 33 | "запускай", 34 | "включай", 35 | "начинай", 36 | "помогу", 37 | "поможем", 38 | "спасем", 39 | "спасем королевство", 40 | "помогу принцессе", 41 | "едем", 42 | "помогу", 43 | "пое", 44 | "поможем", 45 | "да", 46 | "спасем", 47 | "да", 48 | "спасу", 49 | "ага", 50 | "да да", 51 | "да да да", 52 | "поехали", 53 | "едем", 54 | "поедем", 55 | "хорошо", 56 | "отлично", 57 | "согласен", 58 | "согласна", 59 | "давайте", 60 | "давай", 61 | "да", 62 | "хорошо", 63 | "давай сыграем", 64 | "вперед", 65 | "окей", 66 | "начинаем", 67 | "старт", 68 | ] 69 | ) 70 | 71 | SIMPLE_REPEAT_PHRASES = utils.prepare_phrases_list( 72 | [ 73 | "повтори", 74 | "давай еще раз", 75 | "озвучь еще раз", 76 | "повтори загадку", 77 | "расскажи еще раз", 78 | "повтори еще раз", 79 | "еще раз", 80 | "расскажи второй раз", 81 | "я не понял", 82 | ] 83 | ) 84 | 85 | FULL_REPEAT_PHRASES = utils.prepare_phrases_list(["повтори всё"]) 86 | 87 | 88 | HELLO_PHRASES = [ 89 | [ 90 | "Приглашаю в космическое путешествие, где вас ждут пять экспедиций по Солнечной системе! Вы исследуете планеты, астероиды и кометы. Ну, что — поехали?", 91 | "Приглашаю в космическое путешествие, где вас ждут пять экспедиций по Солнечной системе! Вы исследуете планеты, астероиды и кометы. Ну, что — ^поехали?^", 92 | ] 93 | ] 94 | 95 | QUEST_COMPLETE_PHRASE = ( 96 | "Так закончилась сказка. Пока что закончилась. Хочешь ли ты, чтобы она прямо сейчас началась снова, сложилась иначе? Скажи: да или нет?", 97 | "Так закончилась сказка. Пока что закончилась. Хочешь ли ты, чтобы она прямо сейчас началась снова, сложилась иначе? Скажи: да или нет?", 98 | ) 99 | 100 | HAVE_SAVED_PHRASES = [ 101 | ( 102 | "Вы не завершили ваше космическое путешествие. Хотите продолжить или начать с начала?", 103 | "Вы не завершили ваше космическое путешествие. Хотите продолжить или начать с начала?", 104 | ) 105 | ] 106 | 107 | HAVE_SAVED_ANSWERS_CONTINUE = utils.prepare_phrases_list( 108 | [ 109 | "да", 110 | "продолжим", 111 | "продолжить", 112 | "продолжаем", 113 | "продолжим историю", 114 | "продолжить историю", 115 | "продолжим игру", 116 | "продолжить игру", 117 | "продолжим игру", 118 | "продолжим квест", 119 | "продолжить квест. играем дальше", 120 | "история дальше", 121 | "квест дальше", 122 | "дальше", 123 | ] 124 | ) 125 | 126 | HAVE_SAVED_ANSWERS_NEW = utils.prepare_phrases_list( 127 | [ 128 | "сначала", 129 | "с начала", 130 | "заново", 131 | "давай с начала", 132 | "давай заново", 133 | "по новой", 134 | "новая игра", 135 | "новая история", 136 | ] 137 | ) 138 | 139 | HAVE_SAVED_ANSWERS_EXIT = utils.prepare_phrases_list(["нет", "стоп", "хватит", "выйти"]) 140 | 141 | HAVE_SAVED_BUTTONS = button_menu.ButtonsMenuBuilder( 142 | "hello_state", [{"text": "Продолжить"}, {"text": "С начала"}] 143 | ).get_to_send() 144 | 145 | STOP_PHRASES = utils.prepare_phrases_list(["on_interrupt", "стоп", "хватит"]) 146 | 147 | HELLO_BUTTONS = button_menu.ButtonsMenuBuilder( 148 | "hello_state", [{"text": "Да"}, {"text": "Нет"}] 149 | ).get_to_send() 150 | 151 | EXIT_QUESTION_BUTTONS = HELLO_BUTTONS 152 | 153 | GOODBYE_PHRASES = [ 154 | ("До новых приключений!", "До новых приключений!"), 155 | ("Была рада с вами поиграть!", "Был+а р+ада с в+ами поигр+ать!"), 156 | ("До новых встреч!", "До н+овых встр+еч!"), 157 | ] 158 | -------------------------------------------------------------------------------- /src/skill_cosmo_quest/application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import configparser 3 | import logging 4 | import os 5 | import os.path 6 | import random 7 | import traceback 8 | from pathlib import Path 9 | from typing import Dict, Optional 10 | 11 | from aiohttp import web 12 | 13 | from . import audios, graphite_statistics, phrases, quest, sessions, utils 14 | 15 | logging.basicConfig(level=logging.DEBUG) 16 | logging.info("Start cosmo_quest_skill") 17 | 18 | PACKAGE_DIR = Path(__file__).parent 19 | 20 | config_paths = [ 21 | Path("/config/skill_config.cfg"), 22 | PACKAGE_DIR.parent.parent / "skill_config.cfg", 23 | ] 24 | 25 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 26 | config = configparser.ConfigParser() 27 | is_found: bool = False 28 | for config_path in config_paths: 29 | if config_path.exists(): 30 | config.read(config_path) 31 | is_found = True 32 | break 33 | 34 | if not is_found: 35 | raise Exception(f"Config not found in {config_paths}") 36 | 37 | 38 | HOST_IP = config.get("main", "host") 39 | HOST_PORT = int(config.get("main", "port")) 40 | SESSION_LIFE_TIME_SEC = int(config.get("main", "session_life_time_sec")) 41 | AUDIO_FILES_PATH = config.get("main", "audio_files_path") 42 | 43 | GRAPHITE_HOST = config.get("graphite", "host") 44 | GRAPHITE_PREFIX = config.get("graphite", "prefix") 45 | GRAPHITE_INTERVAL = int(config.get("graphite", "interval")) 46 | 47 | graphite_sender = graphite_statistics.GraphiteSender(GRAPHITE_HOST, GRAPHITE_PREFIX) 48 | 49 | STATE_HELLO = 0 50 | STATE_QUEST = 1 51 | STATE_QUEST_COMPLETED = 2 52 | STATE_HAVE_SAVED_QEUSTION = 3 53 | 54 | 55 | async def marusya_cosmo_quest(request_data) -> web.Response: 56 | global graphite_sender 57 | clear_session = False 58 | request = await request_data.json() 59 | logging.info(f"Request: \n {request}\n\n") 60 | 61 | response = {} 62 | response["version"] = request["version"] 63 | response["session"] = request["session"] 64 | response["response"] = {"end_session": False} 65 | user_session = sessions.get_session(request["session"]["user_id"]) 66 | 67 | if request["session"]["new"]: 68 | if "current_state" in user_session.info and ( 69 | user_session.info["current_state"] == STATE_QUEST 70 | or user_session.info["current_state"] == STATE_HAVE_SAVED_QEUSTION 71 | ): 72 | utils.set_response( 73 | response, 74 | text_and_tts=random.choice(phrases.HAVE_SAVED_PHRASES), 75 | buttons=phrases.HAVE_SAVED_BUTTONS, 76 | ) 77 | user_session.info["current_state"] = STATE_HAVE_SAVED_QEUSTION 78 | else: 79 | utils.set_response( 80 | response, 81 | text_and_tts=random.choice(phrases.HELLO_PHRASES), 82 | buttons=phrases.HELLO_BUTTONS, 83 | ) 84 | user_session.info["current_state"] = STATE_HELLO 85 | user_session.info["user_vars"] = {} 86 | user_session.info["user_vars"]["callsign"] = "DEFAULT" 87 | graphite_sender.inc("quest_start") 88 | print("save hello") 89 | else: 90 | prepared_text: Optional[str] = utils.get_prepared_text(request) 91 | 92 | error_response = False 93 | if prepared_text is not None: 94 | if prepared_text in phrases.STOP_PHRASES: 95 | utils.set_response( 96 | response, text_and_tts=random.choice(phrases.GOODBYE_PHRASES) 97 | ) 98 | 99 | response["response"]["end_session"] = True 100 | else: 101 | try: 102 | play_audio: bool = True 103 | current_state: int = user_session.info["current_state"] 104 | current_stage: Optional[quest.Stage] = None 105 | 106 | # Start message answer 107 | if ( 108 | current_state == STATE_HELLO 109 | or current_state == STATE_QUEST_COMPLETED 110 | ): 111 | if prepared_text not in phrases.NOT_EXIT_PHRASES: 112 | utils.set_response( 113 | response, 114 | text_and_tts=random.choice(phrases.GOODBYE_PHRASES), 115 | ) 116 | clear_session = True 117 | response["response"]["end_session"] = True 118 | else: 119 | current_stage = quest.get_root_stage() 120 | user_session.info["current_state"] = STATE_QUEST 121 | elif current_state == STATE_HAVE_SAVED_QEUSTION: 122 | if prepared_text in phrases.HAVE_SAVED_ANSWERS_NEW: 123 | current_stage = quest.get_root_stage() 124 | user_session.info["current_state"] = STATE_QUEST 125 | elif prepared_text in phrases.HAVE_SAVED_ANSWERS_EXIT: 126 | utils.set_response( 127 | response, 128 | text_and_tts=random.choice(phrases.GOODBYE_PHRASES), 129 | ) 130 | response["response"]["end_session"] = True 131 | else: 132 | current_stage = quest.get_stage_by_id( 133 | user_session.info["current_stage"] 134 | ) 135 | user_session.info["current_state"] = STATE_QUEST 136 | else: 137 | if prepared_text in phrases.SIMPLE_REPEAT_PHRASES: 138 | current_stage = quest.get_stage_by_id( 139 | user_session.info["current_stage"] 140 | ) 141 | play_audio = False 142 | elif prepared_text in phrases.FULL_REPEAT_PHRASES: 143 | current_stage = quest.get_stage_by_id( 144 | user_session.info["current_stage"] 145 | ) 146 | else: 147 | transition = quest.get_stage_by_id( 148 | user_session.info["current_stage"] 149 | ).get_transition(prepared_text) 150 | 151 | if transition is not None: 152 | current_stage = transition.get_dest_stage() 153 | transition.on_go(prepared_text, user_session.info) 154 | else: 155 | current_stage = None 156 | 157 | if not response["response"]["end_session"]: 158 | response_text_and_tts = [[], ""] 159 | 160 | if current_stage is not None: 161 | current_stage_strong: quest.Stage = current_stage 162 | current_stage_strong.add_response_text_and_tts( 163 | response_text_and_tts, 164 | user_session.info["user_vars"], 165 | play_audio, 166 | ) 167 | 168 | # Переходим в следующее состояние, если текущее не требует ввода пользователя 169 | while ( 170 | current_stage_strong.is_unconditional() 171 | and not current_stage_strong.is_end() 172 | ): 173 | transition = current_stage_strong.get_transition() 174 | current_stage = transition.get_dest_stage() 175 | transition.on_go(None, user_session.info) 176 | if current_stage is not None: 177 | current_stage_strong = current_stage 178 | else: 179 | raise ValueError( 180 | "The next stage of non and stage was None" 181 | ) 182 | 183 | current_stage_strong.add_response_text_and_tts( 184 | response_text_and_tts, 185 | user_session.info["user_vars"], 186 | play_audio, 187 | ) 188 | 189 | buttons = current_stage_strong.buttons 190 | 191 | if current_stage_strong.is_end(): 192 | """ 193 | response_text_and_tts[ 194 | 0 195 | ] += f"\n{phrases.QUEST_COMPLETE_PHRASE[0]}" 196 | response_text_and_tts[1] += ( 197 | "\n" + phrases.QUEST_COMPLETE_PHRASE[1] 198 | ) 199 | """ 200 | buttons = phrases.EXIT_QUESTION_BUTTONS 201 | user_session.info[ 202 | "current_state" 203 | ] = STATE_QUEST_COMPLETED 204 | 205 | clear_session = True 206 | response["response"]["end_session"] = True 207 | graphite_sender.inc("all_complete") 208 | 209 | utils.set_response( 210 | response, 211 | text_and_tts=( 212 | response_text_and_tts[0], 213 | response_text_and_tts[1], 214 | ), 215 | buttons=buttons, 216 | ) 217 | 218 | user_session.info["current_stage"] = current_stage_strong.id 219 | else: 220 | error_response = True 221 | 222 | except Exception: 223 | traceback.print_exc() 224 | error_response = True 225 | clear_session = True 226 | else: 227 | error_response = True 228 | 229 | if error_response: 230 | utils.set_response( 231 | response, 232 | text="Простите, я не могу ответить на ваш запрос", 233 | tts="Простите, я не могу ответить на ваш запрос", 234 | ) 235 | response["response"]["end_session"] = True 236 | 237 | # if response["response"]["end_session"]: 238 | # user_session.remove() 239 | 240 | print(f"Current info: {user_session.info}") 241 | 242 | if clear_session: 243 | user_session.remove() 244 | else: 245 | user_session.update() 246 | logging.info(f"Response: \n {response}\n\n") 247 | return web.json_response(response) 248 | 249 | 250 | async def get_main(request_data) -> web.StreamResponse: 251 | graphite_sender.inc("query_get") 252 | return web.json_response({"status": "OK", "tag": "2"}) 253 | 254 | 255 | async def get_readiness_probe(request_data) -> web.StreamResponse: 256 | return web.json_response(healthz()) 257 | 258 | 259 | async def get_liveness_probe(request_data) -> web.StreamResponse: 260 | return web.json_response(healthz()) 261 | 262 | 263 | async def get_startup_probe(request_data) -> web.StreamResponse: 264 | return web.json_response(healthz()) 265 | 266 | 267 | def healthz() -> Dict: 268 | return {"status": "OK"} 269 | 270 | 271 | def init_app(loop) -> web.Application: 272 | app = web.Application(loop=loop, client_max_size=100000000) 273 | app.router.add_post("/skill_cosmo_quest", marusya_cosmo_quest) 274 | app.router.add_post("/", marusya_cosmo_quest) 275 | app.router.add_get("/", get_main) 276 | app.router.add_get("/skill_cosmo_quest", get_main) 277 | app.router.add_get("/readiness_probe", get_readiness_probe) 278 | app.router.add_get("/liveness_probe", get_liveness_probe) 279 | app.router.add_get("/startup_probe", get_startup_probe) 280 | 281 | return app 282 | 283 | 284 | loop = asyncio.get_event_loop() 285 | app = init_app(loop) 286 | quest.init() 287 | audios.init(AUDIO_FILES_PATH) 288 | 289 | 290 | if __name__ == "__main__": 291 | try: 292 | graphite_sender.add_loop_task(loop, GRAPHITE_INTERVAL) 293 | web.run_app(app, host=HOST_IP, port=HOST_PORT) 294 | except web.GracefulExit: 295 | print("server was stopped") 296 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7bf56ade01f03dabb3134a389e80b956345beb7da94a43d693136570a3140b51" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aiohttp": { 20 | "hashes": [ 21 | "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", 22 | "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", 23 | "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", 24 | "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", 25 | "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", 26 | "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", 27 | "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", 28 | "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", 29 | "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", 30 | "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", 31 | "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", 32 | "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", 33 | "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", 34 | "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", 35 | "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", 36 | "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", 37 | "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", 38 | "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", 39 | "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", 40 | "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", 41 | "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", 42 | "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", 43 | "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", 44 | "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", 45 | "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", 46 | "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", 47 | "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", 48 | "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", 49 | "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", 50 | "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", 51 | "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", 52 | "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", 53 | "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", 54 | "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", 55 | "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", 56 | "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", 57 | "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" 58 | ], 59 | "index": "pypi", 60 | "version": "==3.7.4.post0" 61 | }, 62 | "async-timeout": { 63 | "hashes": [ 64 | "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", 65 | "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" 66 | ], 67 | "markers": "python_full_version >= '3.5.3'", 68 | "version": "==3.0.1" 69 | }, 70 | "attrs": { 71 | "hashes": [ 72 | "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", 73 | "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 74 | ], 75 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 76 | "version": "==20.3.0" 77 | }, 78 | "chardet": { 79 | "hashes": [ 80 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 81 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 82 | ], 83 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 84 | "version": "==4.0.0" 85 | }, 86 | "graphyte": { 87 | "hashes": [ 88 | "sha256:ab1fd5a0061cd8f412d379273e36dfb4b80906c4c804052cfe3aa1ed416dc072" 89 | ], 90 | "index": "pypi", 91 | "version": "==1.6.0" 92 | }, 93 | "idna": { 94 | "hashes": [ 95 | "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", 96 | "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" 97 | ], 98 | "markers": "python_version >= '3.4'", 99 | "version": "==3.1" 100 | }, 101 | "idna-ssl": { 102 | "hashes": [ 103 | "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" 104 | ], 105 | "markers": "python_version < '3.7'", 106 | "version": "==1.1.0" 107 | }, 108 | "multidict": { 109 | "hashes": [ 110 | "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", 111 | "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", 112 | "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", 113 | "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", 114 | "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", 115 | "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", 116 | "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", 117 | "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", 118 | "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", 119 | "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", 120 | "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", 121 | "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", 122 | "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", 123 | "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", 124 | "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", 125 | "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", 126 | "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", 127 | "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", 128 | "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", 129 | "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", 130 | "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", 131 | "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", 132 | "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", 133 | "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", 134 | "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", 135 | "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", 136 | "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", 137 | "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", 138 | "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", 139 | "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", 140 | "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", 141 | "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", 142 | "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", 143 | "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", 144 | "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", 145 | "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", 146 | "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" 147 | ], 148 | "markers": "python_version >= '3.6'", 149 | "version": "==5.1.0" 150 | }, 151 | "typing-extensions": { 152 | "hashes": [ 153 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 154 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 155 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 156 | ], 157 | "markers": "python_version < '3.8'", 158 | "version": "==3.7.4.3" 159 | }, 160 | "yarl": { 161 | "hashes": [ 162 | "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", 163 | "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", 164 | "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", 165 | "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", 166 | "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", 167 | "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", 168 | "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", 169 | "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", 170 | "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", 171 | "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", 172 | "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", 173 | "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", 174 | "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", 175 | "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", 176 | "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", 177 | "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", 178 | "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", 179 | "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", 180 | "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", 181 | "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", 182 | "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", 183 | "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", 184 | "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", 185 | "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", 186 | "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", 187 | "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", 188 | "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", 189 | "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", 190 | "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", 191 | "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", 192 | "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", 193 | "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", 194 | "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", 195 | "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", 196 | "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", 197 | "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", 198 | "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" 199 | ], 200 | "markers": "python_version >= '3.6'", 201 | "version": "==1.6.3" 202 | } 203 | }, 204 | "develop": { 205 | "appdirs": { 206 | "hashes": [ 207 | "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", 208 | "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" 209 | ], 210 | "version": "==1.4.4" 211 | }, 212 | "atomicwrites": { 213 | "hashes": [ 214 | "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", 215 | "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" 216 | ], 217 | "markers": "sys_platform == 'win32'", 218 | "version": "==1.4.0" 219 | }, 220 | "attrs": { 221 | "hashes": [ 222 | "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", 223 | "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 224 | ], 225 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 226 | "version": "==20.3.0" 227 | }, 228 | "black": { 229 | "hashes": [ 230 | "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" 231 | ], 232 | "index": "pypi", 233 | "version": "==20.8b1" 234 | }, 235 | "click": { 236 | "hashes": [ 237 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 238 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 239 | ], 240 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 241 | "version": "==7.1.2" 242 | }, 243 | "colorama": { 244 | "hashes": [ 245 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 246 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 247 | ], 248 | "markers": "sys_platform == 'win32'", 249 | "version": "==0.4.4" 250 | }, 251 | "dataclasses": { 252 | "hashes": [ 253 | "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", 254 | "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" 255 | ], 256 | "markers": "python_version < '3.7'", 257 | "version": "==0.8" 258 | }, 259 | "flake8": { 260 | "hashes": [ 261 | "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff", 262 | "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0" 263 | ], 264 | "index": "pypi", 265 | "version": "==3.9.0" 266 | }, 267 | "importlib-metadata": { 268 | "hashes": [ 269 | "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", 270 | "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" 271 | ], 272 | "markers": "python_version < '3.8'", 273 | "version": "==3.10.0" 274 | }, 275 | "iniconfig": { 276 | "hashes": [ 277 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 278 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 279 | ], 280 | "version": "==1.1.1" 281 | }, 282 | "isort": { 283 | "hashes": [ 284 | "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", 285 | "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" 286 | ], 287 | "index": "pypi", 288 | "version": "==5.8.0" 289 | }, 290 | "mccabe": { 291 | "hashes": [ 292 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 293 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 294 | ], 295 | "version": "==0.6.1" 296 | }, 297 | "mypy": { 298 | "hashes": [ 299 | "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", 300 | "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", 301 | "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", 302 | "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", 303 | "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", 304 | "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", 305 | "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", 306 | "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", 307 | "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", 308 | "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", 309 | "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", 310 | "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", 311 | "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", 312 | "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", 313 | "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", 314 | "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", 315 | "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", 316 | "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", 317 | "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", 318 | "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", 319 | "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", 320 | "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" 321 | ], 322 | "index": "pypi", 323 | "version": "==0.812" 324 | }, 325 | "mypy-extensions": { 326 | "hashes": [ 327 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 328 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 329 | ], 330 | "version": "==0.4.3" 331 | }, 332 | "packaging": { 333 | "hashes": [ 334 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 335 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 336 | ], 337 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 338 | "version": "==20.9" 339 | }, 340 | "pathspec": { 341 | "hashes": [ 342 | "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", 343 | "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" 344 | ], 345 | "version": "==0.8.1" 346 | }, 347 | "pluggy": { 348 | "hashes": [ 349 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 350 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 351 | ], 352 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 353 | "version": "==0.13.1" 354 | }, 355 | "py": { 356 | "hashes": [ 357 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 358 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 359 | ], 360 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 361 | "version": "==1.10.0" 362 | }, 363 | "pycodestyle": { 364 | "hashes": [ 365 | "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", 366 | "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" 367 | ], 368 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 369 | "version": "==2.7.0" 370 | }, 371 | "pyflakes": { 372 | "hashes": [ 373 | "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", 374 | "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" 375 | ], 376 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 377 | "version": "==2.3.1" 378 | }, 379 | "pyparsing": { 380 | "hashes": [ 381 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 382 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 383 | ], 384 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 385 | "version": "==2.4.7" 386 | }, 387 | "pytest": { 388 | "hashes": [ 389 | "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", 390 | "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" 391 | ], 392 | "index": "pypi", 393 | "version": "==6.2.2" 394 | }, 395 | "pytest-asyncio": { 396 | "hashes": [ 397 | "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", 398 | "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" 399 | ], 400 | "index": "pypi", 401 | "version": "==0.14.0" 402 | }, 403 | "regex": { 404 | "hashes": [ 405 | "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139", 406 | "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5", 407 | "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa", 408 | "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3", 409 | "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df", 410 | "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f", 411 | "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e", 412 | "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd", 413 | "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d", 414 | "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e", 415 | "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f", 416 | "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa", 417 | "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68", 418 | "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643", 419 | "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3", 420 | "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be", 421 | "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578", 422 | "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c", 423 | "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5", 424 | "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba", 425 | "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe", 426 | "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c", 427 | "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a", 428 | "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb", 429 | "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d", 430 | "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38", 431 | "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18", 432 | "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce", 433 | "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa", 434 | "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6", 435 | "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5", 436 | "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90", 437 | "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c", 438 | "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106", 439 | "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7", 440 | "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0", 441 | "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689", 442 | "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd", 443 | "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932", 444 | "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf", 445 | "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14" 446 | ], 447 | "version": "==2021.3.17" 448 | }, 449 | "toml": { 450 | "hashes": [ 451 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 452 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 453 | ], 454 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 455 | "version": "==0.10.2" 456 | }, 457 | "typed-ast": { 458 | "hashes": [ 459 | "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", 460 | "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", 461 | "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", 462 | "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", 463 | "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", 464 | "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", 465 | "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", 466 | "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", 467 | "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", 468 | "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", 469 | "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", 470 | "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", 471 | "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", 472 | "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", 473 | "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", 474 | "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", 475 | "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", 476 | "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", 477 | "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", 478 | "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", 479 | "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", 480 | "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", 481 | "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", 482 | "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", 483 | "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", 484 | "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", 485 | "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", 486 | "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", 487 | "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", 488 | "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" 489 | ], 490 | "version": "==1.4.2" 491 | }, 492 | "typing-extensions": { 493 | "hashes": [ 494 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 495 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 496 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 497 | ], 498 | "markers": "python_version < '3.8'", 499 | "version": "==3.7.4.3" 500 | }, 501 | "zipp": { 502 | "hashes": [ 503 | "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", 504 | "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" 505 | ], 506 | "markers": "python_version >= '3.6'", 507 | "version": "==3.4.1" 508 | } 509 | } 510 | } 511 | --------------------------------------------------------------------------------