State machine python aiogram

Урок 3. Машина состояний и то самое логгирование

Важно! Не забывайте обновлять библиотеку командой python3.6 -m pip install -U aiogram , так как разработчик постоянно обновляет её, избавляя от различных багов. Например в последнем релизе исправлены ошибки Fatal Python error: PyImport_GetModuleDict: no module dictionary! и передача списков в фильтр состояний.

Урок проводится с использованием aiogram версии 1.2

Сегодня мы научимся использовать:

Традиционно код урока доступен на GitHub

Создаем состояния

Так как мы сейчас будем разбирать машину состояний, эти самые состояния необходимо сначала создать. Сперва в голову приходит использование енумов, однако я предпочел воспользоваться встроенным классом Helper, который подходит для данной задачи даже лучше.

Итак, запишем в файл utils.py наш демонстрационный класс с состояниями:

from aiogram.utils.helper import Helper, HelperMode, ListItem class TestStates(Helper): mode = HelperMode.snake_case TEST_STATE_0 = ListItem() TEST_STATE_1 = ListItem() TEST_STATE_2 = ListItem() TEST_STATE_3 = ListItem() TEST_STATE_4 = ListItem() TEST_STATE_5 = ListItem() 

Обращу внимание читателя на то, что здесь нам интересно использовать именно ListItem , а не Item , так как в таком случае мы сможем использовать сложение разных состояний для передачи в handler (об этом дальше).

Если вы ещё не знакомы с классом Helper , то советую позже ознакомиться, а сейчас можно просто посмотреть на значения всех элементов (или каждого по отдельности, если захотите):

print(TestStates.all()) # ['test_state_0', 'test_state_1', 'test_state_2', 'test_state_3', 'test_state_4', 'test_state_5'] 

Так как в этот раз нам придется отправлять много сообщений, для разнообразия вынесем их в messages.py — для красоты.

Читайте также:  Css pseudo classes visible

Ещё не забываем добавить в config.py токен своего бота и мы готовы писать логику!

Указываем хранилище состояний и включаем логгирование

К привычным с прошлых уроков импортам у нас добавляется ещё парочка, а именно:

from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.contrib.middlewares.logging import LoggingMiddleware 
dp = Dispatcher(bot, storage=MemoryStorage()) dp.middleware.setup(LoggingMiddleware()) 

На первой строчке мы указали хранилище состояний в оперативной памяти, так как потеря этих состояний нам не страшна (да и этот вариант больше всего подходит для демонстрационных целей, так как не требует настройки). Однако если у вас от состояний что-то зависит, рекомендуется ипользовать более надеждное хранилище. На данный момент можно подключить Redis и RethinkDB.

На второй строчке подключаем логгирование. На нем долго останавливаться не буду — лучше проверьте его работу самостоятельно: например, можно добавить хэндлер только на текстовые сообщения, тогда при отправке стикера, фото (и т.п.), поста в канал, где бот является администратором, в логах увидите, что эти апдейты не отработаны никаким хэндлером.

Обрабатываем входящие сообщения

Итак, по традиции добавляем обработчики команд start и help :

@dp.message_handler(commands=['start']) async def process_start_command(message: types.Message): await message.reply(MESSAGES['start']) @dp.message_handler(commands=['help']) async def process_help_command(message: types.Message): await message.reply(MESSAGES['help']) 

А так же «ловим» все сообщения, отправленные при «нулевом» состоянии:

@dp.message_handler() async def echo_message(msg: types.Message): await bot.send_message(msg.from_user.id, msg.text) 

Переходим к главной теме нашего урока: состояниям

Эти самые состояния нужно как-то устанавливать, поэтому сделаем так:

@dp.message_handler(state='*', commands=['setstate']) async def process_setstate_command(message: types.Message): argument = message.get_args() state = dp.current_state(user=message.from_user.id) if not argument: await state.reset_state() return await message.reply(MESSAGES['state_reset']) if (not argument.isdigit()) or (not int(argument) < len(TestStates.all())): return await message.reply(MESSAGES['invalid_key'].format(key=argument)) await state.set_state(TestStates.all()[int(argument)]) await message.reply(MESSAGES['state_change'], reply=False) 

Не забываем, что хэндлеры обрабатываются в порядке их расположения в коде, поэтому описанная выше функция должна идти раньше приведенных ниже обработчиков.

В функции process_setstate_command мы проверяем, идут ли с командой какие-то аргументы и запрашиваем текущее состояние пользователя. Если никаких аргументов с командой не идёт, сбрасываем состояние. Если же аргументы есть, то проверяем, чтобы те соответствовали нашему условию: не отрицательное число (минус в строке не пройдет валидацию .isdigit() ), которое меньше количества всех состояний. Если аргумент не подходит, сообщаем пользователю об этом. В ином случае устанавливаем новое текущее состояние и даем фидбек на действие сообщением. Обращу внимание читателя также на конструкцию message.reply(‘Some text’, reply=False) . Указав reply=False мы используем шорткат, позволяющий нам ответить в тот же чат, не доставая айди пользователя / чата как, например, вот тут.

Теперь отрабатываем входящие сообщения при выбранном состоянии

@dp.message_handler(state=TestStates.TEST_STATE_1) async def first_test_state_case_met(message: types.Message): await message.reply('Первый!', reply=False) 

В данном хэндлере мы передаем в состояние TestStates.TEST_STATE_1 , что эквивалентно [‘test_state_1’] — отмечу дважды, тут передается именно массив.

Теперь добавим такой хэндлер:

@dp.message_handler(state=TestStates.TEST_STATE_2[0]) async def second_test_state_case_met(message: types.Message): await message.reply('Второй!', reply=False) 

Здесь мы уже указываем состояние ‘test_state_2’ .

Библиотека сама понимает, когда мы передаем список состояний, а когда только одно состояние и под капотом обрабатывает их по-разному, но нам нет смысла об этом задумываться. В этом плане мы в плюсе, так как можем сделать вот так:

@dp.message_handler(state=TestStates.TEST_STATE_3 | TestStates.TEST_STATE_4) async def third_or_fourth_test_state_case_met(message: types.Message): await message.reply('Третий или четвертый!', reply=False) 

Выражение TestStates.TEST_STATE_3 | TestStates.TEST_STATE_4 возвращает массив: [‘test_state_3’, ‘test_state_4’] . Грубо говоря, при проверке состояний библиотека проходится по списку и если встречает совпадение, хэндлер отрабатывается.

Ну и последний на сегодня хэндлер:

@dp.message_handler(state=TestStates.all()) async def some_test_state_case_met(message: types.Message): with dp.current_state(user=message.from_user.id) as state: text = MESSAGES['current_state'].format( current_state=await state.get_state(), states=TestStates.all() ) await message.reply(text, reply=False) 

Он принимает в себя сообщения при всех состояниях из возможных в этом уроке, однако так как состояния с первого по четвертый ловятся хэндлерами выше, в этот попадают только нулевое и пятое.

Ещё в приведённом выше блоке кода используется контекстный менеджер ( with as ) — многим с ним удобнее.

Для красоты ещё стоит закрывать соединение с хранилищем состояний, для этого объявляем функцию:

async def shutdown(dispatcher: Dispatcher): await dispatcher.storage.close() await dispatcher.storage.wait_closed() 
if __name__ == '__main__': executor.start_polling(dp, on_shutdown=shutdown) 

Источник

Оцените статью