oebfare

Writing a custom management command

One of the neat things you can do in Django 1.0 is write custom management commands. Django gives you an API that you can use to easily write code that you can execute on the command line. We are familiar with some of the built-in ones such as syncdb and runserver. This post will cover how to create our own.

Background

Today I was tasked with the need to deploy an application in the wild on my local intranet. It was a stupid simple Django application that basically used the admin app to browse some data in a MySQL database. All this will eventually be in a PostgreSQL database running on a nice Django project. However, in the mean time I needed something quick and working.

I could use runserver and daemonize it. Ideally I wanted something that could handle multiple requests at time because there were cases when a couple users would be using this heavily at the same time. I turned to the trusty WSGI server that CherryPy provides. It is multi-threaded and super simple to setup.

Getting started

The reason why I want to blog about this is because the Django documentation on management commands is pretty sparse. It does show you the basics of where the command must go to be recongized. Inside your app you will want to have a directory sturcture similiar to this:

wsgi/
    __init__.py
    models.py
    management/
        __init__.py
        commands/
            ___init__.py
            runwsgi.py

Above is what my app looks like for the command we are going to write. As you can see there is wsgi/management/commands/runwsgi.py. This is where your command code will live.

Django provides you with some base classes you can dervive your Command from. All commands ultimately derive from django.core.management.base.BaseCommand and in this case ours will be too. However, if you have a specific task there may be a helper base class to do some work for you.

  • django.core.management.base.BaseCommand
  • django.core.management.base.AppCommand
  • django.core.management.base.LabelCommand
  • django.core.management.base.NoArgsCommand

Most of the command that I write extend from either BaseCommand or NoArgsCommand.

Writing the management command

Now that we know where to put our command in our app and what the command can extend from lets actually see something in action. Here is some code to get you started on something:

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **options):
        # do something interesting here

The code above in the case of the layout shown before lives in runwsgi.py. The name of the file is significant by the fact it is used in how you call your management command.

Note

Your app must be in INSTALLED_APPS for Django to see the management command.

This command can be executed by running in the same directory as manage.py:

./manage.py runwsgi

Seriously, do something interesting

Ok, lets cut the crap and get down to something useful. We are writing code for the runwsgi management command. Lets see some working code here:

from django.core.management.base import BaseCommand
from django.core.handlers.wsgi import WSGIHandler

try:
    from cherrypy.wsgiserver import CherryPyWSGIServer
except ImportError:
    from wsgiserver import CherryPyWSGIServer

class Command(BaseCommand):
    def handle(self, *args, **options):
        server = CherryPyWSGIServer(("127.0.0.1", 9001), WSGIHandler())
        try:
            server.start()
        except KeyboardInterrupt:
            server.stop()

Oops. I did say CherryPy's WSGI server was simple right? The above code hooks up Django right into the CherryPy multi-threaded WSGI server and runs it. One thing that is interesting, is based on that code, we can see so many places where we can let the user override values and behavior of the process.

Accepting and using user options

One thing that I find the best part of how Django's management command are implemented is it provides you with a nice way of specifying options. These options will come in through the command line that can adjust values based on how the user wants the process to behave. Based on the code shown just earlier we can see that we might want to the user to provide the host, port and specify the ability to daemonize itself. Lets go ahead and add these options that the user can specify:

from django.core.management.base import BaseCommand
from django.core.handlers.wsgi import WSGIHandler

from optparse import OptionParser, make_option

try:
    from cherrypy.wsgiserver import CherryPyWSGIServer
except ImportError:
    from wsgiserver import CherryPyWSGIServer

class Command(BaseCommand):
    option_list = BaseCommand.option_list + (
        make_option("-h", "--host", dest="host", default="127.0.0.1"),
        make_option("-p", "--port", dest="port", default=9001),
        make_option("-d", "--daemon", dest="daemonize", action="store_true"),
    )

    def handle(self, *args, **options):
        server = CherryPyWSGIServer((options["host"], options["port"]), WSGIHandler())
        try:
            server.start()
        except KeyboardInterrupt:
            server.stop()

