Metadata-Version: 2.4
Name: telegram-menu-builder
Version: 0.4.0
Summary: A powerful builder library for creating recursive inline keyboard menus in python-telegram-bot
Author-email: Simone Flavio Paris <info@sf-paris.dev>
License-Expression: MIT
Project-URL: Homepage, https://github.com/smoxy/telegram-menu-builder
Project-URL: Documentation, https://smoxy.github.io/telegram-menu-builder/
Project-URL: Repository, https://github.com/smoxy/telegram-menu-builder
Project-URL: Bug Tracker, https://github.com/smoxy/telegram-menu-builder/issues
Keywords: telegram,bot,inline-keyboard,menu,builder,python-telegram-bot
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: AsyncIO
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-telegram-bot<22.8,>=20.0
Requires-Dist: pydantic<3.0,>=2.4
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: pytest-mock>=3.12; extra == "dev"
Requires-Dist: fakeredis>=2.20; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: pyright>=1.1.350; extra == "dev"
Requires-Dist: ruff>=0.2.0; extra == "dev"
Requires-Dist: black>=24.0; extra == "dev"
Requires-Dist: pip-audit>=2.7; extra == "dev"
Requires-Dist: pre-commit>=3.6; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.6; extra == "docs"
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
Requires-Dist: mkdocs-include-markdown-plugin>=6.0; extra == "docs"
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == "redis"
Provides-Extra: sql
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0.30; extra == "sql"
Requires-Dist: aiosqlite>=0.19; extra == "sql"
Provides-Extra: postgres
Requires-Dist: asyncpg>=0.29; extra == "postgres"
Provides-Extra: mysql
Requires-Dist: asyncmy>=0.2.9; extra == "mysql"
Provides-Extra: all
Requires-Dist: telegram-menu-builder[mysql,postgres,redis,sql]; extra == "all"
Dynamic: license-file

# Telegram Menu Builder

