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.BaseCommanddjango.core.management.base.AppCommanddjango.core.management.base.LabelCommanddjango.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.