Django provides a quick a simple way to use Python's optparse module to define how you want your command to accept options. We have added options for host, port and daemonize. I have hooked up the host and port when we create the CherryPyWSGIServer. I will touch on daemonize later.

One thing I want to draw some attension to is specifically the -h option. optparse defines -h for help. If you try to run the above code you will get an exception indicating there is a clash. To fix this you need take control of how Django create the option parser:

from django.core.management.base import BaseCommand
from optparse import OptionParser, make_option

class Command(BaseCommand):
    # ...

    def create_parser(self, prog_name, subcommand):
        """
        Create and return the ``OptionParser`` which will be used to
        parse the arguments to this command.
        """
        return OptionParser(prog=prog_name, usage=self.usage(subcommand),
            version = self.get_version(),
            option_list = self.option_list,
            conflict_handler = "resolve")

    # ...

The part of significance above is that we are passing in conflict_handler to OptionParser. This lets the parser handle the collision intelligently. You can read about this on Python documentation for conflicts between options.

Daemonizing the process

One small thing I want to touch on is how you can daemonize this Python process. Right now it will be attached to the shell and wont be very useful for long periods of time. I have spent some time with Twisted so I came across this neat snippet of code:

def daemonize():
    """
    Detach from the terminal and continue as a daemon.
    """
    # swiped from twisted/scripts/twistd.py
    # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16
    if os.fork():   # launch child and...
        os._exit(0) # kill off parent
    os.setsid()
    if os.fork():   # launch child and...
        os._exit(0) # kill off parent again.
    os.umask(077)
    null = os.open("/dev/null", os.O_RDWR)
    for i in range(3):
        try:
            os.dup2(null, i)
        except OSError, e:
            if e.errno != errno.EBADF:
                raise
    os.close(null)

Just put that in your runwsgi.py file and call it when you need it. You can do this conditionally based on the option being passed it as well. I will leave that bit as an exercise to you.

Coming to an end

I have pretty much covered everything I did to write this management command. It was really simple and we got some usable code out of it. I did leave you with an exercise if you did want to implement this command, but I like pulling code from others just as much as you probably do it is available on my blog's source code. You can find it here. Please ask questions or provide better examples if you so wish. I would love to hear your opinions and see your code.


Comments

You have a small typo repeating through your code examples handle method; you bind the CherrPy Server instance to the local variable 'server' but reference the instance variable self.server below it. Otherwise, great tutorial. Thanks

Posted by Andrew on Nov 3, 2008 at 3:56 PM

Another quick note, have you seen django.utils.daemonize (http://code.djangoproject.com/browser... )?

Posted by Andrew on Nov 3, 2008 at 4:01 PM

Andrew,

Good catch. I have fixed it.

Posted by Brian Rosner on Nov 3, 2008 at 4:02 PM

Andrew,

Yes, I have seen the one built in Django. It actually never occurred to me using it here. I normally work in code outside of Django so requiring Django for that is silly. Here it would be appropriate to include it. Also of course to keep mine simple it is Unix only and the one Django provides is both Unix and Windows. It seems to a viable option for those Windows users. Thanks for mentioning it.

Posted by Brian Rosner on Nov 3, 2008 at 4:07 PM

thanks for fixing the rss!

Posted by Foo on Nov 3, 2008 at 11:35 PM

tucvvxukkzzyzxuqwell, hi admin adn people nice forum indeed. how's life? hope it's introduce branch ;)

Posted by Sningulfins on Dec 27, 2008 at 10:46 AM

strict space for buddy. craving to get more from your side :)

Posted by Penisa on Dec 29, 2008 at 12:16 AM

Add Your Comment






Entry Details

Published: Nov 3, 2008 at 3:42 PM

© 2007 - 2008 Brian Rosner