Загрузка...

Gram_tools | Утильки для упрощенной разработки ботов на aiogram 3

Тема в разделе Python создана пользователем Maehdakvan 9 ноя 2024. (поднята 23 июл 2025 в 15:52) 893 просмотра

  1. Maehdakvan
    Maehdakvan Автор темы 9 ноя 2024 76 14 ноя 2020

    Ну шо, давно ничего не выпускал...

    Но сегодня я рад представить обновлённую версию —
    gram-tools — набор утилит для упрощённой разработки ботов на aiogram 3

    gram-tools создана для того, чтобы сделать разработку ботов на aiogram 3 более удобной и эффективной. Она включает множество полезных инструментов, которые помогут вам быстрее создавать функциональные и надёжные боты.

    и бла, бла, бла...

    Что же такого офигительного в gram-tools:

    Message Packing: Сериализация и десериализация Telegram-сообщений с поддержкой всех типов медиа
    Asset Management: Эффективное управление медиафайлами с кэшированием и повторным использованием file_id
    CRUD Operations: Универсальные асинхронные операции CRUD для моделей SQLAlchemy
    • ⌨ Pagination: Гибкая пагинация с inline-клавиатурой и возможностью поиска
    Menu Builder: Простое создание сложных вложенных меню
    FSM Visualizer: Генерация диаграмм ваших КоооНечных автоматов (Finite State Machines)

    Если для новичка это кажется чем-то нудным и страшным, то вот шо это даёт:

    Message Packing: Позволяет удобно упаковывать сообщения для хранения в базе данных вместо того, чтобы хранить JSON с полями 'message', 'text', 'attachment' и создавать отдельные типы и прочую фигню. Ты просто хранишь одну строку в base64.

    Asset Management: Штука, которая по сути просто экономит наши расходы на интернет и время на загрузку на сервер телеги файлов. Например, есть у нас баннер профиля, нафига нам каждый раз загружать его на сервер телеги, если мы можем один раз загрузить и всем потом слать этот баннер. Тут уже всё это реализовано, причём если вы фотку замените, скриптец сам поймёт, что что-то поменялось.

    CRUD Operations: В любом проекте приходится что-то записать в базу, достать из неё, поменять, удалить, и для этого постоянно создают какую-то ужасную простыню кода (который, кстати, называется boilerplate код). Типа get_user_by_id, get_user_from_item_id и много таких функций и методов для каждой новой абстракции, которую мы описываем, будь то Юзерок или Товар или Дерево и Машина. Эта штука предоставляет уже всё нужное, достаточно описать модель и передать в одну функцию, и потом управлять ей.

    • ⌨ Pagination: Головная боль в каждом проекте — постоянно делать клавиатуры со страничками, далее, назад, и ещё туда поиск прикрутить, а ещё надо границы страниц как-то определять, чтобы покрасивше было. Да короче, сами всё знаете, а тут бац бац — и уже готово!

    Menu Builder: Теперь создание сложных вложенных меню — это просто. Больше не нужно писать кучу кода для меню, просто используйте этот инструмент и наслаждайтесь результатом.

    FSM Visualizer: Генерация визуальных диаграмм ваших конечных автоматов (FSM). Теперь вы можете легко визуализировать состояния и переходы в вашем боте чтобы понять шо вы там могли упустить и где. Ведь визуализация это сила

    И не я ща не шучу, зацените
    Джим Керри: "Я визуализировал, как получаю чек на 10 миллионов долларов за актерскую работу. Я написал себе чек и датировал его на День благодарения 1995 года. И я получил его."

    Вот и вы визуализируйте шоб отладить вашего бота! Кто знает, может потом и он вам 10 лимонов принесет

    Как установить:

    Код
    pip install gram-tools
    Показываю весь функционал:

    Message Packer

    Поехали, вот как мы пакуем, распаковываем... Короче, сейчас убедитесь, как это легко и просто.

    Python

    from gram_tools.packer import pack_message, unpack_message, send_packed_message, answer_packed_message

    # Упаковать сообщение
    packed = await pack_message(message)

    # Отправить упакованное сообщение
    await send_packed_message(bot, chat_id, packed)

    # Ответить упакованным сообщением
    await answer_packed_message(message, packed)
    Детальный пример

    Python

    import asyncio
    from aiogram import Bot, Dispatcher, Router
    from aiogram.types import Message
    from aiogram.filters import Command

    from gram_tools.packer import pack_message, send_packed_message

    API_TOKEN = "ВАШ ТОКЕН БОТА"

    bot = Bot(token=API_TOKEN)
    dp = Dispatcher()
    router = Router()

    # Временное хранилище для упакованных сообщений
    packed_messages = {}

    @router.message(Command("save"))
    async def save_message(message: Message):
    if not message.reply_to_message:
    await message.answer("Пожалуйста, ответьте на сообщение, которое хотите сохранить.")
    return

    # Упаковать ответное сообщение
    packed = await pack_message(message.reply_to_message)

    # Сохранить упакованное сообщение по ID пользователя
    packed_messages[message.from_user.id] = packed
    await message.answer("Сообщение сохранено!")

    @router.message(Command("send"))
    async def send_saved_message(message: Message):
    packed = packed_messages.get(message.from_user.id)
    if not packed:
    await message.answer("Нет сохранённых сообщений. Используйте /save для сохранения.")
    return

    # Отправить сохранённое сообщение обратно пользователю
    await send_packed_message(bot, message.chat.id, packed)

    async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

    if __name__ == "__main__":
    asyncio.run(main())
    Ассет менеджер

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

    Python

    from gram_tools.assets import answer_with_asset, send_with_asset

    # Отправить медиафайл
    await send_with_asset(bot, chat_id, "path/to/file.jpg", caption="Моё фото") # Также можно без подписи, с reply_markup и другими параметрами

    # Ответить медиафайлом
    await answer_with_asset(message, "path/to/file.mp4")
    Детальный пример

    Python

    import asyncio
    from aiogram import Bot, Dispatcher, Router
    from aiogram.types import Message
    from aiogram.filters import Command

    from gram_tools.assets import send_with_asset, answer_with_asset

    API_TOKEN = "ВАШ ТОКЕН БОТА"

    bot = Bot(token=API_TOKEN)
    dp = Dispatcher()
    router = Router()

    @router.message(Command("photo"))
    async def send_photo(message: Message):
    await send_with_asset(
    bot,
    chat_id=message.chat.id,
    file_path="path/to/your/photo.jpg",
    caption="Вот ваше фото!"
    )

    @router.message(Command("video"))
    async def send_video(message: Message):
    await answer_with_asset(
    message=message,
    file_path="path/to/your/video.mp4",
    caption="Вот ваше видео!"
    )

    async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

    if __name__ == "__main__":
    asyncio.run(main())
    Поддерживаемые типы медиа:
    - Фото: jpg, jpeg, png, svg, webp, bmp, jfif, heic, heif
    - Видео: mp4, mov, avi, mkv, m4v, 3gp
    - Аудио: mp3, wav, flac, m4a, ogg, aac
    - Голосовые сообщения: ogg, opus
    - Анимации: gif
    - *********: Любые другие типы файлов

    CRUD операции

    CRUD реально крут! Очередной примерчик, что вообще да как. Если что, там сессию передавать надо, но aiogram позволяет через middleware тянуть за собой и экземпляр сессии, и бота, и так далее. Можете загуглить.

    Python

    from gram_tools.crud import get_crud
    from sqlalchemy import Column, Integer, String
    from sqlalchemy.ext.declarative import declarative_base

    Base = declarative_base()

    class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    telegram_id = Column(Integer, unique=True, index=True)
    name = Column(String)

    user_crud = get_crud(User)

    # Добавить новый экземпляр
    await user_crud.add(session, user)

    # Получить экземпляр
    user = await user_crud.get(session, id=1)

    # Получить все экземпляры
    active_users = await user_crud.get_all(session, status="active")

    # Получить количество экземпляров
    active_users_count = await user_crud.get_all_count(session, status="active")

    # Обновить экземпляр
    await user_crud.update(session, user, name="Новое Имя")

    # Удалить экземпляр
    await user_crud.delete(session, user)
    Клавиатуры с пагинацией

    Парацетамол в мире aiogram-кодеров:

    Python

    from gram_tools.keyboards.pagination import InlinePageBuilder, SearchButton

    # Создать пагинацию с поиском
    builder = InlinePageBuilder(
    per_page=5,
    layout=1,
    search_button=SearchButton("")
    )

    items = ["Элемент 1", "Элемент 2", "Элемент 3", ...]
    keyboard = builder.get_paginated(items, page=1)

    # С поиском
    keyboard = builder.get_paginated(items, page=1, search_term="поиск")
    Опции настройки:
    - per_page: Количество элементов на странице
    - layout: Количество кнопок в строке
    - next_button_text: Текст кнопки следующей страницы
    - prev_button_text: Текст кнопки предыдущей страницы
    - page_callback_prefix: Префикс для callback данных пагинатора и выбора элементов
    - ignore_callback_prefix: Префикс для игнорирования callback данных при клике
    - not_exist_page: Текст для отключённых навигационных кнопок
    - search_button: Опциональная конфигурация кнопки поиска

    Детальный пример

    Python

    import asyncio
    from aiogram import Bot, Dispatcher, Router
    from aiogram.types import CallbackQuery, Message
    from aiogram.filters import Command
    from aiogram.fsm.context import FSMContext
    from aiogram.fsm.state import StatesGroup, State

    from gram_tools.keyboards.pagination import InlinePageBuilder, SearchButton

    API_TOKEN = "ВАШ ТОКЕН БОТА"

    bot = Bot(token=API_TOKEN)
    dp = Dispatcher()
    router = Router()

    items = [f"Элемент {i}" for i in range(1, 21)]
    search_button = SearchButton(button_text=" Поиск")
    inline_builder = InlinePageBuilder(per_page=5, search_button=search_button)

    class Form(StatesGroup):
    waiting_for_search_term = State()

    @router.message(Command("start"))
    async def show_inline_keyboard(message: Message, state: FSMContext):
    await state.clear()
    keyboard = inline_builder.get_paginated(items, page=1)
    await message.answer("Выберите элемент:", reply_markup=keyboard)

    @router.callback_query(inline_builder.page_callback.filter())
    async def handle_callback(callback: CallbackQuery, callback_data: inline_builder.page_callback, state: FSMContext):
    action = callback_data.action
    value = callback_data.value

    data = await state.get_data()
    search_term = data.get('search_term')

    if action in ("next", "prev"):
    page = value
    keyboard = inline_builder.get_paginated(items, page=page, search_term=search_term)
    await callback.message.edit_reply_markup(reply_markup=keyboard)
    await callback.answer()
    elif action == "sel":
    item_index = value
    if search_term:
    filtered_items = [item for item in items if search_term.lower() in str(item).lower()]
    else:
    filtered_items = items

    if 0 <= item_index < len(filtered_items):
    selected_item = filtered_items[item_index]
    await callback.message.answer(f"Вы выбрали: {selected_item}")
    else:
    await callback.message.answer("Элемент не найден.")
    await callback.answer()

    @router.callback_query(search_button.search_callback.filter())
    async def handle_search_callback(callback: CallbackQuery, state: FSMContext):
    await state.update_data(search_term=None)
    await callback.message.answer("Введите текст для поиска:")
    await state.set_state(Form.waiting_for_search_term)
    await callback.answer()

    @router.message(Form.waiting_for_search_term)
    async def perform_search(message: Message, state: FSMContext):
    search_term = message.text
    await state.update_data(search_term=search_term)
    await state.set_state(None)

    keyboard = inline_builder.get_paginated(items, page=1, search_term=search_term)

    if keyboard.inline_keyboard:
    await message.answer("Результаты:", reply_markup=keyboard)
    else:
    await message.answer("Ничего не найдено.")

    @router.callback_query(inline_builder.ignore_callback.filter())
    async def handle_ignore_callback(callback: CallbackQuery):
    await callback.answer()

    async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

    if __name__ == "__main__":
    asyncio.run(main())
    Меню билдер

    Создание сложных вложенных меню стало проще простого.

    Python

    from gram_tools.keyboards.menu import InlineMenuBuilder, MenuCallbackData, ActionCallbackData

    # Инициализация билдера меню
    menu_builder = InlineMenuBuilder()

    # Определение меню и кнопок
    main_menu_buttons = [
    {'text': '➡ Перейти в Подменю 1', 'menu': 'submenu1'},
    {'text': ' Опция в Главном Меню', 'action': 'option_main'},
    {'text': 'ℹ Инфо', 'action': 'info'},
    {'text': '⚙ Настройки', 'menu': 'settings_menu'}
    ]

    submenu1_buttons = [
    {'text': ' Опция в Подменю 1', 'action': 'option_sub1'},
    {'text': '⬅ Назад в Главное Меню', 'menu': 'main_menu'}
    ]

    settings_menu_buttons = [
    {'text': ' Уведомления', 'action': 'notifications'},
    {'text': ' Язык', 'action': 'language'},
    {'text': ' Приватность', 'action': 'privacy'},
    {'text': '⬅ Назад в Главное Меню', 'menu': 'main_menu'}
    ]

    # Создание меню с размером строк
    menu_builder.create_menu('main_menu', main_menu_buttons, row_sizes=[2, 2])
    menu_builder.create_menu('submenu1', submenu1_buttons, row_sizes=[1, 1])
    menu_builder.create_menu('settings_menu', settings_menu_buttons, row_sizes=[2, 1])

    # Регистрация обработчиков действий
    @router.callback_query(ActionCallbackData.filter())
    async def handle_action(callback: CallbackQuery, callback_data: ActionCallbackData):
    action = callback_data.action
    value = callback_data.value
    handler = menu_builder.get_handler(action)
    if handler:
    await handler(callback, value)
    else:
    await callback.message.answer(f"Действие '{action}' не обработано.")
    await callback.answer()

    @router.callback_query(MenuCallbackData.filter())
    async def handle_menu(callback: CallbackQuery, callback_data: MenuCallbackData):
    action = callback_data.action
    menu_name = callback_data.menu_name
    if action in ('open', 'back'):
    markup = menu_builder.get_menu(menu_name)
    if markup:
    await callback.message.edit_reply_markup(reply_markup=markup)
    await callback.answer()

    # Обработчики действий
    async def option_main_handler(callback: CallbackQuery, value: Optional[str]):
    await callback.message.answer("Вы выбрали опцию в Главном Меню.")

    async def info_handler(callback: CallbackQuery, value: Optional[str]):
    await callback.message.answer("Это информация о боте.")

    async def notifications_handler(callback: CallbackQuery, value: Optional[str]):
    await callback.message.answer("Настройки уведомлений.")

    async def language_handler(callback: CallbackQuery, value: Optional[str]):
    await callback.message.answer("Настройки языка.")

    async def privacy_handler(callback: CallbackQuery, value: Optional[str]):
    await callback.message.answer("Настройки приватности.")

    async def option_sub1_handler(callback: CallbackQuery, value: Optional[str]):
    await callback.message.answer("Вы выбрали опцию в Подменю 1.")

    # Регистрация обработчиков
    menu_builder.register_handler('option_main', option_main_handler)
    menu_builder.register_handler('info', info_handler)
    menu_builder.register_handler('notifications', notifications_handler)
    menu_builder.register_handler('language', language_handler)
    menu_builder.register_handler('privacy', privacy_handler)
    menu_builder.register_handler('option_sub1', option_sub1_handler)

    # Обработчик команды /start
    @router.message(Command("start"))
    async def start_cmd(message: Message):
    markup = menu_builder.get_menu('main_menu')
    await message.answer("Добро пожаловать в бота. Выберите опцию:", reply_markup=markup)
    FSM Visualizer

    Легендарный визуализатор Finite State Machines (FSM), анализируя Python-код вашего бота. Функция `visualize_fsm` парсит код вашего бота для извлечения обработчиков, состояний FSM и переходов, и генерирует визуальное представление FSM с помощью NetworkX и Matplotlib.

    Python

    from gram_tools.visual import visualize_fsm

    visualize_fsm('path/to/your_code.py')
    Пример

    Предположим, у вас есть бот `bot.py` со состояниями FSM:

    [code=python]

    from aiogram import Bot, Dispatcher, types, Router
    from aiogram.filters import Command
    from aiogram.fsm.state import State, StatesGroup
    from aiogram.fsm.context import FSMContext

    API_TOKEN = "ВАШ ТОКЕН БОТА"

    bot = Bot(token=API_TOKEN)
    dp = Dispatcher()
    router = Router()

    class Registration(StatesGroup):
    waiting_for_name = State()
    waiting_for_email = State()

    @router.message(Command('register'))
    async def cmd_register(message: types.Message, state: FSMContext):
    await message.answer("Как тебя зовут?")
    await state.set_state(Registration.waiting_for_name)

    @router.message(Registration.waiting_for_name)
    async def process_name(message: types.Message, state: FSMContext):
    await state.update_data(name=message.text)
    await message.answer("Какой у тебя email?")
    await state.set_state(Registration.waiting_for_email)

    @router.message(Registration.waiting_for_email)
    async def process_email(message: types.Message, state: FSMContext):
    user_data = await state.get_data()
    await message.answer(f"Регистрация завершена! Имя: {user_data['name']}, Email: {message.text}")
    await state.clear()

    [/code]Чтобы визуализировать FSM, создайте отдельный скрипт:

    [code=python]
    from gram_tools.visual import visualize_fsm

    visualize_fsm('bot.py')
    [/code]Запустите этот скрипт:

    [code]
    python visualize.py
    [/code]Это сгенерирует изображение, представляющее FSM в вашем `bot.py`:

    [IMG]

    Дополнительная информация

    - Функция `visualize_fsm` парсит ваш Python-код для извлечения состояний FSM и переходов.
    - Она поддерживает реализации FSM в aiogram с использованием `StatesGroup` и `State`.
    - Сгенерированная диаграмма поможет понять и отладить поток состояний в вашем боте.

    Перейти на GitHub


    Спасибо за то, что внимание уделили!
     
    9 ноя 2024 Изменено
  2. Gosha_coder
    Gosha_coder 9 ноя 2024 394 10 янв 2022
    Выглядит прикольно)
     
  3. derkown
    нахуя оно надо если можно обычными средствами питона обойтись?
     
    1. Maehdakvan Автор темы
      derkown, Можно, но и без айограма можно бота создать, однако его юзают. Готов рассмотреть предложения что можно добавить из более стоящего функционала
      1 дек 2024 Изменено
  4. architectcoders
    architectcoders 9 ноя 2024 Заблокирован(а)
    Как будто велосипед, средствами aiogram 3.x все много проще делается
    --- Сообщение объединено с предыдущим 9 ноя 2024
    Что полезно так это пагинация
     
    1. Maehdakvan Автор темы
      architectcoders, Это ещё прям бета-бета версия, из уникального только FSM визуализатор, ну и может по большей части людям будет полезна пагинация с меню билдером. Все же я думаю велосипед не всегда плохо, ведь есть модификации, шото вроде горного велосипеда с несколькими цепями или зимнего под другие уловия. Все ситуативно, но либа действительно еще сырая. Хотелось бы больше конструктива, предложений чего не хватает или что очень сильно долго делается типа прям рутина рутина
  5. Aisan
    Aisan 30 ноя 2024 Ничего не продаю и не создаю. Не пишите мне 15 755 26 авг 2020
    А зачем это, если для каждой функции из библиотеки есть качественные аналоги?
     
    1. Maehdakvan Автор темы
      Aisan, Я возможно не все аналоги знаю, если не сложно можно будет некоторые показать хотя б?
  6. architectcoders
    architectcoders 30 ноя 2024 Заблокирован(а)
    Python
    template = T("Привет, ${name}!")
    text = template.text(name="Пользователь")

    0_0

    "Привет, ${0}!".format("Пользователь")
     
  7. asyncTraffic
    asyncTraffic 2 мар 2025 :BrainCosmic: Комьюнити: https://t.me/+q8QT8dzyDwdiZTky 376 30 дек 2024
    отличная библиотека судя по описанию, особенно пакер разъеб) у меня уже есть свои наработки которыми пользуюсь, но либа вообще огонь) попробую обязательно как нибудь. а те кто пишут что нахуя либа нужна видимо задача подобного рода не решали, с пагинацией, хранением медиафайлов и тд. еще бы знаешь что добавить, крч функцию, куда ты будешь предавать названия и url кнопок и на выходе получать клавиатуру нужную, и вот что бы ее можно было сейвить в базу, крч как пакер только отдельно для клавы, было бы прикольно
     
  8. Wiwoji
    Wiwoji 21 июл 2025 в 13:43 google 94 12 мар 2021
    а мидлварь нам и нахуй не нужОн? Слышал что то про них?
     
    1. Maehdakvan Автор темы
      Wiwoji, естественно
Загрузка...
Top