Async and sync¶
PGPCbot has the same features with and without asyncio:
async |
sync (blocking) |
|
|---|---|---|
bot with handlers |
|
|
low-level API client |
|
|
a received message |
|
|
a sent message |
|
|
a button press |
|
|
The sync classes have the same methods, with the same parameters, without await. A test
checks this for every method.
Which one to use¶
Botif your code is already async, if you call other async libraries (databases, HTTP APIs), or if you serve a webhook. It handles many chats with little memory.SyncBotif you prefer plain functions, or if you call libraries that block (requests, a classic database driver). Each handler runs in a thread.Client/AsyncClientfor scripts that only call the API, with no handlers: a cron job that posts a report, a migration script.
SyncBot¶
from pgpcbot import SyncBot
bot = SyncBot()
@bot.command("start")
def start(message):
message.reply("Hello!")
@bot.on_callback_query()
def pressed(query):
query.answer("OK")
bot.run()
Handlers, filters, middlewares and error handlers are plain functions. An
async defhandler makes the bot refuse to start, with an error that names it.Handlers run in a pool of
concurrencythreads (8 by default), namedpgpcbot-handler_0,pgpcbot-handler_1and so on. As withBot, one chat is handled one update at a time, in order.Inside a handler you can call any method of the bot (
bot.send_message(...),message.reply(...)): they block until done.Code shared between handlers must be thread-safe, because handlers of different chats run at the same time.
SyncBot.run() cannot be called from inside a running event loop (for example in a Jupyter
cell): there, use Bot and await bot.start_polling().
SyncBot.stop() is a plain function too: it asks the bot to stop and returns at once, from a
handler or from any other thread; run() returns once the running handlers are done.
Bot inside your own event loop¶
bot.run() creates and owns its event loop. When you already have one, use the async methods:
async def main():
async with Bot() as bot: # starts the bot without polling: sends work
await bot.send_message(7781, "The bot is up")
...
asyncio.run(main())
async def main():
bot = Bot()
polling = asyncio.create_task(bot.start_polling())
await other_work()
await bot.stop()
await polling
In a FastAPI app, use the webhook and lifespan=bot.lifespan (Webhooks).
The low-level clients¶
from pgpcbot import AsyncClient, Client
async with AsyncClient() as api:
me = await api.get_me()
with Client() as api:
me = api.get_me()
They map one method to one operation of the API, with validation, retries, rate limits and
typed results. They don’t encrypt anything: in an encrypted chat you pass armored= yourself.
A bot’s client is bot.api.
Close a client when you are done with it (with does it for you): it holds a pool of
connections. A client must not be shared between event loops, and cannot be pickled.
Testing your handlers¶
process_update runs one update through the whole pipeline (decryption, filters, handlers)
without polling:
await bot.process_update({
"updateId": 1,
"kind": "message",
"message": {
"id": 10, "chat": {"id": 55, "kind": "private"},
"from": {"id": 55, "username": "anna"},
"date": 1791462000000, "kind": "user", "text": "/start",
},
})
SyncBot.process_update does the same without await. It still calls the API for the bot’s
own details and for any reply, so in tests give the bot a fake transport
(Bot(token=..., transport=...), an httpx2 transport) or a test server.