Django web framework is famous for how easy it is to start developing
application with it. An installation of python and django is all you need to
get busy. Django provides you with all the essential gears and gadgets
including a simple development web server that you can use to run the
application you are developing, while you are developing it.
However, as your project grows more and more complex, you will discover the
limitations of the development server. The first one you are most likely to
face is the inability to serve static files (like site-specific media files).
To be frank, it is not so much an inability but reluctance — there is a
well-documented recepie of how to set up serving of static files, but it
has “do not use in production” written all over it. In fact, any use of the
development server in production environment is discouraged, as the following
disclaimer found in the documentation promptly states:
DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through
security audits or performance tests. (And that’s how it’s gonna stay.
We’re in the business of making Web frameworks, not Web servers, so
improving this server to be able to handle a production environment is
outside the scope of Django.)
Another, more serious limitation is its inherent single-threadedness. If you
are developing any kind of AJAX application that might need to have more than
one connection to the web server at any point in time you will run into
problems — the development server will only start processing the second
request once it’s done with processing the first one. Recently, the topic of
a more advanced, multi-threaded development server has been coming up in the
mailing lists more and more often. All the pleads to improve the default dev.
server were met with “won’t fix” comments pointing out that it is extremely
easy to hookup a custom CherryPy server to the django WSGI request handler
achieving this functionality.
As of version 1.0, Django provides an interface for adding custom management
commands, which can be used to integrate Django with CherryPy.
http://oebfare.com/blog/2008/nov/03/writing-custom-management-command/ is a
good article on how to integrate the two, but for the sake of clarity and
completeness let’s reiterate.
A word of warning: The code presented here is for CherryPy
version 3.1 since it uses WSGIPathInfoDispatcher class. It may be
possible to back-port it to version 3.0, but I make no guaranties.
We will add a “runcherrypy” command, so that the server is started as:
./manage.sh runcherrypy [-h <address>] [-p <port>]
The following file tree will be added to the project directory:
devserver/
|-- __init__.py
`-- management/
|-- __init__.py
`-- commands/
|-- __init__.py
`-- runcherrypy.py
where runcherrypy.py is as follows:
from django.core.management.base import BaseCommand
from django.core.handlers.wsgi import WSGIHandler
from cherrypy.wsgiserver import CherryPyWSGIServer
from optparse import OptionParser, make_option
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=8000 ),
)
def handle( self, *args, **options ):
app = WSGIHandler()
server = CherryPyWSGIServer( (options['host'], options['port']),
app )
try:
server.start()
except KeyboardInterrupt:
server.stop()
def create_parser( self, prog, subcommand ):
"""
Create and return the ``OptionParser`` which will be used to
parse the arguments to this command.
"""
return OptionParser( prog=prog,
usage=self.usage(subcommand),
version=self.get_version(),
option_list=self.option_list,
conflict_handler="resolve" )
Now, add 'devserver' to your settings.py and you’re ready to go! If
you have set up your static file serving in django, as specified earlier,
everything should “just work”. But why go through the network of django
resolvers just to arrive at basic static page serving if you can do it on
the CherryPy side?
A careful observer might notice that django actually serves the admin
interface static content quite happily. That is done by
django.core.servers.basehttp.AdminMediaHandler class. Steven Wilcox
at http://www.devpicayune.com has already taken AdminMediaHandler into
use in his article on django/CherryPy integration. If only one could use
AdminMediaHandler to serve static content outside of the
.../contrib/admin/media/ directory…
Why not? Here is a modified version of AdminMediaHandler applicable for
serving any static content. We will put it to devserver/mediahandler.py:
import os, stat, mimetypes
import django
from django.utils.http import http_date
from django.conf import settings
class MediaHandler( object ):
def __init__( self, media_root ):
self.media_root = media_root
def __call__( self, environ, start_response ):
def done( status, headers, output ):
start_response( status, headers.items() )
return output
path_info = environ['PATH_INFO']
if path_info[0] == '/':
path_info = path_info[1:]
file_path = os.path.join( self.media_root, path_info )
if not os.path.exists( file_path ):
status = '404 NOT FOUND'
headers = {'Content-type': 'text/plain'}
output = ['Page not found: %%s' %% file_path]
return done( status, headers, output )
try:
fp = open( file_path, 'rb' )
except IOError:
status = '401 UNAUTHORIZED'
headers = {'Content-type': 'text/plain'}
output = ['Permission denied: %%s' %% file_path]
return done( status, headers, output )
# This is a very simple implementation of conditional GET with
# the Last-Modified header. It makes media files a bit speedier
# because the files are only read off disk for the first request
# (assuming the browser/client supports conditional GET).
mtime = http_date( os.stat(file_path)[stat.ST_MTIME] )
headers = {'Last-Modified': mtime}
if environ.get('HTTP_IF_MODIFIED_SINCE', None) == mtime:
status = '304 NOT MODIFIED'
output = []
else:
status = '200 OK'
mime_type = mimetypes.guess_type(file_path)[0]
if mime_type:
headers['Content-Type'] = mime_type
output = [fp.read()]
fp.close()
return done( status, headers, output )
CherryPy version 3.1 provides WSGIPathInfoDispatcher class which exposes
an interface to run multiple WSGI applications side-by-side, distinguished
by request path. We will utilize this to set up three applications:
This is how runcherrypy.py needs to be modified:
...
import django.contrib.admin
from cherrypy.wsgiserver import CherryPyWSGIServer, WSGIPathInfoDispatcher
from devserver.mediahandler import *
import os.path
...
def handle( self, *args, **options ):
app = WSGIHandler()
path = { '/': app,
settings.MEDIA_URL: MediaHandler( settings.MEDIA_ROOT ),
settings.ADMIN_MEDIA_PREFIX:
MediaHandler(
os.path.join( django.contrib.admin.__path__[0],
'media' )
)
}
dispatcher = WSGIPathInfoDispatcher( path )
server = CherryPyWSGIServer( (options['host'], options['port']),
dispatcher )
As you notice, we deduce the on-disk path of the admin media based on the
__path__ variable of the imported django.contrib.admin module.
First, a few practical thought on the layout of things. Django insists
that MEDIA_ROOT is an absolute path. If you are hosting your project
completely with CherryPy, you will probably keep all your media files
together with the project. I prefer to keep my media in a media/
directory within my project directory. settings.py features the
following code to resolve MEDIA_ROOT properly:
MEDIA_ROOT = 'media'
import os.path
MEDIA_ROOT = os.path.join(
os.path.normpath( os.path.dirname(__file__) ),
MEDIA_ROOT )
My applications start out from a simple structure:
alpha/
|-- __init__.py
|-- models.py
|-- views.py
|-- urls.py
|-- media/
| `-- ...
`-- templates/
`-- ...
This way, I can create symbolic links in the project media/
directory like:
$ ls -l media/ total 0 lrwxrwxrwx 1 arteme users 13 Feb 27 09:34 alpha -> ../alpha/media/
which will make it easy to reference “alpha” application’s media in its
template files as “{{MEDIA_URL}}alpha/...“
Attached is a simple test application that put all these things together.
It features a project called cherrytest with a single application app.
The application has one view servers one HTML file with a 570×562 image
that is split into 35 100×100 tiles. Happy hacking!
February 27th, 2008 at 18:47
Wow! Very nice work and thanks for the link back.
March 9th, 2009 at 14:32
Thanks for documenting this. Could this setup be appropriate for a moderate-traffic production site?
March 13th, 2009 at 08:21
@greg: I guess that depends on what you mean by “moderate traffic”. I would use this in a production system as a pain-free deployment option. It surely outperforms the default dev server option ;)
At least, according to some comments at http://www.eflorenzano.com/blog/post/hosting-django-site-pure-python/, CherryPy is the tool for the job.
March 25th, 2009 at 19:58
This runs very nicely indeed. It doesn’t respond to changes in the code (views, models) etc, which I guess is to be expected as its designed? for production?
I have servers that are using Apache/mod_wsgi and ’startup.wsgi’ scripts that essentially look like this:
#!/usr/bin/python import os, sys sys.path.append('/export/home/users/djangoprojects/OurSite') sys.path.append('/export/home/users/djangoprojects') os.environ['DJANGO_SETTINGS_MODULE'] = 'OurSite.settings' import django.core.handlers.wsgi application = django.core.handlers.wsgi.WSGIHandler()I don’t know enough about CherryPy? to make comparisons, but could this method replace the startup.wsgi code cleanly? I’m thinking about ease of deployment here.
Cheers, Tone
September 29th, 2009 at 14:42
You’re handing the static files by
output = [fp.read()] fp.close()which causes entire content is loaded to the memory (and not only once
by my observation). This is unacceptable for large files. I found this
to be much more memory & CPU efficient:
class BlockIterator(object): def __init__(self, fp): self.fp = fp def __iter__(self): return self def next(self): chunk = self.fp.read(20*1024) if chunk: return chunk self.fp.close() raise StopIteration ... output = BlockIterator(fp)The iterator spits the content by 20KiB blocks to the network socket and
closes the file at EOF.