├── 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 |
--------------------------------------------------------------------------------