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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14968404aa1b9392bf3359febdcec45f1d523c3fc11f6b2c284f58d11b996799
|
|
| MD5 |
b814ad7714ca56d42d3eda8423fe26e0
|
|
| BLAKE2b-256 |
8c46acfd5c509fc4f6d9ef851f10cf62bab2ffd6b22a402a6d6b8e2e5757817a
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08f6a4f2462af5512cbf1dccd1dff30bbc6d987faa09754b16fccbb667f142ea
|
|
| MD5 |
1f417ed9cb07c4a8ccaf9baedf12272a
|
|
| BLAKE2b-256 |
d6607bb2db0c669120c32e454f43ad21c79c2fdb994857375685140d812a3637
|