Обложка: Личный опыт: как прокачать навыки программирования, работая над pet-проектом

Личный опыт: как прокачать навыки программирования, работая над pet-проектом

Всем привет! Я студент первого курса технического университета, по воле случая стал старостой группы и погряз в бесконечных отчётах. Отчёты нужно было отправлять каждый день, часто даже без содержимого, а перспектива остаться без премии из-за пропуска одного дня слегка напрягала.

Но, к счастью, у меня был опыт программирования на Python.

Первые шаги

Сначала было необходимо освободиться от отчётов, поэтому я быстренько создал нового бота у «отца ботов» в Telegram и отправился в интернет. Нужно было реализовать следующие пункты:

  1. Создать простой код для прослушивания сообщений ботом.
  2. Реализовать изменение даты в нужных ячейках Excel-файла с отчётом.
  3. Найти способ отправлять сообщения по почте прямо из бота.

Для первого пункта подошла библиотека telebot.

Для второго пункта — openpyxl, datetime.

А для третьего — email, smtplib, mimetypes и много чужого кода.

Запуск и автоматизация

Для автоматической отправки отчётов в деканат было решено использовать библиотеку schedule. Она позволила настроить конкретные дни и время:

cur_time = "13:30"
schedule.every().monday.at(cur_time).do(check_sending)
schedule.every().tuesday.at(cur_time).do(check_sending)
schedule.every().wednesday.at(cur_time).do(check_sending)
schedule.every().thursday.at(cur_time).do(check_sending)
schedule.every().friday.at(cur_time).do(check_sending)

Новая проблема не заставила себя долго ждать. Бот был полностью готов к запуску, но на чём его запускать? Тут и пригодилась купленная два года назад Raspberry Pi 3.  Для начала хватило простого переноса через флешку всего написанного кода. Но позже пришлось выкручиваться.

Оказалось очень удобно переносить файлы на Raspberry через SSH-соединение. Один запуск bat-файла — и версия бота обновлена.

