Metadata-Version: 2.4
Name: tg-tree-wizard
Version: 1.0.0
Summary: Лёгкий движок древовидных inline-опросников (wizard) для aiogram 3
Author: Артем Самарин
License: MIT License
        
        Copyright (c) 2026 Артем Самарин
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/ваш-ник/tg-tree-wizard
Project-URL: Repository, https://github.com/ваш-ник/tg-tree-wizard
Project-URL: Issues, https://github.com/ваш-ник/tg-tree-wizard/issues
Keywords: aiogram,telegram,telegram-bot,inline-keyboard,fsm,wizard
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: AsyncIO
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiogram>=3.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# tg-tree-wizard

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

## Сколько кода экономит

Замер на дереве из 6 уровней (язык → формат → цель → индивидуально/группа →
возраст → уровень владения), с 2-7 вариантами ответа на каждом —
навигационная часть кода, без учёта бизнес-логики финального шага
(сохранение заявки и т.п. — она одинакова в обоих случаях):

| | Строк кода |
|---|---|
| Вручную (клавиатура + парсинг callback_data + кнопка "назад" на каждый уровень) | ~169 |
| `tg-tree-wizard` (`linear_wizard` + подключение роутера) | ~26 |

Разница держится линейно — не растёт и не убывает в зависимости от того,
сколько таких деревьев в проекте: каждое новое дерево стоит фиксированные
~26 строк вместо ~169, потому что логика навигации, "назад" и защита от
превышения лимита `callback_data` уже написаны и покрыты тестами один раз,
внутри самой библиотеки.

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

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

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

## Установка

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

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

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

```bash
pip install pytest
pytest tests/ -v
```

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

```bash
python examples/simulate_flow.py
```

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

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

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

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

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

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

```python
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` напрямую. Нужен, когда переходов в
дерево больше, чем шагов, и связь "шаг → следующий по умолчанию" не
работает (сложные ветвящиеся графы, несколько независимых корней и т.п.):

```python
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 узла, бот упадёт при старте с понятной ошибкой.
