Running Django code in consoles, scheduled and always-on tasks with custom management commands

Django is a really useful and powerful web framework; indeed, the web interface and API for PythonAnywhere itself are all Django-based.

One thing that makes it particularly useful is its powerful ORM system, where you can work with the data that your website needs just by using normal Python classes and objects. If you want to set the field to say that the user with username joe is active, you can just write this:

from django.contrib.auth.models import User

# ...

user = User.objects.get(username="joe")
user.is_active = True
user.save()

All of the complexities of connecting to the database -- including finding the connections details in your settings.py file -- and generating appropriate SQL code are hidden from you.

The downside is that this code will only work in the context of a Django site -- that is, if you don't do some extra work, it has to be inside a function that forms part of your website's code. If you were to run a script that used that code directly from a console, it would error out:

Traceback (most recent call last):
  File "/home/gt20240628u1/deactivate.py", line 1, in <module>
    from django.contrib.auth.models import User
  File "/usr/local/lib/python3.10/site-packages/django/contrib/auth/models.py", line 3, in <module>
    from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
  File "/usr/local/lib/python3.10/site-packages/django/contrib/auth/base_user.py", line 49, in <module>
    class AbstractBaseUser(models.Model):
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 127, in __new__
    app_config = apps.get_containing_app_config(module)
  File "/usr/local/lib/python3.10/site-packages/django/apps/registry.py", line 260, in get_containing_app_config
    self.check_apps_ready()
  File "/usr/local/lib/python3.10/site-packages/django/apps/registry.py", line 137, in check_apps_ready
    settings.INSTALLED_APPS
  File "/usr/local/lib/python3.10/site-packages/django/conf/__init__.py", line 87, in __getattr__
    self._setup(name)
  File "/usr/local/lib/python3.10/site-packages/django/conf/__init__.py", line 67, in _setup
    raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment
 variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

This is because all of the setup -- reading the settings, connecting to the database, and so on -- is handled when a Django website is started, so your view functions run in that context, but throwaway scripts need something to do all of that setup for them.

From the error message, you can see that there is a way to write some extra code to do it. But we'd recommend a different way: custom django-admin commands, also known as custom management commands. The Django documentation page we linked to just there is a useful resource, but here are some simpler examples.

Custom management commands for use in consoles and scheduled tasks

Imagine that you were writing a site where people could leave comments, but you wanted all comments that were more than 24 hours old to be deleted once a day -- kind of like Snapchat, or the vanishing messages in WhatsApp. You might have a Comment class like this:

class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)
    text = models.TextField(max_length=1024)

Now, you could put something complicated into your view functions that hid or deleted comments that were older than 24 hours, but a nice simple solution would be to have a scheduled task that kicked off every day at the same time and ran code like this:

Comment.objects.filter(timestamp__lt=datetime.now() - timedelta(days=1)).delete()

If you were to put that (with the appropriate imports) into a script and run it, you'd get the ImproperlyConfigured exception shown above. However, you can get it working with a management command.

Let's say that the code for your Comment class was in ~/mysite/comments/models.py. You could create a new file at ~/mysite/comments/management/commands/delete_old_comments.py, and in there you would put code like this:

from datetime import datetime, timedelta

from django.core.management.base import BaseCommand, CommandError
from comments.models import Comment


class Command(BaseCommand):
    help = "Delete comments more than 24 hours old"

    def handle(self, *args, **options):
        Comment.objects.filter(timestamp__lt=datetime.now() - timedelta(days=1)).delete()

With that, you could easily run it ad-hoc in a Bash console:

cd ~/mysite
./manage.py delete_old_comments

That would start up Django's normal manage.py system, which of course loads in all of the settings and connects to the database for you, then would run the code in side the handle function, doing the work that you need.

You could also call it in a scheduled task, which would achieve the once-per-day deletion that we want. Let's say that your Django site is not using a virtualenv; you could just schedule this to run daily:

~/mysite/manage.py delete_old_comments

If you were using a virtualenv -- say, one that you had created using mkvirtualenv and called myenv -- you would just need to activate it first:

workon myenv; ~/mysite/manage.py delete_old_comments

You could also make it more complex if you wanted; perhaps you might want to add in a command-line option to change the number of days old a comment would have to be before it was deleted. Check out the documentation for details.

Custom management commands in always-on tasks

Custom management commands don't have to just be one-shot utility functions like that. Let's say that you have simple bot that is connected to a messaging service. It's a script that is started, and is expected to keep running forever. Ideally you would like it to be re-started if it ever crashes (or if there's a hardware issue or system maintenance on the machine where it's running, or something like that). The right way to set that up on PythonAnywhere would be to use an always-on task. But you want it to have access to the Django stuff -- the objects that you have stored in your database.

How would you use a custom management command for that? Let's say that what you wanted was to receive messages from users of some messaging service like WhatsApp, Signal or Telegram. Each incoming message would specify the name of a user, and the bot would respond with a message containing that user's most recent comment on your site. It's an always-on task that is connected to the database via Django, and to the messaging service.

Now, real messaging services have complex APIs that would make this example unnecessarily complicated, so we'll imagine that we're using a new service with a simpler one. It's called WhatsSignaGram, and its API allows us to connect to it as a particular bot user using a secret:

bot_connection = whatssignagram.Connection(secret="z4vk14=yzfm*35+%c^=&yc*jcp9y$1al1^(^v-%ahk$j0ssz!k")

...then we can get the next message that was sent to our bot with a function that just blocks if there is currently no message, and unblocks when one comes in:

message = bot_connection.next_message()

The message has a contents field, and a reply method, so we could simply echo stuff back by saying

message.reply(f"You just said {message.contents}")

Let's use that imaginary API to write a custom management command that implements the more complex bot that was described earlier, to reply to each message with the most recent comment left on our site by the user with the username provided in the message:

import whatssignagram

from django.core.management.base import BaseCommand, CommandError
from comments.models import Comment


class Command(BaseCommand):
    help = "WhatsSignaGram bot to provide the most recent comment for a user"

    def handle(self, *args, **options):
        bot_connection = whatssignagram.Connection(secret="z4vk14=yzfm*35+%c^=&yc*jcp9y$1al1^(^v-%ahk$j0ssz!k")

        while True:
            message = bot_connection.next_message()

            username = message.contents
            comments = Comment.objects.filter(user__username=username).order_by("timestamp")
            if comments.count() == 0:
                message.reply(f"No comments found for user {username}")
            else:
                message.reply(f"Last comment for {username} was {comments.last().text}")

You would save that in (say) ~/mysite/comments/management/commands/comment_bot.py and it could be run with manage.py by providing the first parameter comment_bot. So, just as with scheduled tasks, if you were not using a virtualenv; you could just specify this as the command for your always-on task:

~/mysite/manage.py comment_bot

...and if you were using a virtualenv called myenv, you would just need to activate it first:

workon myenv; ~/mysite/manage.py comment_bot

So there you have it! A custom management command that can connect to the fictional WhatsSignaGram library and interact with its users and the Django-managed database, returning the most recent comment for a user when asked, running as an always-on task.

Of course, in a real-world example your code for the bot would be likely to be more complicated. But there's no problem with splitting things out into multiple functions, or indeed across multiple files. For example, the above code could just as easily be this:

import whatssignagram

from django.core.management.base import BaseCommand, CommandError
from comments.models import Comment


def do_bot_loop():
    bot_connection = whatssignagram.Connection(secret="z4vk14=yzfm*35+%c^=&yc*jcp9y$1al1^(^v-%ahk$j0ssz!k")

    while True:
        message = bot_connection.next_message()

        username = message.contents
        comments = Comment.objects.filter(user__username=username).order_by("timestamp")
        if comments.count() == 0:
            message.reply(f"No comments found for user {username}")
        else:
            message.reply(f"Last comment for {username} was {comments.last().text}")


class Command(BaseCommand):
    help = "Comment WhatsSignaGram bot"

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

...and do_bot_loop could easily be split into different functions as the complexity of the bot grew over time, and even moved to a separate module.

Conclusion

Django's custom management commands are an easy way to be able to connect to your Django site's functions and data from scripts that aren't running as part of the website itself -- from one-shot helper scripts that perform a task and then exit, to longer-running scripts that provide ongoing services. We strongly recommend that you use them for any Django code that you have that you would like to run in consoles, scheduled tasks, or always-on tasks.