Skip to main content

tg-tree-wizard

Лёгкий движок древовидных inline-опросников для aiogram 3. Дерево описывается данными (Node/Option), а не отдельным хендлером на каждый уровень; "назад" и переходы обрабатываются двумя общими хендлерами независимо от глубины дерева.

Структура пакета

tg_tree_wizard/
├── pyproject.toml              # метаданные пакета, как его ставить
├── src/tg_tree_wizard/
│   ├── __init__.py             # публичный API (что видно снаружи через import)
│   ├── core.py                 # чистая логика дерева, БЕЗ aiogram
│   ├── aiogram_adapter.py      # клавиатуры + хендлеры aiogram поверх core.py
│   ├── middleware.py            # middleware-система для перехвата событий
│   ├── manager.py              # WizardManager — управление состоянием сессий
│   └── testing.py              # simulate_wizard — прогон дерева без aiogram
├── tests/
│   ├── test_core.py            # тесты ядра, без сети и без Telegram
│   └── test_adapter.py         # тесты адаптера (keyboard, middleware, cancel)
└── examples/
    ├── simulate_flow.py        # прогон через настоящий aiogram Dispatcher, без сети
    └── language_school_bot.py  # реальный бот — запускается с настоящим токеном

Почему core.py отдельно от aiogram_adapter.py: это главный приём, который делает библиотеку тестируемой. Ядро ничего не знает про Telegram — значит, его логику (переходы, "назад", ошибки) можно проверить обычным pytest за доли секунды, без сети и без живого бота. Адаптер — тонкий слой, который только рендерит клавиатуры и дёргает ядро.

Установка (у себя, не здесь)

cd tg_tree_wizard
pip install -e .          # editable-режим: правите код — сразу видно в проекте

-e (editable) значит, что пакет ставится "по ссылке" на исходники, а не копируется — удобно, пока сами дорабатываете библиотеку.

Тесты ядра (без Telegram)

pip install pytest
pytest tests/ -v

Прогон через настоящий aiogram, но без сети

python examples/simulate_flow.py

Использует настоящий Dispatcher и MemoryStorage, подменяет только реальный HTTP-запрос к api.telegram.org — так что проверяется всё, кроме собственно доставки сообщений.

Запуск с реальным ботом

export BOT_TOKEN=ваш_настоящий_токен
python examples/language_school_bot.py

Дальше пишите /survey в бота и проходите дерево — это уже 100% реальная проверка через живой Telegram.

Отмена опроса и кнопка «Назад»

По умолчанию кнопки отмены нет. Включите её, передав cancel_button:

wizard = TreeWizard(
    TREE,
    root="start",
    cancel_button=True,           # показать кнопку «Отменить» в каждом узле
    cancel_callback_data="cancel",  # callback_data для кнопки отмены
)

Кнопка «Назад» добавляется автоматически на каждом шаге (кроме корневого).

Middleware — перехват событий

Middleware-хуки позволяют логировать, валидировать или модифицировать события:

from tg_tree_wizard import MiddlewareData, AbortWizard

def logging_hook(data: MiddlewareData):
    print(f"[{data.event_type}] узел={data.node_id}")
    if data.event_type == "start":
        print("Опрос начат!")

wizard = TreeWizard(
    TREE,
    root="start",
    middleware=[logging_hook],
)

Типы событий: "start", "choice", "back", "cancel".

Чтобы прервать опрос из middleware, бросьте AbortWizard:

def validate_email(data: MiddlewareData):
    if data.event_type == "choice":
        value = data.selected.value
        if not "@" in value:
            raise AbortWizard("Некорректный email")

Динамические варианты (DynamicOption)

DynamicOption позволяет генерировать клавиатуру на лету, используя данные из состояния пользователя:

from tg_tree_wizard import DynamicOption

def languages_factory(state: WizardState) -> list[tuple[str, str]]:
    user_lang = state.answers.get("lang", "en")
    if user_lang == "ru":
        return [("Русский", "ru"), ("Украинский", "uk")]
    return [("English", "en"), ("German", "de")]

TREE = {
    "lang": Node(text="Язык:", options=(
        DynamicOption(label_factory=languages_factory, value="dyn_lang", next_node="format"),
    )),
    "format": Node(text="Формат:", options=(
        Option("Очно", "offline"), Option("Онлайн", "online"),
    )),
}

Кнопки с URL и Switch Inline Query (P2.2)

from tg_tree_wizard import URLOption, SwitchOption

TREE = {
    "menu": Node(text="Меню:", options=(
        Option("В каталог", "catalog"),
        URLOption(label="🌐 Наш сайт", value="url", url="https://example.com"),
        SwitchOption(label="🔍 Поиск по чату", value="search", switch_inline_query="поиск..."),
    )),
}

WizardManager — управление состоянием сессий (P2.1)

WizardManager упрощает работу с несколькими активными опросами:

from tg_tree_wizard import WizardManager

manager = WizardManager()
wizard = TreeWizard(TREE, root="start")
dp.include_router(wizard.router)

# Менеджер автоматически отслеживает активные опросы
active_count = manager.active_count()  # количество активных опросов

Тестирование дерева без aiogram (P2.4)

simulate_wizard позволяет прогнать дерево через asyncio.run() без запуска бота:

from tg_tree_wizard.testing import simulate_wizard, simulate_wizard_with_state
import asyncio

tree = {
    "a": Node(text="Шаг A", options=(Option("A1", "v1", "b"), Option("A2", "v2", None))),
    "b": Node(text="Шаг B", options=(Option("B1", "v3", None),)),
}

