Search code examples
pythondjangoasynchronousasync-awaitpython-telegram-bot

How do I make database calls asynchronous inside Telegram bot?


I have a Django app that runs a Telegram chatbot script as a command.

I start the Django app with python manage.py runserver. I start the telegram client with python manage.py bot.

I want to list the entries from the Animal table within the async method that is called when a user types "/animals" in the telegram chat. My code works if I use a hard-coded list or dictionary as a data source. However, I'm not able to get the ORM call to work in async mode.

File structure:

|Accounts---------------------
|------| models.py------------
|Main-------------------------
|------| Management-----------
|---------------| Commands----
|-----------------------bot.py

Animal model:

class Animal(models.Model):
    id = models.AutoField(primary_key=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    name = models.CharField(max_length=255)

I removed a lot from the file, leaving only the relevant bits.
bot.py

# removed unrelated imports
from asgiref.sync import sync_to_async
from accounts.models import Animal

class Command(BaseCommand):
    help = "Starts the telegram bot."
    # assume that the token is correct
    TOKEN = "abc123"

    def handle(self, *args, **options):

        async def get_animals():
            await Animal.objects.all()

        async def animals_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:

            async_db_results = get_animals()

            message = ""
            counter = 0
            for animal in async_db_results:
                message += animal.name + "\n"
                counter += 1

            await update.message.reply_text(message)

        application = Application.builder().token(TOKEN).build()
        application.add_handler(CommandHandler("animals", animals_command))
        application.run_polling(allowed_updates=Update.ALL_TYPES)

Error message for this code:
TypeError: 'coroutine' object is not iterable

Initially I had Animal.objects.all() in place of async_db_results. The ORM call is not async, so I got this error message:

django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

This is a prototype app, I know I should not be using runserver. And I should also use a webhook instead of long-polling, but I don't think these issues are related to my trouble with async.
The next thing I'm going to try is using asyncio but I have spent a lot of time already, I figured I would ask the question.

I have looked at these resources (and many others):
docs.djangoproject.com: asgiref.sync.sync_to_async
stackoverflow: Django: SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async
stackoverflow: Sync to Async Django ORM queryset foreign key property


Solution

  • I figured it out. Correct use of sync_to_async and await did it.

    @sync_to_async
    def get_animals():
        return list(Animal.objects.all())
    
    async def animals_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """Send a message when the command /animal is issued."""
        async_db_results = await get_animals()
    
        message = "Animal list:"
        for h in async_db_results:
            message += "{}\n".format(h.name)
    
        await update.message.reply_text(message)