[![PyPI version](https://img.shields.io/pypi/v/telegram-menu-builder.svg)](https://pypi.org/project/telegram-menu-builder/)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](docs/python-compatibility.md)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![CI](https://github.com/smoxy/telegram-menu-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/smoxy/telegram-menu-builder/actions/workflows/ci.yml)
[![Type checked: mypy + pyright](https://img.shields.io/badge/types-mypy%20%2B%20pyright-blue)](https://github.com/microsoft/pyright)
[![Linting: ruff](https://img.shields.io/badge/linting-ruff-red)](https://github.com/astral-sh/ruff)

A type-safe, async-first Python library for building recursive **inline keyboard menus** in
[python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) v20+. You declare
buttons with a fluent `MenuBuilder`, the library encodes each callback payload to fit Telegram's
64-byte limit (compressing inline or spilling to pluggable storage), and a `MenuRouter` decodes
incoming callbacks and dispatches them to your handlers.

📖 **Documentation:** <https://smoxy.github.io/telegram-menu-builder/> — or jump to the
[doc index below](#-documentation).

> **Status:** alpha (`0.x`). The API may change before `1.0`. See the [changelog](CHANGELOG.md).

## ✨ Features

- 🏗️ **Fluent builder API** — chainable, readable menu construction.
- 📦 **Smart callback encoding** — automatic inline / short-term / persistent strategy to stay
  under Telegram's 64-byte limit, with zlib compression and deduplication.
- 🧭 **Routing & middleware** — `MenuRouter` dispatches callbacks to named handlers with
  `before` / `after` / `on_error` hooks and handler groups.
- 🔄 **Unlimited nesting** — submenus and navigation (back / next / exit / cancel) buttons.
- 🧩 **Pluggable storage** — built-in in-memory, async SQL, and Redis/Valkey backends are included;
  or implement the `StorageBackend` protocol (or subclass `BaseStorage`) for your own.
- 🪄 **Application-free output** — `to_markup()` / `to_raw()` build a `telegram.InlineKeyboardMarkup`
  or a plain Bot API dict synchronously, with no PTB `Application`, event loop, or storage required —
  share one menu definition across codebases.
- 🔐 **Strict typing** — full type hints, validated with both `mypy --strict` and `pyright`
  (Pydantic v2 models), shipped with `py.typed`.
- 🧪 **Well tested** — ~90% coverage, CI on every push and pull request, plus a
  `telegram_menu_builder.testing` module to drive handlers with no live Telegram.

## 🚀 Quick start

```bash
pip install telegram-menu-builder
```

Optional extras: `telegram-menu-builder[redis]` (built-in Redis/Valkey backend), `[sql]` (plus
`[postgres]` / `[mysql]` drivers), `[dev]`, `[docs]`. See [Installation](docs/installation.md).

```python
from telegram import Update
from telegram.ext import Application, CallbackQueryHandler, ContextTypes
from telegram_menu_builder import MenuBuilder, MenuRouter

router = MenuRouter()


async def show_settings(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    menu = (
        MenuBuilder()
        .add_item("🌍 Language", handler="set_language")
        .add_item("👤 Profile", handler="edit_profile", user_id=update.effective_user.id)
        .columns(2)
        .add_back_button()
        .build()
    )
    await update.message.reply_text("⚙️ Settings", reply_markup=menu)


@router.handler("set_language")
async def handle_language(update: Update, context: ContextTypes.DEFAULT_TYPE, params: dict) -> None:
    # `params` are the values you passed to add_item(...), decoded from the callback.
    await update.callback_query.edit_message_text("Choose a language…")


app = Application.builder().token("YOUR_TOKEN").build()
app.add_handler(CallbackQueryHandler(router.route))
app.run_polling()
```

> **Tip:** in `async` code, prefer `await builder.build_async()`. `build()` is a synchronous
> convenience wrapper (it also works inside a running event loop). See
> [Building menus](docs/guide/menu-building.md#build-vs-build_async).

## 📚 Examples at a glance

**Buttons carry arbitrary parameters** — they are encoded into the callback and decoded back into
the `params` dict your handler receives:

```python
menu = (
    MenuBuilder()
    .add_item("📝 Edit", handler="edit_user", user_id=123, field="email")
    .add_item("🗑️ Delete", handler="delete_user", user_id=123, confirm=True)
    .columns(1)
    .add_back_button(handler="user_list", page=2)
    .build()
)
```

**Nested submenus:**

```python
users = MenuBuilder().add_item("Add user", handler="add_user").add_back_button()
main = MenuBuilder().add_submenu("👥 Users", users)
```

**Storage backends** — in-memory, SQL, and Redis/Valkey backends ship built-in; for anything else,
subclass `BaseStorage` (or satisfy the `StorageBackend` protocol) and pass it in:

```python
from telegram_menu_builder import MenuBuilder
from telegram_menu_builder.storage import RedisStorage  # one client, also speaks Valkey

builder = MenuBuilder(storage=RedisStorage(url="redis://localhost:6379/0"))
# custom backends: docs/advanced/custom-storage.md
```

See the runnable [examples/](examples/) and the [guides](#-documentation) for complete,
copy-pasteable bots — including [ConversationHandler integration](docs/conversation_handler_guide.md).

## 🧠 How callback encoding works

Telegram limits `callback_data` to 64 bytes. The library chooses a strategy automatically:

1. **Inline** (fits in 64 bytes) — JSON → zlib → base64 directly in the callback.
2. **Short-term** — stored in the backend with a TTL; the callback carries a short reference.
3. **Persistent** — stored without expiry for large/long-lived payloads.

Identical payloads reuse the same key (deduplication). Full details:
[Callback encoding internals](docs/advanced/encoding.md).

## 📖 Documentation

📚 **Rendered site:** <https://smoxy.github.io/telegram-menu-builder/>

| Get started | Guides | Advanced | Reference |
| --- | --- | --- | --- |
| [Installation](docs/installation.md) | [Building menus](docs/guide/menu-building.md) | [Encoding internals](docs/advanced/encoding.md) | [MenuBuilder](docs/api/builder.md) |
| [Quick start](docs/quickstart.md) | [Routing callbacks](docs/guide/routing.md) | [Custom storage](docs/advanced/custom-storage.md) | [MenuRouter](docs/api/router.md) |
| | [Storage backends](docs/guide/storage.md) | | [Types & models](docs/api/types.md) |
| | [ConversationHandler](docs/conversation_handler_guide.md) | | [Encoding](docs/api/encoding.md) · [Storage](docs/api/storage.md) |

**Project docs:** [Changelog](CHANGELOG.md) · [Security policy](SECURITY.md) ·
[Dependency & CVE audit](docs/dependency-audit.md) · [Python compatibility](docs/python-compatibility.md) ·
[Development guide](docs/development.md) · [Contributing](CONTRIBUTING.md)

## 🤝 Contributing

**Every contribution is welcome** — bug reports, documentation, examples, and code alike. Start
with the [Contributing guide](CONTRIBUTING.md) and the [Development guide](docs/development.md).

```bash
git clone https://github.com/smoxy/telegram-menu-builder.git
cd telegram-menu-builder
python -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pre-commit install
make test          # run the suite (≈90% coverage)
```

Before opening a pull request, make sure the gates pass:

```bash
make lint          # ruff + black --check
make type-check    # mypy --strict + pyright
make test          # pytest with coverage
```

- 🐛 **Bugs** → [open an issue](https://github.com/smoxy/telegram-menu-builder/issues) (templates provided).
- 💡 **Ideas / questions** → [GitHub Discussions](https://github.com/smoxy/telegram-menu-builder/discussions).
- 🤖 **AI-assisted development** → this repo ships [`CLAUDE.md`](CLAUDE.md), [`AGENTS.md`](AGENTS.md),
  and ready-made agents/skills under [`.claude/`](.claude/) to keep changes consistent.

Releases are published to PyPI automatically via [GitHub Releases and trusted publishing](.github/workflows/python-publish.yml) — no manual token handling needed.

## 🔐 Security

Found a vulnerability? Please report it privately — see [SECURITY.md](SECURITY.md). Dependency CVEs
are tracked in the [dependency audit](docs/dependency-audit.md) (for example, pydantic is pinned
`>=2.4` to exclude CVE-2024-3772).

## 🗺️ Roadmap

- ✅ Fluent builder, navigation, submenus
- ✅ Smart callback encoding (inline / short-term / persistent) with compression & dedup
- ✅ `MenuRouter` with middleware and handler groups
- ✅ In-memory storage backend, strict typing, CI
- ✅ Built-in SQL backend via SQLAlchemy (async) for PostgreSQL/Supabase, MySQL/MariaDB, and SQLite
  (install `[sql]`, plus `[postgres]` or `[mysql]` for those drivers — see
  [storage backends](docs/guide/storage.md))
- ✅ Built-in Redis backend via `redis-py` that also speaks **Valkey** (the RESP-compatible Redis
  fork we recommend), verified against Redis 7.4.9 & Valkey 8.1.8 — install `[redis]` (see
  [Redis/Valkey storage](docs/guide/redis-storage.md))
- ✅ Application-free build (`to_markup` / `to_raw`) + atomic multi-operator claim + testing helpers
  (v0.4.0)
- 📅 Helpers for pagination and form/wizard flows

## 📝 License

Released under the **MIT License** — see [LICENSE](LICENSE). Free for commercial, private, and
personal use, modification, and distribution. Using this code to **train AI/ML models is explicitly
permitted**. Attribution is appreciated but not required.

---

Made with ❤️ for the Telegram Bot community · [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) v20+ · [Pydantic v2](https://github.com/pydantic/pydantic)