# Простой прогон: выбираем путь a->0 -> b->0
answers, final_node = asyncio.run(simulate_wizard(tree, root="a", choices=[("a", 0), ("b", 0)]))
print(final_node)  # "b"

# С полным состоянием:
answers, state = asyncio.run(simulate_wizard_with_state(tree, root="a", choices=[("a", 0), ("b", 0)]))
print(state.answers)  # {'v1': ..., 'v3': ...}

Как описать своё дерево

Короткий способ (рекомендуется для большинства случаев)linear_wizard. Подходит, когда вопросы идут по порядку и лишь изредка нужно свернуть в сторону:

from tg_tree_wizard import linear_wizard, TreeWizard

TREE, ROOT = linear_wizard([
    ("lang", "Выберите язык:", ["Английский", "Немецкий"]),       # label == value
    ("delivery", "Формат:", [("Очно", "offline"), ("Онлайн", "online")]),
    ("topping", "Начинка:", [
        ("Пепперони", "pepperoni", "spicy"),   # явный переход = ветвление
        ("Маргарита", "margherita"),           # без 3-го элемента = следующий шаг по порядку
    ]),
    ("spicy", "Поострее?", ["Да", "Нет"]),
])

wizard = TreeWizard(TREE, root=ROOT, on_finish=my_finish_callback)
dp.include_router(wizard.router)

Правила для варианта ответа:

  • "Текст" — метка и значение совпадают, переход на следующий шаг по списку;
  • ("Текст", "значение") — то же самое, но значение отдельно от текста кнопки;
  • ("Текст", "значение", "id_узла") — явный переход, для ветвления или для перехода не на следующий, а на произвольный узел (в т.ч. в обход промежуточных шагов).

Последний шаг в списке — финальный по умолчанию (после него опрос завершается), если явно не переопределить переход у его вариантов.

Полный способNode/Option напрямую. Нужен, когда переходов в дерево больше, чем шагов, и связь "шаг → следующий по умолчанию" не работает (сложные ветвящиеся графы, несколько независимых корней и т.п.):

from tg_tree_wizard import Node, Option, TreeWizard

TREE = {
    "start": Node(
        text="Первый вопрос:",
        options=(
            Option("Вариант A", "a", "next_node_id"),
            Option("Вариант B", "b", None),  # None = опрос завершается
        ),
    ),
    "next_node_id": Node(...),
}

wizard = TreeWizard(TREE, root="start", on_finish=my_finish_callback)

TreeWizard(...) сам вызывает validate_tree при создании — если где-то опечатались в id узла, бот упадёт при старте с понятной ошибкой, а не у живого пользователя посреди опроса.

Публикация на PyPI (чтобы pip install tg-tree-wizard работал у других)

Один раз:

pip install build twine

1. Проверьте, что имя свободно — откройте https://pypi.org/project/tg-tree-wizard/ в браузере. Если там 404 — имя свободно. Если занято — поменяйте name в pyproject.toml (например tg-tree-wizard-yourname) и в [project.urls].

2. Заведите аккаунт на pypi.org (и отдельно — на test.pypi.org, для тестовой публикации перед настоящей).

3. Создайте API-токен: PyPI → Account settings → API tokens → "Add API token". Сохраните его — второй раз не покажут.

4. Соберите пакет:

python -m build

Появится dist/tg_tree_wizard-0.1.0.tar.gz и dist/tg_tree_wizard-0.1.0-py3-none-any.whl.

5. Сначала загрузите на TestPyPI (песочница, не настоящий PyPI — не страшно ошибиться):

twine upload --repository testpypi dist/*

Попросит username — введите __token__ (буквально это слово), и password — вставьте токен с test.pypi.org.

Проверьте, что ставится:

pip install --index-url https://test.pypi.org/simple/ tg-tree-wizard

6. Если всё встало и импортируется — публикуем по-настоящему:

twine upload dist/*

Здесь username тоже __token__, а password — токен уже с настоящего pypi.org.

7. Дальше у любого разработчика:

pip install tg-tree-wizard

Про версии: при каждой новой публикации version в pyproject.toml обязательно нужно поднимать (PyPI не даст перезалить тот же номер версии) — следуйте semver: багфикс → 0.1.1, новая фича без поломки API → 0.2.0, ломающее изменение (например, переименовали параметр у TreeWizard) → 1.0.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tg_tree_wizard-1.1.0.tar.gz (30.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tg_tree_wizard-1.1.0-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

File details

Details for the file tg_tree_wizard-1.1.0.tar.gz.

File metadata

  • Download URL: tg_tree_wizard-1.1.0.tar.gz
  • Upload date:
  • Size: 30.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for tg_tree_wizard-1.1.0.tar.gz
Algorithm Hash digest
SHA256 14968404aa1b9392bf3359febdcec45f1d523c3fc11f6b2c284f58d11b996799
MD5 b814ad7714ca56d42d3eda8423fe26e0
BLAKE2b-256 8c46acfd5c509fc4f6d9ef851f10cf62bab2ffd6b22a402a6d6b8e2e5757817a

See more details on using hashes here.

File details

Details for the file tg_tree_wizard-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: tg_tree_wizard-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for tg_tree_wizard-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 08f6a4f2462af5512cbf1dccd1dff30bbc6d987faa09754b16fccbb667f142ea
MD5 1f417ed9cb07c4a8ccaf9baedf12272a
BLAKE2b-256 d6607bb2db0c669120c32e454f43ad21c79c2fdb994857375685140d812a3637

See more details on using hashes here.

Release history Release notifications | RSS feed

2.0.2

1 file

2.0.1

2 files

1.1.1

2 files

This release

1.1.0 This release

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page