Modern, fully async framework for Bale (بله) messenger bots — inspired by aiogram.
Project description
If you have ever worked with aiogram, you already know baleio. Same architecture,
same patterns — this time for the Bale bot API (https://tapi.bale.ai). Routers,
magic filters, FSM, dependency injection, keyboard builders, callback-data factories
and centralized error handling all work the way you expect.
Features
- ⚡️ Fully asynchronous on top of
aiohttp. - 🧩
Dispatcher/Routerwith nested routing and per-event observers. - 🎯 Filters:
Command,CommandStart,StateFilter, the magic filterF, plain callables, and&/|/~combinators. - 🏷️
CallbackDatafactory — type-safe, packed/unpacked structured callback payloads. - 💾 Finite State Machine with
StatesGroup/Stateand pluggable storage (MemoryStorage). - ⌨️ Keyboard builders for inline and reply keyboards.
- 🧱 Pydantic v2 models for every Bale API type.
- 🔌 Middlewares at the event level (inner & outer).
- 🧯 Centralized error handling via
@dp.errors()andErrorEvent. - 📎 File uploads (
multipart/form-data), sending byfile_idor URL, and media groups. - 💳 Wallet payments (
sendInvoice,PreCheckoutQuery,answerPreCheckoutQuery, …). - 🪝 Long polling and webhook support.
Installation
pip install baleio # once published to PyPI
# or, from source:
git clone https://github.com/ehsndvr/baleio && cd baleio
pip install -e .
Requirements: Python 3.9+, aiohttp, pydantic>=2, magic-filter.
Quick start
The snippet below is the official aiogram quickstart, ported to baleio almost
line-for-line. Only the import roots and the text formatter change (Bale renders
Markdown, so md replaces aiogram's html).
import asyncio
import logging
import sys
from os import getenv
from baleio import Bot, Dispatcher, md
from baleio.client.default import DefaultBotProperties
from baleio.enums import ParseMode
from baleio.filters import CommandStart
from baleio.types import Message
TOKEN = getenv("BOT_TOKEN") # get one from @botfather in Bale
dp = Dispatcher()
@dp.message(CommandStart())
async def command_start_handler(message: Message) -> None:
await message.answer(f"Hello, {md.bold(message.from_user.full_name)}!")
@dp.message()
async def echo_handler(message: Message) -> None:
try:
await message.send_copy(chat_id=message.chat.id)
except TypeError:
await message.answer("Nice try!")
async def main() -> None:
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.MARKDOWN))
await dp.start_polling(bot)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())
Compatibility note: Bale does not accept a
parse_moderequest parameter (it always renders Markdown), butDefaultBotProperties(parse_mode=...)is accepted for full aiogram parity and safely ignored. Usemdfor bold/italic/link formatting.
Core concepts
The Bot client
bot = Bot("123456:TOKEN")
me = await bot.get_me()
await bot.send_message(chat_id, "Hello")
await bot.send_photo(chat_id, "https://example.com/pic.jpg", caption="A photo")
await bot.send_photo(chat_id, FSInputFile("local.jpg")) # upload from disk
await bot.edit_message_text(chat_id, message_id, "New text")
await bot.delete_message(chat_id, message_id)
Every documented Bale method is implemented: sendMessage, forwardMessage,
copyMessage, sendPhoto/Audio/Document/Video/Animation/Voice, sendMediaGroup,
sendLocation, sendContact, sendChatAction, getFile, answerCallbackQuery,
askReview, chat administration (banChatMember, promoteChatMember, getChat,
getChatAdministrators, pinChatMessage, …), message editing/deletion, stickers,
and payments (sendInvoice, createInvoiceLink, answerPreCheckoutQuery,
inquireTransaction).
Dispatcher & Router
from baleio import Dispatcher, Router
from baleio.filters import Command
dp = Dispatcher()
admin = Router(name="admin")
dp.include_router(admin)
@dp.message(Command("start"))
async def start(message): ...
@admin.message(Command("ban"))
async def ban(message): ...
Handlers are checked in registration order; the first whose filters all pass runs.
Dependency injection
Each handler receives the event as its first argument. Remaining parameters are filled by name from the propagated context:
@dp.message(Command("me"))
async def me(message: Message, bot: Bot, state: FSMContext):
... # bot and state are injected automatically
Available keys: bot, state, raw_state, event_update (the Update),
event_router, and anything a filter returned (e.g. command).
Filters
from baleio.filters import Command, CommandStart, StateFilter, F
@dp.message(Command("help")) # /help
@dp.message(CommandStart()) # /start
@dp.message(F.text == "hello") # magic filter
@dp.message(F.text.startswith("/"))
@dp.message(F.from_user.id == 12345)
@dp.message(lambda m: m.text and len(m.text) > 100) # plain callable
@dp.message(Command("go") & (F.from_user.id == 42)) # combinators
Command also injects a CommandObject:
@dp.message(Command("say"))
async def say(message: Message, command: CommandObject):
await message.answer(command.args or "Say what?")
CallbackData factory
Type-safe, structured callback payloads — exactly like aiogram:
from baleio import F
from baleio.filters import CallbackData
from baleio.types import CallbackQuery
from baleio.utils import InlineKeyboardBuilder
class Vote(CallbackData, prefix="vote"): # optional: prefix="v", sep="|"
action: str
post_id: int
kb = (
InlineKeyboardBuilder()
.button("👍", callback_data=Vote(action="up", post_id=7).pack()) # -> "vote:up:7"
.button("👎", callback_data=Vote(action="down", post_id=7).pack())
.as_markup()
)
@dp.callback_query(Vote.filter(F.action == "up"))
async def upvote(query: CallbackQuery, callback_data: Vote):
await query.answer(f"Post {callback_data.post_id} liked")
Fields are coerced back to their declared types on unpack (post_id is an int),
and Bale's 64-byte callback_data limit is enforced automatically.
Finite State Machine
from baleio.fsm import State, StatesGroup, FSMContext
class Form(StatesGroup):
name = State()
age = State()
@dp.message(Command("start"))
async def start(message: Message, state: FSMContext):
await state.set_state(Form.name)
await message.answer("What's your name?")
@dp.message(Form.name) # a State used directly as a filter
async def got_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(Form.age)
await message.answer("How old are you?")
Keyboards
from baleio.utils import InlineKeyboardBuilder
kb = (
InlineKeyboardBuilder()
.button("Yes", callback_data="yes")
.button("No", callback_data="no")
.adjust(2)
.as_markup()
)
await message.answer("Agree?", reply_markup=kb)
@dp.callback_query(F.data == "yes")
async def yes(cb):
await cb.answer("Great!", show_alert=True)
await cb.message.edit_text("You chose: Yes")
⚠️ You must call
answerCallbackQuery(orcb.answer()) to release the button.
Centralized error handling
If a handler raises, an ErrorEvent is routed to @dp.errors() handlers:
from baleio.filters import ExceptionTypeFilter
from baleio.types import ErrorEvent
@dp.errors(ExceptionTypeFilter(ValueError))
async def on_value_error(event: ErrorEvent):
await event.update.message.answer("Something went wrong 😔")
@dp.errors() # any other unhandled error
async def on_any_error(event: ErrorEvent):
logging.exception("Unhandled", exc_info=event.exception)
If no error handler matches, the exception is re-raised (logged in polling, so the bot stays alive; handled by you in webhook mode).
Shortcut methods
Every received object is bound to its Bot, so you never pass bot around:
await message.answer("...") # sendMessage to the same chat
await message.reply("...") # reply (reply_to_message_id)
await message.answer_photo(photo)
await message.send_copy(chat_id) # content-aware copy
await message.edit_text("...")
await message.delete()
await callback.answer("...")
await callback.message.edit_text("...")
Receiving updates
Long polling
await dp.start_polling(bot) # async
dp.run_polling(bot) # blocking (wraps asyncio.run)
await dp.start_polling(bot, drop_pending_updates=True)
Webhook
from aiohttp import web
from baleio.types import Update
async def handler(request):
await dp.feed_update(bot, Update.model_validate(await request.json()))
return web.Response()
Examples
examples/quickstart.py— the aiogram quickstart, ported.examples/echo_bot.py— echo + inline keyboard + callbacks.examples/fsm_form_bot.py— a registration form with FSM.examples/webhook_bot.py— receiving updates via webhook.
export BOT_TOKEN=123456:xxxxxxxx
python examples/echo_bot.py
Development
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
aiogram → baleio cheat sheet
| aiogram | baleio |
|---|---|
from aiogram import Bot, Dispatcher, F |
from baleio import Bot, Dispatcher, F |
aiogram.types.Message |
baleio.types.Message |
aiogram.filters.Command |
baleio.filters.Command |
aiogram.filters.callback_data.CallbackData |
baleio.filters.CallbackData |
aiogram.fsm.state.StatesGroup |
baleio.fsm.StatesGroup |
@dp.errors() + ErrorEvent |
@dp.errors() + baleio.types.ErrorEvent |
dp.start_polling(bot) |
dp.start_polling(bot) |
InlineKeyboardBuilder |
baleio.utils.InlineKeyboardBuilder |
aiogram.html.bold |
baleio.md.bold (Bale is Markdown-only) |
Key differences from Telegram/aiogram: Bale has no parse_mode parameter (always
Markdown), its updates are limited to message, edited_message, callback_query
and pre_checkout_query, and payments are wallet-only.
Contributing
Contributions are welcome! Please read CONTRIBUTING.md and open an issue or pull request.
License
MIT © baleio contributors.
Disclaimer:
baleiois an independent, community project and is not affiliated with or endorsed by Bale Messenger or the aiogram project.
Project details
Release history Release notifications | RSS feed
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 baleio-0.1.0.tar.gz.
File metadata
- Download URL: baleio-0.1.0.tar.gz
- Upload date:
- Size: 56.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9369bd34009f8243d683e2c39b262162b8e7a2e5527c3bf56e951f86eea92ecc
|
|
| MD5 |
cbaa883c1bec58a994e7d4f18ba70f7a
|
|
| BLAKE2b-256 |
8564be98db32515032f80ff8439e4d5929976d961187177c8af29ab3241a6535
|
Provenance
The following attestation bundles were made for baleio-0.1.0.tar.gz:
Publisher:
publish.yml on ehsndvr/baleio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
baleio-0.1.0.tar.gz -
Subject digest:
9369bd34009f8243d683e2c39b262162b8e7a2e5527c3bf56e951f86eea92ecc - Sigstore transparency entry: 2084481328
- Sigstore integration time:
-
Permalink:
ehsndvr/baleio@b15b92732bd230935fc24bf9b740c724c937cd3d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ehsndvr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b15b92732bd230935fc24bf9b740c724c937cd3d -
Trigger Event:
release
-
Statement type:
File details
Details for the file baleio-0.1.0-py3-none-any.whl.
File metadata
- Download URL: baleio-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cded3614389146212987468a7bacd164cb1e4ad37bad5a78aaf47943cfda4e90
|
|
| MD5 |
a6e84257bc8884a089ba41da5db3d1b3
|
|
| BLAKE2b-256 |
d36aac145397e6c408dcc03b1a84ffd2a140ea74de66bf8e810b124201c5a869
|
Provenance
The following attestation bundles were made for baleio-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ehsndvr/baleio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
baleio-0.1.0-py3-none-any.whl -
Subject digest:
cded3614389146212987468a7bacd164cb1e4ad37bad5a78aaf47943cfda4e90 - Sigstore transparency entry: 2084481331
- Sigstore integration time:
-
Permalink:
ehsndvr/baleio@b15b92732bd230935fc24bf9b740c724c937cd3d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ehsndvr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b15b92732bd230935fc24bf9b740c724c937cd3d -
Trigger Event:
release
-
Statement type: