baleropy 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- baleropy-0.2.0/.gitignore +14 -0
- baleropy-0.2.0/CHANGELOG.md +25 -0
- baleropy-0.2.0/LICENSE +21 -0
- baleropy-0.2.0/PKG-INFO +315 -0
- baleropy-0.2.0/README.md +270 -0
- baleropy-0.2.0/baleropy/__init__.py +21 -0
- baleropy-0.2.0/baleropy/__meta__.py +1 -0
- baleropy-0.2.0/baleropy/client/__init__.py +3 -0
- baleropy-0.2.0/baleropy/client/bot.py +568 -0
- baleropy-0.2.0/baleropy/client/server.py +32 -0
- baleropy-0.2.0/baleropy/client/session/__init__.py +2 -0
- baleropy-0.2.0/baleropy/client/session/aiohttp.py +84 -0
- baleropy-0.2.0/baleropy/client/session/base.py +124 -0
- baleropy-0.2.0/baleropy/dispatcher/__init__.py +5 -0
- baleropy-0.2.0/baleropy/dispatcher/dispatcher.py +230 -0
- baleropy-0.2.0/baleropy/dispatcher/event/__init__.py +0 -0
- baleropy-0.2.0/baleropy/dispatcher/event/bases.py +32 -0
- baleropy-0.2.0/baleropy/dispatcher/event/handler.py +86 -0
- baleropy-0.2.0/baleropy/dispatcher/event/observer.py +86 -0
- baleropy-0.2.0/baleropy/dispatcher/flags.py +19 -0
- baleropy-0.2.0/baleropy/dispatcher/middlewares/__init__.py +2 -0
- baleropy-0.2.0/baleropy/dispatcher/middlewares/base.py +33 -0
- baleropy-0.2.0/baleropy/dispatcher/middlewares/builtin.py +106 -0
- baleropy-0.2.0/baleropy/dispatcher/middlewares/user_context.py +29 -0
- baleropy-0.2.0/baleropy/dispatcher/router.py +129 -0
- baleropy-0.2.0/baleropy/exceptions.py +88 -0
- baleropy-0.2.0/baleropy/filters/__init__.py +7 -0
- baleropy-0.2.0/baleropy/filters/base.py +79 -0
- baleropy-0.2.0/baleropy/filters/callback_data.py +92 -0
- baleropy-0.2.0/baleropy/filters/chat.py +64 -0
- baleropy-0.2.0/baleropy/filters/command.py +124 -0
- baleropy-0.2.0/baleropy/filters/extra.py +49 -0
- baleropy-0.2.0/baleropy/filters/magic.py +155 -0
- baleropy-0.2.0/baleropy/filters/state.py +37 -0
- baleropy-0.2.0/baleropy/fsm/__init__.py +4 -0
- baleropy-0.2.0/baleropy/fsm/context.py +39 -0
- baleropy-0.2.0/baleropy/fsm/middleware.py +78 -0
- baleropy-0.2.0/baleropy/fsm/state.py +108 -0
- baleropy-0.2.0/baleropy/fsm/storage/__init__.py +3 -0
- baleropy-0.2.0/baleropy/fsm/storage/base.py +46 -0
- baleropy-0.2.0/baleropy/fsm/storage/memory.py +37 -0
- baleropy-0.2.0/baleropy/fsm/storage/redis.py +68 -0
- baleropy-0.2.0/baleropy/methods/__init__.py +14 -0
- baleropy-0.2.0/baleropy/methods/base.py +64 -0
- baleropy-0.2.0/baleropy/methods/chats.py +118 -0
- baleropy-0.2.0/baleropy/methods/limits.py +32 -0
- baleropy-0.2.0/baleropy/methods/media_group.py +59 -0
- baleropy-0.2.0/baleropy/methods/messages.py +168 -0
- baleropy-0.2.0/baleropy/methods/misc.py +137 -0
- baleropy-0.2.0/baleropy/py.typed +0 -0
- baleropy-0.2.0/baleropy/types/__init__.py +50 -0
- baleropy-0.2.0/baleropy/types/base.py +35 -0
- baleropy-0.2.0/baleropy/types/chat.py +127 -0
- baleropy-0.2.0/baleropy/types/input_file.py +53 -0
- baleropy-0.2.0/baleropy/types/markup.py +73 -0
- baleropy-0.2.0/baleropy/types/media.py +104 -0
- baleropy-0.2.0/baleropy/types/message.py +321 -0
- baleropy-0.2.0/baleropy/types/payments.py +53 -0
- baleropy-0.2.0/baleropy/utils/__init__.py +5 -0
- baleropy-0.2.0/baleropy/utils/chat_action.py +75 -0
- baleropy-0.2.0/baleropy/utils/deep_linking.py +43 -0
- baleropy-0.2.0/baleropy/utils/keyboard.py +157 -0
- baleropy-0.2.0/baleropy/utils/media_group.py +71 -0
- baleropy-0.2.0/baleropy/utils/text.py +47 -0
- baleropy-0.2.0/baleropy/webhook/__init__.py +1 -0
- baleropy-0.2.0/baleropy/webhook/aiohttp_server.py +93 -0
- baleropy-0.2.0/examples/colored_buttons.py +40 -0
- baleropy-0.2.0/examples/echo_bot.py +35 -0
- baleropy-0.2.0/examples/fsm_form.py +61 -0
- baleropy-0.2.0/examples/modular_bot.py +96 -0
- baleropy-0.2.0/examples/webhook_bot.py +42 -0
- baleropy-0.2.0/pyproject.toml +79 -0
- baleropy-0.2.0/tests/test_dispatcher.py +251 -0
- baleropy-0.2.0/tests/test_examples.py +130 -0
- baleropy-0.2.0/tests/test_filters.py +172 -0
- baleropy-0.2.0/tests/test_fsm.py +175 -0
- baleropy-0.2.0/tests/test_runtime.py +235 -0
- baleropy-0.2.0/tests/test_session.py +280 -0
- baleropy-0.2.0/tests/test_shortcuts_and_keyboards.py +189 -0
- baleropy-0.2.0/tests/test_utils_and_limits.py +212 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Serialize FSM processing for updates sharing a storage key within the same process.
|
|
6
|
+
- Add `@dp.unknown()` handlers for update types not modeled by the library.
|
|
7
|
+
- Stream `FSInputFile` uploads in chunks instead of loading the whole file into memory.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
- Published on PyPI as **`balegram-api`** (`pip install balegram-api`); the import name stays `balegram`.
|
|
12
|
+
- Colored buttons: `style` on inline/reply buttons (`ButtonStyle`), `style=` in the keyboard builders.
|
|
13
|
+
- Chat ids are always sent as strings (Bale rejects an integer `chat_id` on `sendChatAction`).
|
|
14
|
+
- 429 handling: retry by default, one pause shared by all requests of a bot, bounded attempts.
|
|
15
|
+
- `Dispatcher.start_polling(max_concurrent_updates=...)`; API errors are logged as one line.
|
|
16
|
+
- Documented length limits are validated client-side.
|
|
17
|
+
- New helpers: deep links, Markdown helpers, `split_text`, `ChatActionSender`, `MediaGroupBuilder`,
|
|
18
|
+
`InlineKeyboardBuilder.from_markup/copy`, `Bot.business()`.
|
|
19
|
+
- New filters: `MagicData`, `ExceptionTypeFilter`, `ExceptionMessageFilter`; `F.a == F.b`; `get_flag`.
|
|
20
|
+
- A handler parameter named `event` always receives the event.
|
|
21
|
+
- `Message.reply_markup` no longer drops an update when Bale sends an unexpected shape.
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
First release: Bot API client, routers, middlewares, filters, FSM, polling and webhook.
|
baleropy-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reza Mardani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
baleropy-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: baleropy
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Modern, fully-typed, asynchronous framework for the Bale Bot API
|
|
5
|
+
Project-URL: Homepage, https://github.com/rezamardani01/Baleropy
|
|
6
|
+
Project-URL: Documentation, https://rezamardani01.github.io/baleropy-docs/
|
|
7
|
+
Project-URL: Source, https://github.com/rezamardani01/Baleropy
|
|
8
|
+
Project-URL: Issues, https://github.com/rezamardani01/Baleropy/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/rezamardani01/Baleropy/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Reza Mardani
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: aiogram,api,asyncio,bale,bale-bot,bot,chatbot,framework,pydantic
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Framework :: Pydantic :: 2
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Natural Language :: English
|
|
19
|
+
Classifier: Natural Language :: Persian
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Topic :: Communications :: Chat
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
30
|
+
Classifier: Typing :: Typed
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Requires-Dist: aiohttp>=3.9
|
|
33
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: aresponses>=3; extra == 'dev'
|
|
36
|
+
Requires-Dist: build>=1; extra == 'dev'
|
|
37
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
41
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
42
|
+
Provides-Extra: redis
|
|
43
|
+
Requires-Dist: redis>=5; extra == 'redis'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# baleropy
|
|
47
|
+
|
|
48
|
+
Modern, fully-typed, asynchronous framework for the [Bale](https://bale.ai) Bot API.
|
|
49
|
+
Routers, middlewares, composable filters, FSM and pydantic models, with an architecture
|
|
50
|
+
familiar to [aiogram](https://github.com/aiogram/aiogram) users.
|
|
51
|
+
|
|
52
|
+
- **Routers**: nest them freely to split a bot into modules.
|
|
53
|
+
- **Middlewares**: outer (every event) and inner (after a handler was chosen), class or function based, at update, router or event level. Outgoing request middlewares too.
|
|
54
|
+
- **Filters**: `F` magic filter (`F.a == F.b` too), `MagicData`, `Command`, `CallbackData`, `StateFilter`, chat/admin/membership filters, and your own. Combine with `&`, `|`, `~`. A filter can return a `dict` that is injected into the handler.
|
|
55
|
+
- **Dependency injection**: handlers just declare the arguments they want (`bot`, `state`, `event_from_user`, your own data...).
|
|
56
|
+
- **FSM**: states and groups, pluggable storage (memory, Redis), three isolation strategies.
|
|
57
|
+
- **Full Bot API coverage**: every method and type of the [Bale docs](https://docs.bale.ai), as typed method objects *and* `bot.*` shortcuts. Unknown fields never break parsing.
|
|
58
|
+
- **Polling and webhook** runners, 429 auto-retry, graceful shutdown.
|
|
59
|
+
- **Colored buttons** (`style=`), deep links, Markdown helpers, Business API, flood control.
|
|
60
|
+
- Python **3.10+**, strict `mypy`, tested on 3.10 and 3.15.
|
|
61
|
+
- 📘 **Full documentation (Persian + English): https://rezamardani01.github.io/baleropy-docs/**
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install baleropy
|
|
67
|
+
# with Redis storage for the FSM
|
|
68
|
+
pip install "baleropy[redis]"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Install the **`baleropy`** package and import it with `import baleropy`.
|
|
72
|
+
|
|
73
|
+
Latest unreleased code: `pip install git+https://github.com/rezamardani01/Baleropy.git`
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
from baleropy import Bot, Dispatcher, F
|
|
80
|
+
from baleropy.filters import CommandStart
|
|
81
|
+
from baleropy.types import Message
|
|
82
|
+
|
|
83
|
+
TOKEN = "YOUR_BOT_TOKEN" # get it from @botfather (https://ble.ir/botfather)
|
|
84
|
+
|
|
85
|
+
dp = Dispatcher()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dp.message(CommandStart())
|
|
89
|
+
async def start(message: Message):
|
|
90
|
+
await message.answer("Hello!")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dp.message(F.text)
|
|
94
|
+
async def echo(message: Message):
|
|
95
|
+
await message.reply(message.text)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
asyncio.run(dp.start_polling(Bot(TOKEN), skip_updates=True))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Routers
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from baleropy import Router
|
|
105
|
+
|
|
106
|
+
shop = Router(name="shop")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@shop.message(Command("buy"))
|
|
110
|
+
async def buy(message: Message): ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
dp.include_router(shop)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Events are offered to the router's own handlers first, then to its sub-routers; the first matching
|
|
117
|
+
handler wins (raise `SkipHandler` to pass the event on).
|
|
118
|
+
|
|
119
|
+
## Middlewares
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from baleropy.dispatcher import BaseMiddleware
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class DbMiddleware(BaseMiddleware):
|
|
126
|
+
async def __call__(self, handler, event, data):
|
|
127
|
+
async with pool.acquire() as db:
|
|
128
|
+
data["db"] = db # available as `db` in every handler
|
|
129
|
+
return await handler(event, data)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
dp.update.outer_middleware(DbMiddleware()) # every update
|
|
133
|
+
router.message.middleware(my_inner_mw) # only when a message handler matched
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Built in: `LoggingMiddleware`, `ThrottlingMiddleware`, `CallbackAnswerMiddleware`.
|
|
137
|
+
Raise `CancelHandler` inside a middleware to drop the event.
|
|
138
|
+
|
|
139
|
+
## Filters
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from baleropy import F
|
|
143
|
+
from baleropy.filters import Filter, Command, CallbackData, ChatTypeFilter
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.message(F.text.startswith("!"), ChatTypeFilter("group"))
|
|
147
|
+
@router.message(Command("ban"), F.reply_to_message)
|
|
148
|
+
@router.message((F.text == "a") | (F.text == "b"))
|
|
149
|
+
class Product(CallbackData, prefix="prod"):
|
|
150
|
+
id: int
|
|
151
|
+
action: str
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.callback_query(Product.filter(F.action == "buy"))
|
|
155
|
+
async def buy(query, callback_data: Product): ...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class IsPremium(Filter): # custom filter
|
|
159
|
+
async def __call__(self, event, db) -> bool | dict:
|
|
160
|
+
user = await db.get_user(event.from_user.id)
|
|
161
|
+
return {"user": user} if user.premium else False
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## FSM
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from baleropy.fsm import State, StatesGroup, FSMContext
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Form(StatesGroup):
|
|
171
|
+
name = State()
|
|
172
|
+
age = State()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dp.message(Command("form"))
|
|
176
|
+
async def begin(message: Message, state: FSMContext):
|
|
177
|
+
await state.set_state(Form.name)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dp.message(Form.name)
|
|
181
|
+
async def name(message: Message, state: FSMContext):
|
|
182
|
+
await state.update_data(name=message.text)
|
|
183
|
+
await state.set_state(Form.age)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Updates for the same FSM key (by default, the same user in the same chat) are processed one at a time
|
|
187
|
+
within a process. This does not coordinate separate bot processes or workers; use distributed locking
|
|
188
|
+
if those share the same FSM storage.
|
|
189
|
+
|
|
190
|
+
## Unknown updates
|
|
191
|
+
|
|
192
|
+
When Bale sends an update type this version does not recognize, it can be handled with a catch-all
|
|
193
|
+
handler. The original `Update` is passed to the handler, and unmodeled fields are available in
|
|
194
|
+
`unknown_event_types`:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from baleropy.types import Update
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dp.unknown()
|
|
201
|
+
async def handle_unknown(update: Update):
|
|
202
|
+
print(update.unknown_event_types)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Webhook
|
|
206
|
+
|
|
207
|
+
See [`examples/webhook_bot.py`](https://github.com/rezamardani01/Baleropy/blob/main/examples/webhook_bot.py). Bale webhooks need HTTPS on port 443 or 88
|
|
208
|
+
and send no secret header, so use `secret_path()` (or a `verify=` callback) to keep the URL private.
|
|
209
|
+
|
|
210
|
+
## Sending files
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from baleropy.types import FSInputFile, BufferedInputFile
|
|
214
|
+
|
|
215
|
+
await bot.send_document(chat_id, FSInputFile("report.pdf"))
|
|
216
|
+
await bot.send_photo(chat_id, BufferedInputFile(data, "pic.png"), caption="hi")
|
|
217
|
+
await bot.send_photo(chat_id, "an_existing_file_id")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`FSInputFile` streams the file in chunks during upload, so it does not load the entire file into
|
|
221
|
+
memory. `BufferedInputFile` and `BufferedInputFile.from_file()` keep the file contents in memory.
|
|
222
|
+
|
|
223
|
+
## Colored buttons
|
|
224
|
+
|
|
225
|
+
Bale's beta clients render `success` (green), `danger` (red) and `primary` (blue) buttons.
|
|
226
|
+
Older clients ignore `style` and show a normal button.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from baleropy.types import ButtonStyle
|
|
230
|
+
from baleropy.utils import InlineKeyboardBuilder
|
|
231
|
+
|
|
232
|
+
kb = InlineKeyboardBuilder()
|
|
233
|
+
kb.button(text="Accept", callback_data="ok", style=ButtonStyle.SUCCESS)
|
|
234
|
+
kb.button(text="Reject", callback_data="no", style="danger") # plain strings work too
|
|
235
|
+
kb.button(text="More", callback_data="more", style=ButtonStyle.PRIMARY)
|
|
236
|
+
await message.answer("Choose:", reply_markup=kb.adjust(2, 1).as_markup())
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`ReplyKeyboardBuilder.button(..., style=...)` accepts the same argument (only inline buttons are
|
|
240
|
+
confirmed to render colors so far).
|
|
241
|
+
|
|
242
|
+
## Bot links, formatting and other helpers
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from baleropy.utils import (
|
|
246
|
+
ChatActionSender,
|
|
247
|
+
MediaGroupBuilder,
|
|
248
|
+
bold,
|
|
249
|
+
italic,
|
|
250
|
+
link,
|
|
251
|
+
split_text,
|
|
252
|
+
create_start_link,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
url = await create_start_link(bot, "ref42") # https://ble.ir/<bot>?start=ref42
|
|
256
|
+
# read it back: @dp.message(CommandStart(deep_link=True)) -> command.args == "ref42"
|
|
257
|
+
|
|
258
|
+
await message.answer(f"price: {bold('10')} rial, see {link('docs', 'https://docs.bale.ai')}")
|
|
259
|
+
for chunk in split_text(very_long_text): # Bale allows 4096 characters per message
|
|
260
|
+
await message.answer(chunk)
|
|
261
|
+
|
|
262
|
+
async with ChatActionSender.typing(bot, message.chat.id): # "typing..." while this block runs
|
|
263
|
+
answer = await slow_work()
|
|
264
|
+
|
|
265
|
+
album = MediaGroupBuilder(caption="Trip").add_photo(FSInputFile("a.jpg")).add_photo("file_id")
|
|
266
|
+
await bot.send_media_group(message.chat.id, album.build())
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Bale formats every message as Markdown (`**bold**`, `__italic__`, `[label](url)`); the docs ask for a
|
|
270
|
+
space before the opening and after the closing bold/italic marker.
|
|
271
|
+
|
|
272
|
+
Documented limits are checked before a request leaves your machine (message text 1-4096, callback
|
|
273
|
+
answer text 200, media-group caption 1024, invoice title 32 / description 255 / payload 128 bytes,
|
|
274
|
+
sticker set name/title 64), so you get a clear `ValidationError` instead of an HTTP 400.
|
|
275
|
+
|
|
276
|
+
## Business API (bulk messaging)
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
bot = Bot.business("TOKEN") # https://tapi.bale.ai/business/bot<TOKEN>/...
|
|
280
|
+
await bot.send_message(private_chat_id, "Hello!")
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Only `getMe`, `sendMessage`, `forwardMessage`, `sendPhoto`, `sendVideo`, `sendAudio` and
|
|
284
|
+
`sendDocument` exist there, `chat_id` must be a private chat and files are sent by `file_id` only.
|
|
285
|
+
Sending is billed from your Bale business account credit.
|
|
286
|
+
|
|
287
|
+
## Flood control
|
|
288
|
+
|
|
289
|
+
On HTTP 429 the session pauses *every* request of that bot for `retry_after` seconds and retries
|
|
290
|
+
(`AiohttpSession(retry_on_flood=60, max_flood_retries=5)`). `Dispatcher.start_polling` handles at
|
|
291
|
+
most `max_concurrent_updates=50` updates at once.
|
|
292
|
+
|
|
293
|
+
## Examples
|
|
294
|
+
|
|
295
|
+
[`examples/`](https://github.com/rezamardani01/Baleropy/tree/main/examples) holds a minimal bot, a modular bot (routers + middlewares + custom filter),
|
|
296
|
+
an FSM form, colored buttons and a webhook bot. Put your token in the `TOKEN` constant at the top
|
|
297
|
+
of each file. The examples are exercised by the test suite.
|
|
298
|
+
|
|
299
|
+
## Development
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
303
|
+
ruff check . && mypy baleropy && pytest
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Notes about Bale
|
|
307
|
+
|
|
308
|
+
- `answerCallbackQuery` is not supported by old clients; `CallbackQuery.answer()` skips them
|
|
309
|
+
(detected by an id starting with `1`, as the Bale docs describe).
|
|
310
|
+
- Messages can only be deleted within 48 hours; the server keeps 2000 updates for 24 hours.
|
|
311
|
+
- Payments use the Bale wallet (`provider_token` for tests: `WALLET-TEST-1111111111111111`).
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT
|
baleropy-0.2.0/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# baleropy
|
|
2
|
+
|
|
3
|
+
Modern, fully-typed, asynchronous framework for the [Bale](https://bale.ai) Bot API.
|
|
4
|
+
Routers, middlewares, composable filters, FSM and pydantic models, with an architecture
|
|
5
|
+
familiar to [aiogram](https://github.com/aiogram/aiogram) users.
|
|
6
|
+
|
|
7
|
+
- **Routers**: nest them freely to split a bot into modules.
|
|
8
|
+
- **Middlewares**: outer (every event) and inner (after a handler was chosen), class or function based, at update, router or event level. Outgoing request middlewares too.
|
|
9
|
+
- **Filters**: `F` magic filter (`F.a == F.b` too), `MagicData`, `Command`, `CallbackData`, `StateFilter`, chat/admin/membership filters, and your own. Combine with `&`, `|`, `~`. A filter can return a `dict` that is injected into the handler.
|
|
10
|
+
- **Dependency injection**: handlers just declare the arguments they want (`bot`, `state`, `event_from_user`, your own data...).
|
|
11
|
+
- **FSM**: states and groups, pluggable storage (memory, Redis), three isolation strategies.
|
|
12
|
+
- **Full Bot API coverage**: every method and type of the [Bale docs](https://docs.bale.ai), as typed method objects *and* `bot.*` shortcuts. Unknown fields never break parsing.
|
|
13
|
+
- **Polling and webhook** runners, 429 auto-retry, graceful shutdown.
|
|
14
|
+
- **Colored buttons** (`style=`), deep links, Markdown helpers, Business API, flood control.
|
|
15
|
+
- Python **3.10+**, strict `mypy`, tested on 3.10 and 3.15.
|
|
16
|
+
- 📘 **Full documentation (Persian + English): https://rezamardani01.github.io/baleropy-docs/**
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install baleropy
|
|
22
|
+
# with Redis storage for the FSM
|
|
23
|
+
pip install "baleropy[redis]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Install the **`baleropy`** package and import it with `import baleropy`.
|
|
27
|
+
|
|
28
|
+
Latest unreleased code: `pip install git+https://github.com/rezamardani01/Baleropy.git`
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from baleropy import Bot, Dispatcher, F
|
|
35
|
+
from baleropy.filters import CommandStart
|
|
36
|
+
from baleropy.types import Message
|
|
37
|
+
|
|
38
|
+
TOKEN = "YOUR_BOT_TOKEN" # get it from @botfather (https://ble.ir/botfather)
|
|
39
|
+
|
|
40
|
+
dp = Dispatcher()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dp.message(CommandStart())
|
|
44
|
+
async def start(message: Message):
|
|
45
|
+
await message.answer("Hello!")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dp.message(F.text)
|
|
49
|
+
async def echo(message: Message):
|
|
50
|
+
await message.reply(message.text)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
asyncio.run(dp.start_polling(Bot(TOKEN), skip_updates=True))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Routers
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from baleropy import Router
|
|
60
|
+
|
|
61
|
+
shop = Router(name="shop")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@shop.message(Command("buy"))
|
|
65
|
+
async def buy(message: Message): ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
dp.include_router(shop)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Events are offered to the router's own handlers first, then to its sub-routers; the first matching
|
|
72
|
+
handler wins (raise `SkipHandler` to pass the event on).
|
|
73
|
+
|
|
74
|
+
## Middlewares
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from baleropy.dispatcher import BaseMiddleware
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DbMiddleware(BaseMiddleware):
|
|
81
|
+
async def __call__(self, handler, event, data):
|
|
82
|
+
async with pool.acquire() as db:
|
|
83
|
+
data["db"] = db # available as `db` in every handler
|
|
84
|
+
return await handler(event, data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
dp.update.outer_middleware(DbMiddleware()) # every update
|
|
88
|
+
router.message.middleware(my_inner_mw) # only when a message handler matched
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Built in: `LoggingMiddleware`, `ThrottlingMiddleware`, `CallbackAnswerMiddleware`.
|
|
92
|
+
Raise `CancelHandler` inside a middleware to drop the event.
|
|
93
|
+
|
|
94
|
+
## Filters
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from baleropy import F
|
|
98
|
+
from baleropy.filters import Filter, Command, CallbackData, ChatTypeFilter
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.message(F.text.startswith("!"), ChatTypeFilter("group"))
|
|
102
|
+
@router.message(Command("ban"), F.reply_to_message)
|
|
103
|
+
@router.message((F.text == "a") | (F.text == "b"))
|
|
104
|
+
class Product(CallbackData, prefix="prod"):
|
|
105
|
+
id: int
|
|
106
|
+
action: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.callback_query(Product.filter(F.action == "buy"))
|
|
110
|
+
async def buy(query, callback_data: Product): ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class IsPremium(Filter): # custom filter
|
|
114
|
+
async def __call__(self, event, db) -> bool | dict:
|
|
115
|
+
user = await db.get_user(event.from_user.id)
|
|
116
|
+
return {"user": user} if user.premium else False
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## FSM
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from baleropy.fsm import State, StatesGroup, FSMContext
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Form(StatesGroup):
|
|
126
|
+
name = State()
|
|
127
|
+
age = State()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dp.message(Command("form"))
|
|
131
|
+
async def begin(message: Message, state: FSMContext):
|
|
132
|
+
await state.set_state(Form.name)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dp.message(Form.name)
|
|
136
|
+
async def name(message: Message, state: FSMContext):
|
|
137
|
+
await state.update_data(name=message.text)
|
|
138
|
+
await state.set_state(Form.age)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Updates for the same FSM key (by default, the same user in the same chat) are processed one at a time
|
|
142
|
+
within a process. This does not coordinate separate bot processes or workers; use distributed locking
|
|
143
|
+
if those share the same FSM storage.
|
|
144
|
+
|
|
145
|
+
## Unknown updates
|
|
146
|
+
|
|
147
|
+
When Bale sends an update type this version does not recognize, it can be handled with a catch-all
|
|
148
|
+
handler. The original `Update` is passed to the handler, and unmodeled fields are available in
|
|
149
|
+
`unknown_event_types`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from baleropy.types import Update
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dp.unknown()
|
|
156
|
+
async def handle_unknown(update: Update):
|
|
157
|
+
print(update.unknown_event_types)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Webhook
|
|
161
|
+
|
|
162
|
+
See [`examples/webhook_bot.py`](https://github.com/rezamardani01/Baleropy/blob/main/examples/webhook_bot.py). Bale webhooks need HTTPS on port 443 or 88
|
|
163
|
+
and send no secret header, so use `secret_path()` (or a `verify=` callback) to keep the URL private.
|
|
164
|
+
|
|
165
|
+
## Sending files
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from baleropy.types import FSInputFile, BufferedInputFile
|
|
169
|
+
|
|
170
|
+
await bot.send_document(chat_id, FSInputFile("report.pdf"))
|
|
171
|
+
await bot.send_photo(chat_id, BufferedInputFile(data, "pic.png"), caption="hi")
|
|
172
|
+
await bot.send_photo(chat_id, "an_existing_file_id")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`FSInputFile` streams the file in chunks during upload, so it does not load the entire file into
|
|
176
|
+
memory. `BufferedInputFile` and `BufferedInputFile.from_file()` keep the file contents in memory.
|
|
177
|
+
|
|
178
|
+
## Colored buttons
|
|
179
|
+
|
|
180
|
+
Bale's beta clients render `success` (green), `danger` (red) and `primary` (blue) buttons.
|
|
181
|
+
Older clients ignore `style` and show a normal button.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from baleropy.types import ButtonStyle
|
|
185
|
+
from baleropy.utils import InlineKeyboardBuilder
|
|
186
|
+
|
|
187
|
+
kb = InlineKeyboardBuilder()
|
|
188
|
+
kb.button(text="Accept", callback_data="ok", style=ButtonStyle.SUCCESS)
|
|
189
|
+
kb.button(text="Reject", callback_data="no", style="danger") # plain strings work too
|
|
190
|
+
kb.button(text="More", callback_data="more", style=ButtonStyle.PRIMARY)
|
|
191
|
+
await message.answer("Choose:", reply_markup=kb.adjust(2, 1).as_markup())
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`ReplyKeyboardBuilder.button(..., style=...)` accepts the same argument (only inline buttons are
|
|
195
|
+
confirmed to render colors so far).
|
|
196
|
+
|
|
197
|
+
## Bot links, formatting and other helpers
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from baleropy.utils import (
|
|
201
|
+
ChatActionSender,
|
|
202
|
+
MediaGroupBuilder,
|
|
203
|
+
bold,
|
|
204
|
+
italic,
|
|
205
|
+
link,
|
|
206
|
+
split_text,
|
|
207
|
+
create_start_link,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
url = await create_start_link(bot, "ref42") # https://ble.ir/<bot>?start=ref42
|
|
211
|
+
# read it back: @dp.message(CommandStart(deep_link=True)) -> command.args == "ref42"
|
|
212
|
+
|
|
213
|
+
await message.answer(f"price: {bold('10')} rial, see {link('docs', 'https://docs.bale.ai')}")
|
|
214
|
+
for chunk in split_text(very_long_text): # Bale allows 4096 characters per message
|
|
215
|
+
await message.answer(chunk)
|
|
216
|
+
|
|
217
|
+
async with ChatActionSender.typing(bot, message.chat.id): # "typing..." while this block runs
|
|
218
|
+
answer = await slow_work()
|
|
219
|
+
|
|
220
|
+
album = MediaGroupBuilder(caption="Trip").add_photo(FSInputFile("a.jpg")).add_photo("file_id")
|
|
221
|
+
await bot.send_media_group(message.chat.id, album.build())
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Bale formats every message as Markdown (`**bold**`, `__italic__`, `[label](url)`); the docs ask for a
|
|
225
|
+
space before the opening and after the closing bold/italic marker.
|
|
226
|
+
|
|
227
|
+
Documented limits are checked before a request leaves your machine (message text 1-4096, callback
|
|
228
|
+
answer text 200, media-group caption 1024, invoice title 32 / description 255 / payload 128 bytes,
|
|
229
|
+
sticker set name/title 64), so you get a clear `ValidationError` instead of an HTTP 400.
|
|
230
|
+
|
|
231
|
+
## Business API (bulk messaging)
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
bot = Bot.business("TOKEN") # https://tapi.bale.ai/business/bot<TOKEN>/...
|
|
235
|
+
await bot.send_message(private_chat_id, "Hello!")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Only `getMe`, `sendMessage`, `forwardMessage`, `sendPhoto`, `sendVideo`, `sendAudio` and
|
|
239
|
+
`sendDocument` exist there, `chat_id` must be a private chat and files are sent by `file_id` only.
|
|
240
|
+
Sending is billed from your Bale business account credit.
|
|
241
|
+
|
|
242
|
+
## Flood control
|
|
243
|
+
|
|
244
|
+
On HTTP 429 the session pauses *every* request of that bot for `retry_after` seconds and retries
|
|
245
|
+
(`AiohttpSession(retry_on_flood=60, max_flood_retries=5)`). `Dispatcher.start_polling` handles at
|
|
246
|
+
most `max_concurrent_updates=50` updates at once.
|
|
247
|
+
|
|
248
|
+
## Examples
|
|
249
|
+
|
|
250
|
+
[`examples/`](https://github.com/rezamardani01/Baleropy/tree/main/examples) holds a minimal bot, a modular bot (routers + middlewares + custom filter),
|
|
251
|
+
an FSM form, colored buttons and a webhook bot. Put your token in the `TOKEN` constant at the top
|
|
252
|
+
of each file. The examples are exercised by the test suite.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
258
|
+
ruff check . && mypy baleropy && pytest
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Notes about Bale
|
|
262
|
+
|
|
263
|
+
- `answerCallbackQuery` is not supported by old clients; `CallbackQuery.answer()` skips them
|
|
264
|
+
(detected by an id starting with `1`, as the Bale docs describe).
|
|
265
|
+
- Messages can only be deleted within 48 hours; the server keeps 2000 updates for 24 hours.
|
|
266
|
+
- Payments use the Bale wallet (`provider_token` for tests: `WALLET-TEST-1111111111111111`).
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import exceptions, methods, types
|
|
2
|
+
from .__meta__ import __version__
|
|
3
|
+
from .client import BUSINESS, PRODUCTION, AiohttpSession, ApiServer, BaseSession, Bot
|
|
4
|
+
from .dispatcher import Dispatcher, Router
|
|
5
|
+
from .filters import F
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BUSINESS",
|
|
9
|
+
"PRODUCTION",
|
|
10
|
+
"AiohttpSession",
|
|
11
|
+
"ApiServer",
|
|
12
|
+
"BaseSession",
|
|
13
|
+
"Bot",
|
|
14
|
+
"F",
|
|
15
|
+
"Dispatcher",
|
|
16
|
+
"Router",
|
|
17
|
+
"__version__",
|
|
18
|
+
"exceptions",
|
|
19
|
+
"methods",
|
|
20
|
+
"types",
|
|
21
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|