@echo off
echo  [1;32m "Loading files from raspberry"  [0m
pause
scp pi@111.111.1.11:/home/pi/Desktop/ReportBot/students_id.txt C:/Users/Cloud/Desktop
scp pi@111.111.1.11:/home/pi/Desktop/ReportBot/csv_data/data.csv C:/Users/Cloud/Desktop/ReportBot/csv_data
echo  [1;32m "Removing files from raspberry"  [0m
plink pi@111.111.1.11 -pw 111111 rm -rfv /home/pi/Desktop/ReportBot; mkdir /home/pi/Desktop/ReportBot

echo  [1;32m "Loading csv data into raspberry"  [0m
cd C:/Users/Cloud/Desktop/ReportBot/csv_data
plink pi@111.111.1.11 -pw 111111 mkdir /home/pi/Desktop/ReportBot/csv_data
scp *.csv pi@111.111.1.11:/home/pi/Desktop/ReportBot/csv_data

echo  [1;32m "Loading all another data into raspberry"  [0m
pause
cd ..
scp *.py *.xlsx *.txt *.csv pi@111.111.1.11:/home/pi/Desktop/ReportBot

timeout 30

Также были bat- файлы для старта (./runbot — еще один файл с запуском main.py):

plink pi@111.111.1.11 - pw 111111 ./runbot

и остановки ботов (к сожалению, останавливает все .py файлы):

plink pi@111.111.1.11 - pw 111111 sudo killall python3

Bышeyпoмянyтый файл runbot запускался при каждом перезапуске Raspberry, что позволяло перезапускать бота разными способами.

Однажды в отпуске, уверенный в коде, я залил последнее обновление на Raspberry и ушёл заниматься своими делами. Через несколько дней заметил, что бот отправляет не по одному отчёту, а сразу по 5-10, спамя прямо в деканат. Кажется, всё обошлось 🙂

Дома я бываю нечасто, так что вскоре появилась необходимость загружать обновление бота через любую точку доступа. В этом помог сервис No-IP. Он позволил обращаться к Raspberry через постоянный IP. После изменения адреса подключения с pi@111.111.1.11 на pi@myconnector.ddns можно загружать обновлённые файлы из любого точки земли с Wi-Fi.

Предвещая вопрос о том, почему не воспользоваться GitHub для этой задачи, отвечаю — я о нём даже не подумал. А когда всё-таки додумался, система уже работала и необходимости в изменениях не было.

Понеслась: реализация самых разных идей

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

Кнопки в меню бота

  1. Получение информации о выбранных криптовалютах на бирже Binance.
  2. Получение данных о времени жизни бота (подробнее об этом в следующем разделе).
  3. Ввод строки кода Python для расчётов чего-либо.
  4. Получение информации с Яндекс.Погоды.
  5. Отправление отчёта вручную.
  6. Изменение настроек, в которых хранилась информация о городе пользователя, выбранных криптовалютах, имени файла отчёта и т.д.
Send To   >>>   *********@gmail.com
FileName   >>>   otchet.xlsx
File Send   >>>   1
City   >>>   Москва
Sending Activate   >>>   1
Binance Currency   >>>   XEMBTC
NoIp   >>>   6
None   >>>   date:1

Но всё это было куда-то не туда.

Рефакторинг кода

Бот часто падал, из-за этого я пропускал сразу несколько дней отправки отчётов. Всё исправить помогла библиотека aiogram. В коде обошёлся минимальными изменениями, но он был просто ужасным. Нагромождая новую функциональность, я забывал о качестве.

while True:
  try:
    @bot.message_handler(commands=['start'])
    def start_message(message):
        bot.send_message(message.chat.id, 'Hi hello', 
                         reply_markup=keyboard_main)

    @bot.message_handler(content_types=['text'])
    def send_message(message):
      global change_settings, settings_info_line, settings_info
      global changing_settings, calculate_readline, timings

Был реализован один глобальный цикл while True, внутри которого находились все handler’ы. Нужно было менять много кода. Помню, тогда я закрылся на несколько часов, изменяя уже почти легаси-код.

# <<<<<<<<<<<<<<<<<< Start >>>>>>>>>>>>>>>>>>
@settings.dp.message_handler(commands=["start"])
async def cmd_start(message: types.Message):
    global keyboard
    await message.answer("Запуск основной клавиатуры", 
                         reply_markup=keyboard.get_main(...))

# <<<<<<<<<<<<<<<<<< myid >>>>>>>>>>>>>>>>>>
@settings.dp.message_handler(commands=["myid"])
async def cmd_start(message: types.Message):
    await message.answer("Ваш id: %s" % message.from_user["id"])

В итоге весь код был разбит на отдельные асинхронные handler’ы. После всех этих изменений бот перестал падать.

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

В проекте быстро стали накапливаться новые идеи, ждущие реализации, появился целый блок из TODO. Однако многие пункты и не реализованы.

TODO лист

Переделка бота под рабочие нужды

На работе было нужно считать отработанные часы. Я студент, работаю удалённо с гибким графиком, грубо говоря, нужно отработать только конкретное количество часов в неделю. Записывать их на бумажку или отправлять себе в сообщения было неудобно. Так появилась идея реализовать подобную функциональность в своём боте.

Основную часть логики я прописал в первые же выходные (тогда я просидел за кодом часов 20-25).

Появилась возможность нажатием одной кнопки «начинать работать», а нажатием другой кнопки — «заканчивать». Интервал между двумя нажатиями бот автоматически сохранял в базу данных через PostgreSQL, прикрепляя запись к id конкретного пользователя.

Со временем я добавил вывод данных за прошедшую неделю/месяц, разбиение интервалов работы на теги (теги можно создавать, написав #, а затем сам тег), разделение рабочего и учебного процессов. Старый функционал был спрятан за ненадобностью. А новый расширен.

Пример меню бота

Немного об Inline-кнопках

После написания основной части этой статьи захотелось протестировать возможности Inline-кнопок. В качестве первого теста я решил добавить конструктор по созданию отработанного периода. Здесь Inline-кнопки оказались незаменимы.

Но для начала немного о том, как их реализовать.

Для своего проекта я создал новый класс-хранилище CallbackItems, в котором есть только один метод __init__. В нём я создал много объектов CallbackData и InlineKeyboardButton, чтобы можно было использовать фильтры в handler’ах и собирать из нужных кнопок Inline-сообщение для каждого пользователя.

// создание объекта CallbackData для использования в фильтрах
self.date_callback = CallbackData("date", "time_unit", "val")
// создание кнопки "Назад" во шаблону date_callback
self.date_back_callback = InlineKeyboardButton(
                            text="Back", 
                            callback_data=self.date_callback.new(
                                time_unit="Back",
                                val=-1
                            ))
// создание словаря кнопок для использования их в конструкторе даты
self.days_btn_callback = {
    k: InlineKeyboardButton(
        text=str(k), 
        callback_data=self.date_callback.new(
            time_unit="day",
            val=k
        )
    ) for k in range(1, 32)
}

Дальше можно создавать handler’ы.

Объекты класса CallbackData позволяют нам использовать фильтр в query_handler’ах.

// dp: Dispatcher, callback: CallbackItems, date_callback: CallbackData
@dp.callback_query_handler(callback.checkout_callback.filter(time_unit="min"))
async def save_min_date_callback(call: types.CallbackQuery, callback_data: dict):

Помимо использования фильтров, класс CallbackData позволяет получать данные о нажатии кнопки в виде словаря (в коде выше — callback_data: dict).

Где ключ — указанные при создании CallbackData значения (

CallbackData(«date«, «time_unit«, «val«)).

А значение — значение конкретной кнопки, указанное в callback_data, при ее создании  (InlineKeyboardButton(
text=»Back»,
callback_data=date_callback.new(
time_unit=«Back»,
val=-1
)))

После нажатия пользователем на Inline-кнопку бот может изменить сообщение. Для этого нужно только добавить строчку кода в handler

// call: CallbackQuery
await call.message.edit_text(new_text, reply_markup=new_inline_keuboard)

Результат добавления Inline-кнопок в код

В итоге Inline-кнопки оказались настолько удобными, что я их добавил везде, где только смог. После нажатия на любую кнопку клавиатуры (кроме Start/Stop working) , бот отправлял сообщение с Inline-кнопками.

Конструктор даты (при создании отработанного периода) стал выглядеть так:

Пример конструктора даты

Выбор дня отработанного периода так:

Пример выбора дня отработанного периода Выбор часа начала отработанного периода так:
Пример выбора часа начала работы  

Аналогично происходит выбор минуты начала, часа и минуты окончания отработанного периода.

Генерация отчетов стала в разы удобнее за счёт возможности изменять сообщения после нажатия на Inline кнопку.

Тут я нажал кнопку This week:

Отчёт за неделю А тут This week (details):

Пример отчёта в боте

При нажатии на разные кнопки, в одном и том же сообщении формируются разные отчёты.

Кроме того, с помощью Inline-кнопок удобно выбирать нужный тег:

Список тегов в боте

Это не все применения Inline-кнопок в моём боте. Остальные вы можете посмотреть в самом боте по ссылке внизу статьи.

Используйте Inline-кнопки, это очень удобно как для вас, так и для пользователя.

Заключение

Так с помощью pet-проекта я познакомился со множеством новых для меня библиотек, получил первую критическую ошибку на проде (теперь можно смеяться над мемами), автоматизировал работу с отчётами и подсчёт отработанного времени.

Чтобы посмотреть, как бот работает сейчас, переходите по ссылке.

Исходный код бота в моём GitHub.

А как вам pet-проекты помогали прокачивать свои навыки?