├── .gitignore ├── LICENSE.txt ├── README.md ├── alice_scripts ├── __init__.py ├── modifiers.py ├── requests.py ├── say.py └── skill.py ├── examples ├── guess_number.py ├── guess_number_subgens.py ├── scaling │ ├── docker-compose.yml │ ├── nginx │ │ ├── Dockerfile │ │ └── nginx.conf │ └── web │ │ ├── Dockerfile │ │ ├── guess_number.py │ │ ├── gunicorn.conf.py │ │ └── requirements.txt └── survey.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Created by https://www.gitignore.io/api/python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | ### Python Patch ### 112 | .venv/ 113 | 114 | 115 | # End of https://www.gitignore.io/api/python 116 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Alexander Borzunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | alice_scripts 2 | ============= 3 | 4 | Простой способ создавать сложные сценарии для [Яндекс.Алисы](https://dialogs.yandex.ru/) 5 | 6 | > Библиотека разработана сообществом и не является продуктом Яндекса 7 | 8 | ## 🚀 Быстрый старт 9 | 10 | Эта библиотека позволяет писать многоэтапные сценарии без callback-ов и ручного хранения информации о состоянии диалога. Достаточно использовать условия и циклы: 11 | 12 | ```python 13 | from alice_scripts import Skill, request, say, suggest 14 | skill = Skill(__name__) 15 | 16 | @skill.script 17 | def run_script(): 18 | yield say('Добрый день! Как вас зовут?') 19 | name = request.command 20 | 21 | yield say('Сколько вам лет?') 22 | while not request.matches(r'\d+'): 23 | yield say('Я вас не поняла. Скажите число') 24 | age = int(request.command) 25 | 26 | yield say('Вы любите кошек или собак?', 27 | suggest('Обожаю кошечек', 'Люблю собак')) 28 | while not request.has_lemmas('кошка', 'кошечка', 29 | 'собака', 'собачка'): 30 | yield say('У вас только два варианта - кошки или собаки') 31 | loves_cats = request.has_lemmas('кошка', 'кошечка') 32 | 33 | yield say(f'Рада познакомиться, {name}! Когда вам ' 34 | f'исполнится {age + 1}, я могу подарить ' 35 | f'{"котёнка" if loves_cats else "щенка"}!', 36 | end_session=True) 37 | ``` 38 | 39 | Запустить сценарий можно как обычное [Flask](http://flask.pocoo.org/)-приложение: 40 | 41 | pip install alice_scripts 42 | FLASK_APP=hello.py flask run --with-threads 43 | 44 | ## Примеры 45 | 46 | * [Примеры из документации](examples) 47 | * [Навык «Приложение для знакомств»](https://github.com/FuryThrue/WhoIsAlice/blob/master/app.py) 48 | 49 | ## 📖 Интерфейс 50 | 51 | ### Skill 52 | 53 | Класс `Skill` реализует WSGI-приложение и является наследником класса [flask.Flask](http://flask.pocoo.org/docs/1.0/api/#flask.Flask). Сценарий, соответствующий приложению, регистрируется с помощью декоратора `@skill.script` (см. пример выше). 54 | 55 | Сценарий запускается отдельно для каждого уникального значения `session_id`. 56 | 57 | ### yield say(...) 58 | 59 | Конструкция `yield say(...)` служит для выдачи ответа на запрос и принимает три типа параметров: 60 | 61 | - Неименованные строковые аргументы задают варианты фразы, которую нужно показать и сказать пользователю. При выполнении случайно выбирается один из вариантов: 62 | 63 | ```python 64 | yield say('Как дела?', 'Как вы?', 'Как поживаете?') 65 | ``` 66 | 67 | - Модификаторы (см. ниже) позволяют указать дополнительные свойства ответа. Например, модификатор `suggest` создаёт кнопки с подсказками для ответа: 68 | 69 | ```python 70 | yield say('Как дела?', suggest('Хорошо', 'Нормально', 'Не очень')) 71 | ``` 72 | 73 | - Именованные аргументы позволяют использовать те возможности [протокола](https://tech.yandex.ru/dialogs/alice/doc/protocol-docpage/#response), для которых нет модификаторов: 74 | 75 | ```python 76 | yield say('Здравствуйте! Это мы, хороводоведы.', 77 | tts='Здравствуйте! Это мы, хоров+одо в+еды.') 78 | ``` 79 | 80 | Переданные пары «ключ-значение» будут записаны в словарь `response` в ответе навыка. 81 | 82 | ### Модификаторы 83 | 84 | Модификаторы — это функции, возвращающие [замыкания](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BC%D1%8B%D0%BA%D0%B0%D0%BD%D0%B8%D0%B5_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)). При этом каждое замыкание должно принимать словарь `response` из [ответа](https://tech.yandex.ru/dialogs/alice/doc/protocol-docpage/#response) навыка и добавлять туда нужные ключи. 85 | 86 | - `suggest(...)` 87 | 88 | Создаёт кнопки с подсказками для ответа: 89 | 90 | ```python 91 | yield say('Как дела?', suggest('Хорошо', 'Нормально')) 92 | ``` 93 | 94 | > Так как библиотека находится в стадии proof of concept, других модификаторов пока не реализовано. Используйте именованные параметры в конструкции `yield say(...)`. 95 | 96 | ### request 97 | 98 | Объект `request` представляет собой thread-local хранилище, содержащее информацию о последнем действии пользователя в сессии. 99 | 100 | - С объектом `request` можно работать как со словарём, полученным из [запроса](https://tech.yandex.ru/dialogs/alice/doc/protocol-docpage/#request) к навыку: 101 | 102 | ```python 103 | original_utterance = request['request']['original_utterance'] 104 | ``` 105 | 106 | - `request.command` — свойство, содержащее значение поля [command](https://tech.yandex.ru/dialogs/alice/doc/protocol-docpage/#request), из которого убраны завершающие точки. 107 | 108 | - `request.matches(pattern, flags=0)` — метод, позволяющий проверить, удовлетворяет ли свойство `request.command` регулярному выражению `pattern` (используется функция [re.fullmatch](https://docs.python.org/3/library/re.html#re.fullmatch)). 109 | 110 | - `request.words` — свойство, содержащее все слова (и числа), найденные в поле [command](https://tech.yandex.ru/dialogs/alice/doc/protocol-docpage/#request). 111 | 112 | - `request.lemmas` — свойство, содержащее начальные формы слов из свойства `request.words` (полученные с помощью библиотеки [pymorphy2](http://pymorphy2.readthedocs.io/en/latest/)). 113 | 114 | - `request.has_lemmas(...)` — метод, позволяющий проверить, были ли в запросе слова, чьи начальные формы совпадают с начальными формами указанных слов: 115 | 116 | ```python 117 | if request.has_lemmas('нет', 'не'): 118 | answer = 'no' 119 | elif request.has_lemmas('да', 'ага'): 120 | answer = 'yes' 121 | ``` 122 | 123 | ## Разбиение на подпрограммы 124 | 125 | Сценарий можно (и нужно) разбивать на подпрограммы. Каждая подпрограмма *должна* вызываться с помощью оператора `yield from` и может возвращать значение с помощью оператора `return`. См. [пример](examples/guess_number_subgens.py). 126 | 127 | ## Развёртывание 128 | 129 | В этой библиотеке состояние диалога хранится в виде состояния Python-генератора и не может быть сериализовано. В связи с этим: 130 | 131 | - Реплики из одной сессии всегда должны обрабатываться одним и тем же процессом. 132 | - Навык не может быть запущен на serverless-платформе. 133 | - При перезапуске приложения все сессии будут разорваны. 134 | 135 | Развернуть приложение в production-е можно с помощью gunicorn. Вы можете использовать несколько [потоков](http://docs.gunicorn.org/en/stable/settings.html#threads), но не можете использовать несколько [воркеров](http://docs.gunicorn.org/en/stable/settings.html#workers) (иначе gunicorn будет направлять реплики из одной сессии разным процессам). 136 | 137 | ## Масштабирование 138 | 139 | Если у вашего навыка много пользователей и одного процесса недостаточно, чтобы успевать отвечать на запросы за требуемое время (по протоколу — не более 1,5 сек), можно поступить так: 140 | 141 | 1. Запустите несколько экземпляров gunicorn (в каждом — 1 воркер) на одном или нескольких серверах. 142 | 2. Настройте nginx таким образом, чтобы он направлял запросы с одним и тем же `session_id` к одному и тому же экземпляру gunicorn. 143 | 144 | Пример описанной конфигурации есть в [этой папке](examples/scaling). 145 | 146 | ## Автор 147 | 148 | Copyright © Александр Борзунов, 2018 149 | 150 | The MIT License (MIT) 151 | -------------------------------------------------------------------------------- /alice_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from .modifiers import * 2 | from .requests import * 3 | from .say import * 4 | from .skill import * 5 | 6 | 7 | __version__ = '0.2.post1' 8 | -------------------------------------------------------------------------------- /alice_scripts/modifiers.py: -------------------------------------------------------------------------------- 1 | __all__ = ['suggest'] 2 | 3 | 4 | def suggest(*options): 5 | def modifier(response): 6 | if 'buttons' not in response: 7 | response['buttons'] = [] 8 | 9 | response['buttons'] += [{'title': item, 'hide': True} 10 | for item in options] 11 | 12 | return modifier 13 | -------------------------------------------------------------------------------- /alice_scripts/requests.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import flask 4 | import pymorphy2 5 | from werkzeug.local import LocalProxy 6 | 7 | 8 | __all__ = ['Request', 'request'] 9 | 10 | 11 | morph = pymorphy2.MorphAnalyzer() 12 | 13 | 14 | class Request(dict): 15 | def __init__(self, dictionary): 16 | super().__init__(dictionary) 17 | 18 | self._command = self['request']['command'].rstrip('.') 19 | self._words = re.findall(r'[\w-]+', self._command, flags=re.UNICODE) 20 | self._lemmas = [morph.parse(word)[0].normal_form 21 | for word in self._words] 22 | 23 | @property 24 | def command(self): 25 | return self._command 26 | 27 | @property 28 | def words(self): 29 | return self._words 30 | 31 | def matches(self, pattern, flags=0): 32 | return re.fullmatch(pattern, self._command, flags) is not None 33 | 34 | @property 35 | def lemmas(self): 36 | return self._lemmas 37 | 38 | @property 39 | def session_id(self): 40 | return self['session']['session_id'] 41 | 42 | @property 43 | def user_id(self): 44 | return self['session']['user_id'] 45 | 46 | def has_lemmas(self, *lemmas): 47 | return any(morph.parse(item)[0].normal_form in self._lemmas 48 | for item in lemmas) 49 | 50 | 51 | request = LocalProxy(lambda: flask.g.request) 52 | -------------------------------------------------------------------------------- /alice_scripts/say.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | __all__ = ['say'] 5 | 6 | 7 | def say(*args, **kwargs): 8 | if not all(isinstance(item, str) or callable(item) 9 | for item in args): 10 | raise ValueError('Each argument of say(...) must be str or callable') 11 | 12 | response = kwargs.copy() 13 | 14 | phrases = [item for item in args if isinstance(item, str)] 15 | if phrases: 16 | response['text'] = random.choice(phrases) 17 | 18 | if 'end_session' not in response: 19 | response['end_session'] = False 20 | 21 | for item in args: 22 | if callable(item): 23 | item(response) 24 | 25 | return response 26 | -------------------------------------------------------------------------------- /alice_scripts/skill.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | 4 | import flask 5 | 6 | from .requests import Request 7 | 8 | 9 | __all__ = ['Skill'] 10 | 11 | 12 | class Skill(flask.Flask): 13 | def __init__(self, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | 16 | self._sessions = {} 17 | self._session_lock = threading.RLock() 18 | 19 | def script(self, generator): 20 | @self.route("/", methods=['POST']) 21 | def handle_post(): 22 | flask.g.request = Request(flask.request.get_json()) 23 | logging.debug('Request: %r', flask.g.request) 24 | 25 | content = self._switch_state(generator) 26 | response = { 27 | 'version': flask.g.request['version'], 28 | 'session': flask.g.request['session'], 29 | 'response': content, 30 | } 31 | 32 | logging.debug('Response: %r', response) 33 | return flask.jsonify(response) 34 | 35 | return generator 36 | 37 | def _switch_state(self, generator): 38 | session_id = flask.g.request['session']['session_id'] 39 | with self._session_lock: 40 | if session_id not in self._sessions: 41 | state = self._sessions[session_id] = generator() 42 | else: 43 | state = self._sessions[session_id] 44 | 45 | content = next(state) 46 | 47 | if content['end_session']: 48 | with self._session_lock: 49 | del self._sessions[session_id] 50 | return content 51 | -------------------------------------------------------------------------------- /examples/guess_number.py: -------------------------------------------------------------------------------- 1 | from alice_scripts import Skill, request, say, suggest 2 | 3 | 4 | skill = Skill(__name__) 5 | 6 | 7 | @skill.script 8 | def run_script(): 9 | yield say('Загадайте число от 1 до 100, а я его отгадаю. Готовы?') 10 | 11 | lo, hi = 1, 100 12 | while lo < hi: 13 | middle = (lo + hi) // 2 14 | yield say(f'Ваше число больше {middle}?', 15 | suggest('Ну да', 'Вроде нет')) 16 | 17 | while not request.has_lemmas('да', 'ага', 'нет', 'не'): 18 | yield say('Я вас не поняла. Скажите "да" или "нет"') 19 | 20 | if request.has_lemmas('нет', 'не'): 21 | hi = middle 22 | else: 23 | lo = middle + 1 24 | 25 | yield say(f'Думаю, вы загадали число {lo}!', end_session=True) 26 | -------------------------------------------------------------------------------- /examples/guess_number_subgens.py: -------------------------------------------------------------------------------- 1 | from alice_scripts import Skill, request, say, suggest 2 | 3 | 4 | skill = Skill(__name__) 5 | 6 | 7 | @skill.script 8 | def run_script(): 9 | yield from say_hi() 10 | 11 | lo, hi = 1, 100 12 | while lo < hi: 13 | middle = (lo + hi) // 2 14 | if (yield from ask_if_greater_than(middle)): 15 | lo = middle + 1 16 | else: 17 | hi = middle 18 | 19 | yield say(f'Думаю, вы загадали число {lo}!', end_session=True) 20 | 21 | 22 | def say_hi(): 23 | yield say('Загадайте число от 1 до 100, а я его отгадаю. Готовы?') 24 | 25 | 26 | def ask_if_greater_than(number): 27 | yield say(f'Ваше число больше {number}?', 28 | suggest('Ну да', 'Вроде нет')) 29 | 30 | while not request.has_lemmas('да', 'ага', 'нет', 'не'): 31 | yield say('Я вас не поняла. Скажите "да" или "нет"') 32 | 33 | return not request.has_lemmas('нет', 'не') 34 | -------------------------------------------------------------------------------- /examples/scaling/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | web1: 5 | build: ./web 6 | 7 | web2: 8 | build: ./web 9 | 10 | web3: 11 | build: ./web 12 | 13 | web4: 14 | build: ./web 15 | 16 | nginx: 17 | build: ./nginx 18 | ports: 19 | - "80:80" 20 | volumes: 21 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro 22 | -------------------------------------------------------------------------------- /examples/scaling/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM firesh/nginx-lua 2 | 3 | RUN apk update && apk add --no-cache lua5.1-cjson 4 | -------------------------------------------------------------------------------- /examples/scaling/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | init_by_lua_block { require "cjson" } 2 | 3 | upstream backend { 4 | hash $arg_session_id consistent; 5 | 6 | server web1; 7 | server web2; 8 | server web3; 9 | server web4; 10 | } 11 | 12 | server { 13 | location / { 14 | content_by_lua_block { 15 | ngx.req.read_body() 16 | local data = ngx.req.get_body_data() 17 | if not data then 18 | return ngx.exec('/balance') 19 | end 20 | 21 | local session_id = require "cjson".decode(data).session.session_id 22 | return ngx.exec('/balance', {session_id = session_id}) 23 | } 24 | } 25 | 26 | location /balance { 27 | internal; 28 | proxy_pass http://backend/; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/scaling/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir -p /app 6 | WORKDIR /app 7 | 8 | COPY requirements.txt /app/ 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | COPY . /app 12 | 13 | CMD gunicorn guess_number:skill --config gunicorn.conf.py 14 | 15 | EXPOSE 80 16 | -------------------------------------------------------------------------------- /examples/scaling/web/guess_number.py: -------------------------------------------------------------------------------- 1 | from alice_scripts import Skill, request, say, suggest 2 | 3 | 4 | skill = Skill(__name__) 5 | 6 | 7 | @skill.script 8 | def run_script(): 9 | yield say('Загадайте число от 1 до 100, а я его отгадаю. Готовы?') 10 | 11 | lo, hi = 1, 100 12 | while lo < hi: 13 | middle = (lo + hi) // 2 14 | yield say(f'Ваше число больше {middle}?', 15 | suggest('Ну да', 'Вроде нет')) 16 | 17 | while not request.has_lemmas('да', 'ага', 'нет', 'не'): 18 | yield say('Я вас не поняла. Скажите "да" или "нет"') 19 | 20 | if request.has_lemmas('нет', 'не'): 21 | hi = middle 22 | else: 23 | lo = middle + 1 24 | 25 | yield say(f'Думаю, вы загадали число {lo}!', end_session=True) 26 | -------------------------------------------------------------------------------- /examples/scaling/web/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | user = 'www-data' 2 | group = 'www-data' 3 | bind = '0.0.0.0:80' 4 | workers = 1 5 | threads = 4 6 | accesslog = '-' 7 | errorlog = '-' 8 | -------------------------------------------------------------------------------- /examples/scaling/web/requirements.txt: -------------------------------------------------------------------------------- 1 | alice_scripts==0.2 2 | gunicorn==19.9.0 3 | -------------------------------------------------------------------------------- /examples/survey.py: -------------------------------------------------------------------------------- 1 | from alice_scripts import Skill, request, say, suggest 2 | 3 | 4 | skill = Skill(__name__) 5 | 6 | 7 | @skill.script 8 | def run_script(): 9 | yield say('Добрый день! Как вас зовут?') 10 | name = request.command 11 | 12 | yield say('Сколько вам лет?') 13 | while not request.matches(r'\d+'): 14 | yield say('Я вас не поняла. Скажите число') 15 | age = int(request.command) 16 | 17 | yield say('Вы любите кошек или собак?', 18 | suggest('Обожаю кошечек', 'Люблю собак')) 19 | while not request.has_lemmas('кошка', 'кошечка', 20 | 'собака', 'собачка'): 21 | yield say('У вас только два варианта - кошки или собаки') 22 | loves_cats = request.has_lemmas('кошка', 'кошечка') 23 | 24 | yield say(f'Рада познакомиться, {name}! Когда вам ' 25 | f'исполнится {age + 1}, я могу подарить ' 26 | f'{"котёнка" if loves_cats else "щенка"}!', 27 | end_session=True) 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | from setuptools import setup 7 | 8 | 9 | project_dir = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | with open(os.path.join(project_dir, 'alice_scripts', '__init__.py')) as f: 12 | version = re.search(r"__version__ = '(.+)'", f.read()).groups()[0] 13 | 14 | with open(os.path.join(project_dir, 'README.md'), encoding='utf-8') as f: 15 | long_description = f.read() 16 | 17 | setup( 18 | name='alice_scripts', 19 | version=version, 20 | packages=['alice_scripts'], 21 | 22 | install_requires=['Flask>=0.12.2', 'pymorphy2>=0.8', 'werkzeug>=0.14.1'], 23 | 24 | author='Alexander Borzunov', 25 | author_email='borzunov.alexander@gmail.com', 26 | 27 | description='Simple way to create complex scripts for Yandex.Alice', 28 | long_description=long_description, 29 | url='http://github.com/borzunov/alice_scripts', 30 | 31 | classifiers=[ 32 | 'Development Status :: 4 - Beta', 33 | 'Environment :: Console', 34 | 'Framework :: AsyncIO', 35 | 'Intended Audience :: Developers', 36 | 'Intended Audience :: System Administrators', 37 | 'License :: OSI Approved :: MIT License', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python', 40 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 41 | ], 42 | license='MIT', 43 | keywords=['yandex-alice'], 44 | ) 45 | --------------------------------------------------------------------------------