BotRunner manages the lifecycle of a bot: start, run, stop.
from bot_sdk import BotRunner
runner = BotRunner(
bot_factory: Callable[[AsyncClient], BaseBot],
*,
event_types: list[str] | None = None,
narrow: list[list[str]] | None = None,
client_kwargs: dict[str, Any] | None = None,
)
BaseBot when given an AsyncClient.["message"]; configurable via event_types in bots.yaml).AsyncClient.post_init, open session, on_start.on_stop, cancel long-poll, close client.async with BotRunner(lambda c: MyBot(c)) as runner:
await runner.run_forever()
from bot_sdk import run_bot
run_bot(
bot_cls: type[BaseBot],
*,
event_types: list[str] | None = None,
narrow: list[list[str]] | None = None,
client_kwargs: dict[str, Any] | None = None,
)
run_bot(MyBot).start(), run_forever(), stop() with try/finally.asyncio.gather their run_forever() calls.zuliprc.Examples:
# Single stream
runner = BotRunner(lambda c: MyBot(c), narrow=[["stream", "general"]])
# Private messages only
runner = BotRunner(lambda c: MyBot(c), narrow=[["is", "private"]])
# Stream + topic
runner = BotRunner(
lambda c: MyBot(c),
narrow=[["stream", "general"], ["topic", "bot-testing"]]
)
runner = BotRunner(
lambda c: MyBot(c),
client_kwargs={
"config_file": "~/.zuliprc",
"verbose": True,
"retry_on_errors": True,
},
)
# Or pass credentials directly
runner = BotRunner(
lambda c: MyBot(c),
client_kwargs={
"email": "bot@example.com",
"api_key": "your-api-key",
"site": "https://zulip.example.com",
},
)
import asyncio
import signal
from bot_sdk import BotRunner, BaseBot, Message
from loguru import logger
class ProductionBot(BaseBot):
async def on_start(self):
logger.info("Bot started")
async def on_stop(self):
logger.info("Bot stopping")
async def on_message(self, message: Message):
try:
await self.send_reply(message, "Hello!")
except Exception as e:
logger.error(f"Error: {e}")
async def main():
runner = BotRunner(lambda c: ProductionBot(c))
async with runner:
await runner.run_forever()
if __name__ == "__main__":
asyncio.run(main())