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.
Files changed (80) hide show
  1. baleropy-0.2.0/.gitignore +14 -0
  2. baleropy-0.2.0/CHANGELOG.md +25 -0
  3. baleropy-0.2.0/LICENSE +21 -0
  4. baleropy-0.2.0/PKG-INFO +315 -0
  5. baleropy-0.2.0/README.md +270 -0
  6. baleropy-0.2.0/baleropy/__init__.py +21 -0
  7. baleropy-0.2.0/baleropy/__meta__.py +1 -0
  8. baleropy-0.2.0/baleropy/client/__init__.py +3 -0
  9. baleropy-0.2.0/baleropy/client/bot.py +568 -0
  10. baleropy-0.2.0/baleropy/client/server.py +32 -0
  11. baleropy-0.2.0/baleropy/client/session/__init__.py +2 -0
  12. baleropy-0.2.0/baleropy/client/session/aiohttp.py +84 -0
  13. baleropy-0.2.0/baleropy/client/session/base.py +124 -0
  14. baleropy-0.2.0/baleropy/dispatcher/__init__.py +5 -0
  15. baleropy-0.2.0/baleropy/dispatcher/dispatcher.py +230 -0
  16. baleropy-0.2.0/baleropy/dispatcher/event/__init__.py +0 -0
  17. baleropy-0.2.0/baleropy/dispatcher/event/bases.py +32 -0
  18. baleropy-0.2.0/baleropy/dispatcher/event/handler.py +86 -0
  19. baleropy-0.2.0/baleropy/dispatcher/event/observer.py +86 -0
  20. baleropy-0.2.0/baleropy/dispatcher/flags.py +19 -0
  21. baleropy-0.2.0/baleropy/dispatcher/middlewares/__init__.py +2 -0
  22. baleropy-0.2.0/baleropy/dispatcher/middlewares/base.py +33 -0
  23. baleropy-0.2.0/baleropy/dispatcher/middlewares/builtin.py +106 -0
  24. baleropy-0.2.0/baleropy/dispatcher/middlewares/user_context.py +29 -0
  25. baleropy-0.2.0/baleropy/dispatcher/router.py +129 -0
  26. baleropy-0.2.0/baleropy/exceptions.py +88 -0
  27. baleropy-0.2.0/baleropy/filters/__init__.py +7 -0
  28. baleropy-0.2.0/baleropy/filters/base.py +79 -0
  29. baleropy-0.2.0/baleropy/filters/callback_data.py +92 -0
  30. baleropy-0.2.0/baleropy/filters/chat.py +64 -0
  31. baleropy-0.2.0/baleropy/filters/command.py +124 -0
  32. baleropy-0.2.0/baleropy/filters/extra.py +49 -0
  33. baleropy-0.2.0/baleropy/filters/magic.py +155 -0
  34. baleropy-0.2.0/baleropy/filters/state.py +37 -0
  35. baleropy-0.2.0/baleropy/fsm/__init__.py +4 -0
  36. baleropy-0.2.0/baleropy/fsm/context.py +39 -0
  37. baleropy-0.2.0/baleropy/fsm/middleware.py +78 -0
  38. baleropy-0.2.0/baleropy/fsm/state.py +108 -0
  39. baleropy-0.2.0/baleropy/fsm/storage/__init__.py +3 -0
  40. baleropy-0.2.0/baleropy/fsm/storage/base.py +46 -0
  41. baleropy-0.2.0/baleropy/fsm/storage/memory.py +37 -0
  42. baleropy-0.2.0/baleropy/fsm/storage/redis.py +68 -0
  43. baleropy-0.2.0/baleropy/methods/__init__.py +14 -0
  44. baleropy-0.2.0/baleropy/methods/base.py +64 -0
  45. baleropy-0.2.0/baleropy/methods/chats.py +118 -0
  46. baleropy-0.2.0/baleropy/methods/limits.py +32 -0
  47. baleropy-0.2.0/baleropy/methods/media_group.py +59 -0
  48. baleropy-0.2.0/baleropy/methods/messages.py +168 -0
  49. baleropy-0.2.0/baleropy/methods/misc.py +137 -0
  50. baleropy-0.2.0/baleropy/py.typed +0 -0
  51. baleropy-0.2.0/baleropy/types/__init__.py +50 -0
  52. baleropy-0.2.0/baleropy/types/base.py +35 -0
  53. baleropy-0.2.0/baleropy/types/chat.py +127 -0
  54. baleropy-0.2.0/baleropy/types/input_file.py +53 -0
  55. baleropy-0.2.0/baleropy/types/markup.py +73 -0
  56. baleropy-0.2.0/baleropy/types/media.py +104 -0
  57. baleropy-0.2.0/baleropy/types/message.py +321 -0
  58. baleropy-0.2.0/baleropy/types/payments.py +53 -0
  59. baleropy-0.2.0/baleropy/utils/__init__.py +5 -0
  60. baleropy-0.2.0/baleropy/utils/chat_action.py +75 -0
  61. baleropy-0.2.0/baleropy/utils/deep_linking.py +43 -0
  62. baleropy-0.2.0/baleropy/utils/keyboard.py +157 -0
  63. baleropy-0.2.0/baleropy/utils/media_group.py +71 -0
  64. baleropy-0.2.0/baleropy/utils/text.py +47 -0
  65. baleropy-0.2.0/baleropy/webhook/__init__.py +1 -0
  66. baleropy-0.2.0/baleropy/webhook/aiohttp_server.py +93 -0
  67. baleropy-0.2.0/examples/colored_buttons.py +40 -0
  68. baleropy-0.2.0/examples/echo_bot.py +35 -0
  69. baleropy-0.2.0/examples/fsm_form.py +61 -0
  70. baleropy-0.2.0/examples/modular_bot.py +96 -0
  71. baleropy-0.2.0/examples/webhook_bot.py +42 -0
  72. baleropy-0.2.0/pyproject.toml +79 -0
  73. baleropy-0.2.0/tests/test_dispatcher.py +251 -0
  74. baleropy-0.2.0/tests/test_examples.py +130 -0
  75. baleropy-0.2.0/tests/test_filters.py +172 -0
  76. baleropy-0.2.0/tests/test_fsm.py +175 -0
  77. baleropy-0.2.0/tests/test_runtime.py +235 -0
  78. baleropy-0.2.0/tests/test_session.py +280 -0
  79. baleropy-0.2.0/tests/test_shortcuts_and_keyboards.py +189 -0
  80. baleropy-0.2.0/tests/test_utils_and_limits.py +212 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .env
12
+ .idea/
13
+ .vscode/
14
+ .venv*/
@@ -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.
@@ -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
@@ -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"
@@ -0,0 +1,3 @@
1
+ from .bot import Bot
2
+ from .server import BUSINESS, PRODUCTION, ApiServer
3
+ from .session import AiohttpSession, BaseSession