Dec 29, 2009

flup vs uWSGI with cherokee

Cherokee is one of this many web servers that supports both deployment strategies. Since I have recently blogs about uwsgi
  Several people asked me how do they compare performance wise. Up to now my answer was I don't know and I don't really care because this is not the most critical aspect in my opinion.

However since this question keep comming I have decided to give it a more accurate answer. This blog post takes you on how to setup cherokee with both alternatives and compare their performance. The guinea pig application I have chosen to do this comparison is django-cms because I think it represents fairly well the overhead introduced by the dynamic generation of the page.

Note : in this example django-cms will not be mounted under "/" this will cause all sort of issues if you try to do this but none of them impact the particular page that we try to access.

One of the nice Cherokee's feature is its documentation, here it is the 2 pages related to this blog post :
flup [1]
uswgi [2]

In this blog post I will assume that django-cms is installed, its example project properly configured and that you have added a page to test test against it. I will aslo assume that cherokee, flup and uwsgi are setup.
During this test I have used uwsgi changeset: 145:db356717823c.

Cherokee provides 2 wizard that enables you to get running very fast. First thing first you will need to start cherokee admin :


sudo cherokee-admin -u

media admin
Add a rule to serve the "/media/admin" in my case these files are in "/opt/www/django-cms_tutorial/ve/lib/python2.6/site-packages/django/contrib/admin/media"



media cms
Add a rule to serve the "/media/cms" in my case these files are in "/opt/www/django-cms_tutorial/ve/src/django-cms/cms/media/cms"



Flup


In this case you will need to use the wizard called "Plateforms > django". This wizard takes the web directory and the project directory. In our case :

web directory : /flup
project directory : /opt/www/django-cms_tutorial/ve/src/django-cms/example






Then we need to modify the interpreter command from the "django 1" source because we used virtualenv instead of the global python.
So we need to change it from :


python /opt/www/django-cms_tutorial/ve/src/django-cms/example/manage.py runfcgi protocol=scgi host=127.0.0.1 port=37134
to:


/opt/www/django-cms_tutorial/ve/src/django-cms/example/start_fcgi.sh


Then you need to create this file called start_fcgi.sh

example$ cat > start_fcgi.sh << EOF
> source /opt/www/django-cms_tutorial/ve/bin/activate;
> python /opt/www/django-cms_tutorial/ve/src/django-cms/example/manage.py runfcgi protocol=scgi host=127.0.0.1 port=37134
> EOF


chmod +x start_fcgi.sh

Restarts cherokee and direct your browser to "/flup/".

uWSGI


In order to setup the uwsgi server we are going to use the cherokee's wizard and then modify the result to adapt it to our particular use case.

uwsgi wizard takes only one argument the path to the uwsgi configuration file :

/opt/www/django-cms_tutorial/ve/src/django-cms/example/example_uwsgi.py





We are going to edit the interpreter command to adapt it a bit to our use case :


/usr/local/bin/uwsgi -s 127.0.0.1:42597 -t 10 -M -p 1 -C -w example.example_uwsgi -H /opt/www/django-cms_tutorial/ve/src/django-cms

to :

/usr/local/bin/uwsgi -s 127.0.0.1:46075 -t 10 -M -p 10 -C -w example.example_uwsgi -H /opt/www/django-cms_tutorial/ve/

The file called example_uwsgi looks like this :


import os
import django.core.handlers.wsgi
# Set the django settings and define the wsgi app
os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings'
application = django.core.handlers.wsgi.WSGIHandler()
# Mount the application to the url
applications = {'/uwsgi':application, }

Comparison: uwsgi vs flup

flup


Resource usage for concurency equal to 50 :






(ve)yml@yml-laptop:django-cms_tutorial$ ab -n 1000 -c 50 http://192.168.1.18:8080/flup/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.1.18 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software: Cherokee/0.99.37
Server Hostname: 192.168.1.18
Server Port: 8080

Document Path: /flup/
Document Length: 3485 bytes

Concurrency Level: 50
Time taken for tests: 82.385 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 3688000 bytes
HTML transferred: 3485000 bytes
Requests per second: 12.14 [#/sec] (mean)
Time per request: 4119.256 [ms] (mean)
Time per request: 82.385 [ms] (mean, across all concurrent requests)
Transfer rate: 43.72 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 3
Processing: 750 4042 1424.0 3791 11665
Waiting: 750 4041 1424.0 3791 11665
Total: 753 4042 1424.2 3791 11666

Percentage of the requests served within a certain time (ms)
50% 3791
66% 4098
75% 4350
80% 4509
90% 4972
95% 5566
98% 10388
99% 10974
100% 11666 (longest request)

(ve)yml@yml-laptop:django-cms_tutorial$ ab -n 1000 -c 100 http://192.168.1.18:8080/flup/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.1.18 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software: Cherokee/0.99.37
Server Hostname: 192.168.1.18
Server Port: 8080

Document Path: /flup/
Document Length: 3485 bytes

Concurrency Level: 100
Time taken for tests: 82.614 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 3688000 bytes
HTML transferred: 3485000 bytes
Requests per second: 12.10 [#/sec] (mean)
Time per request: 8261.424 [ms] (mean)
Time per request: 82.614 [ms] (mean, across all concurrent requests)
Transfer rate: 43.59 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.8 0 3
Processing: 1209 7920 1519.3 7694 14118
Waiting: 1209 7920 1518.1 7694 14118
Total: 1213 7920 1519.5 7694 14120

Percentage of the requests served within a certain time (ms)
50% 7694
66% 8161
75% 8405
80% 8574
90% 9215
95% 10456
98% 13048
99% 13983
100% 14120 (longest request)


Server Software: Cherokee/0.99.37
Server Hostname: 192.168.1.18
Server Port: 8080

Document Path: /flup/
Document Length: 3485 bytes

Concurrency Level: 100
Time taken for tests: 81.737 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 3688000 bytes
HTML transferred: 3485000 bytes
Requests per second: 12.23 [#/sec] (mean)
Time per request: 8173.722 [ms] (mean)
Time per request: 81.737 [ms] (mean, across all concurrent requests)
Transfer rate: 44.06 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 1.2 0 5
Processing: 830 7843 1455.0 7574 14385
Waiting: 830 7843 1455.0 7574 14385
Total: 835 7843 1455.3 7574 14388

Percentage of the requests served within a certain time (ms)
50% 7574
66% 7912
75% 8137
80% 8335
90% 8893
95% 10624
98% 13435
99% 13670
100% 14388 (longest request)

Resource usage for concurency equal to 100 :




uwsgi


uWSGI resource usage for concurrency equal to 50 :






(ve)yml@yml-laptop:django-cms_tutorial$ ab -n 1000 -c 50 http://192.168.1.18:8080/uwsgi/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.1.18 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software: Cherokee/0.99.37
Server Hostname: 192.168.1.18
Server Port: 8080

Document Path: /uwsgi/
Document Length: 3494 bytes

Concurrency Level: 50
Time taken for tests: 84.013 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 3697000 bytes
HTML transferred: 3494000 bytes
Requests per second: 11.90 [#/sec] (mean)
Time per request: 4200.629 [ms] (mean)
Time per request: 84.013 [ms] (mean, across all concurrent requests)
Transfer rate: 42.97 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 2
Processing: 682 4115 472.7 4189 4707
Waiting: 682 4115 472.7 4189 4707
Total: 684 4115 472.5 4189 4707

Percentage of the requests served within a certain time (ms)
50% 4189
66% 4246
75% 4286
80% 4316
90% 4378
95% 4440
98% 4515
99% 4544
100% 4707 (longest request)
(ve)yml@yml-laptop:django-cms_tutorial$ ab -n 1000 -c 100 http://192.168.1.18:8080/uwsgi/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.1.18 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
apr_socket_recv: Connection reset by peer (104)
Total of 839 requests completed

This reveals the conservative nature of uwsgi. I had a discussion about this with Roberto De Ioris. He mades a detailled answer detailled answer explaining the situation. So the bottom line is that we will need to modify our configuration and increase the socket timeout "-z" and the socket listen queue "-l"


/usr/local/bin/uwsgi -i -l 120 -z 60 -p 10 -M -s 127.0.0.1:46075 -w example.example_uwsgi -H /opt/www/django-cms_tutorial/ve/

(ve)yml@yml-laptop:django-cms_tutorial$ ab -n 1000 -c 100 http://192.168.1.18:8080/uwsgi/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.1.18 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software: Cherokee/0.99.37
Server Hostname: 192.168.1.18
Server Port: 8080

Document Path: /uwsgi/
Document Length: 3494 bytes

Concurrency Level: 100
Time taken for tests: 86.246 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 3697000 bytes
HTML transferred: 3494000 bytes
Requests per second: 11.59 [#/sec] (mean)
Time per request: 8624.594 [ms] (mean)
Time per request: 86.246 [ms] (mean, across all concurrent requests)
Transfer rate: 41.86 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 1.7 0 7
Processing: 762 8248 1460.7 8509 9706
Waiting: 762 8248 1460.7 8509 9706
Total: 766 8248 1459.5 8509 9706

Percentage of the requests served within a certain time (ms)
50% 8509
66% 8669
75% 8852
80% 8998
90% 9182
95% 9360
98% 9474
99% 9566
100% 9706 (longest request)

uWSGI resource usage for concurrency equal to 100 :





Conclusion

flup is slightly faster than uWSGI at this point but this has to be put into perspective and you need to take into consideration the features that come with uWSGI, an exhaustive list can be found hereuWSGI is unable to complete the ab test with the following argument -n 1000 -c 100 with its default settings you will need to adjust the timeout socket and socket listen queue. However it is interesting to note that the memory footprint for uwsgi is lower by an order of magnitude and that my laptop remains responsive during the ab test using uWSGI where it was almost taken down by same test with flup.

[1] http://www.cherokee-project.com/doc/cookbook_django.html
[2] http://www.cherokee-project.com/doc/cookbook_uwsgi.html
[3] http://projects.unbit.it/uwsgi/

Dec 28, 2009

Add bpython to django's shell management command

Like many of us I am spending a lot of time in the REPL loop to develop django code and this bug in readline cost me an extra backspace each time  I hit "tab" in ipython.

This blog post capture my attention so I have decided to give bpython a try and so far I have been impressed by what I have seen. Here it is a management command that adds the support for bpython to django's shell management command.


import os
from django.core.management.base import NoArgsCommand
from optparse import make_option
from IPython.Shell import IPShell

def start_plain_shell():
    import code
    # Set up a dictionary to serve as the environment for the shell, so
    # that tab completion works on objects that are imported at runtime.
    # See ticket 5082.
    imported_objects = {}
    try: # Try activating rlcompleter, because it's handy.
        import readline
    except ImportError:
        pass
    else:
        # We don't have to wrap the following import in a 'try', because
        # we already know 'readline' was imported successfully.
        import rlcompleter
        readline.set_completer(rlcompleter.Completer(imported_objects).complete)
        readline.parse_and_bind("tab:complete")

    # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system
    # conventions and get $PYTHONSTARTUP first then import user.
    if not use_plain: 
        pythonrc = os.environ.get("PYTHONSTARTUP") 
        if pythonrc and os.path.isfile(pythonrc): 
            try: 
                execfile(pythonrc) 
            except NameError: 
                pass
        # This will import .pythonrc.py as a side-effect
        import user
    code.interact(local=imported_objects)

def start_ipython_shell():
    import IPython
    # Explicitly pass an empty list as arguments, because otherwise IPython
    # would use sys.argv from this script.
    shell = IPython.Shell.IPShell(argv=[])
    shell.mainloop()
    
def start_bpython_shell():
    from bpython import cli
    cli.main(args=[])
    

class Command(NoArgsCommand):
    option_list = NoArgsCommand.option_list + (
        make_option('--plain', action='store_true', dest='plain',
            help='Tells Django to use plain Python, not IPython.'),
        make_option('--ipython', action='store_true', dest='ipython',
            help='Tells Django to use ipython.'),
        make_option('--bpython', action='store_true', dest='bpython',
            help='Tells Django to use bpython.'),
    )
    help = "Runs a Python interactive interpreter. Tries to use IPython, if it's available."

    requires_model_validation = False

    def handle_noargs(self, **options):
        # XXX: (Temporary) workaround for ticket #1796: force early loading of all
        # models from installed apps.
        from django.db.models.loading import get_models
        loaded_models = get_models()

        use_plain = options.get('plain', False)
        use_ipython = options.get('ipython', False)
        use_bpython = options.get('bpython', False)
        
        try:
            if use_plain:
                # Don't bother loading IPython, because the user wants plain Python.
                raise ImportError
            elif use_ipython:
                start_ipython_shell()
            elif use_bpython:
                start_bpython_shell()
            else:
                # backward compatible behavior.
                start_ipython_shell()

        except ImportError:
            # fallback to plain shell if we encounter an ImportError
            start_plain_shell()

Is there an exiting debugger that use bpython ? I am looking for something equivalent to ipdb but for bpython. I am looking for something that could replace :


import ipdb; ipdb.set_trace()

Dec 14, 2009

Paginated feed for cmsplugin_feed

In my previous post [1] I wrote about my experience of writing a new plugins for django-cms [2]. This plugin gives you  the capability to add a feed to a django-cms' Page.

I ended my post by asking about the best practice used to paginate the content of a plugin. Since I didn't get flooded by the answers I assumed that this shouldn't be different than doing it on a django app. Here it is what the fine django documentation says about this topic.

So I have decided to implement this approach, the key point here is to understand that django-cms' plugin has access to the context :


class FeedPlugin(CMSPluginBase):
    [...]
    def render(self, context, instance, placeholder):
        feed = get_cached_feed(instance)
        if instance.paginate_by:
            is_paginated =True
            request = context['request']
        [...]

This capability lets your plugin react to the GET and POST parameters. Once I have understood this the only thing that I had to do is to use the django's Paginator on the list of feed entries.

As you can see in the code below there is nothing specific to django-cms there.

            is_paginated =True
            request = context['request']
            feed_page_param = "feed_%s_page" %str(instance.id)

            feed_paginator = Paginator(feed["entries"], instance.paginate_by) 
            # Make sure page request is an int. If not, deliver first page.
            try:
                page = int(request.GET.get(feed_page_param, '1'))
            except ValueError:
                page = 1
            # If page request (9999) is out of range, deliver last page of results.
            try:
                entries = feed_paginator.page(page)
            except (EmptyPage, InvalidPage):
                entries = feed_paginator.page(paginator.num_pages)

The complete implementation of this feature is available in the bitbucket repository of cmsplugin_feed.

I would be glad to hear from you if this implementation could be enhanced.

[1] http://yml-blog.blogspot.com/2009/12/feed-extension-for-django-cms.html
[2] http://www.django-cms.org/
[3] http://docs.djangoproject.com/en/dev/topics/pagination/
[4] http://bitbucket.org/yml/cmsplugin-feed/changeset/7d0644c7668f/

Dec 12, 2009

feed extension for django-cms

I have been lately looking at the open source CMS alternatives based on django. After some times investigating I have decided to give django-cms [1] a try.

django-cms provides an API to write plugin it is well documented [2] and you can also look the implementation of plugins listed there [3]. I have decided to experiment with it by writing a plugin that displays the content of a feed into a page. The source code is available on my bitbucket account in a project called cmsplugin-feed. The development of this plugin has been relatively straight forward and once you get to know the conventions defined to implement an extension. You feel like you are writing python code for a django application. The CMS part does not stand on your way at least it didn't in my experimentation.

Here it is 2 screenshots showing this plugin in action :
In order to add a feed you need give a name and an URL to your feed



Then the plugin displays the field on a page.




I would be interested to read from someone the "best practice" to paginate the content of a plugin. Ideally I would like to add a parameter for every feed indicating how many items I want per page. Then I would like to paginate the feed in the placeholder of every page and this will allow the user to navigate between the pages.

[1] http://www.django-cms.org
[2] http://www.django-cms.org/en/documentation/2.0/custom_plugins/
[3] http://www.django-cms.org/en/extensions/
[4] http://bitbucket.org/yml/cmsplugin-feed/

Nov 30, 2009

uWSGI reaches the 0.9.3 Milestone

uWSGI project [1] reaches the 0.9.3 milestone. You can review the complete announcement [2] on the mailing list  and download the code here [3]

This new release brings some new exiting features :

- Nginx 0.7.x module
- configuration via python module
- support (non-standard) for Python 3.x
- Twisted client resource adapter
- graceful restart of worker processes and hot-plug substitution/upgrade
of the uWSGI server
- shared memory area to share data between workers/processes
- Tomcat handler
- support for virtualenv

In this post [4] I have covered the usage of both the "configuration via a python module" and the "virtualenv support".

uWSGI allows you to run your favorite wsgi application on top of prefered web server (apache, cherokee or nginx).

[1] http://projects.unbit.it/uwsgi/
[2] http://lists.unbit.it/pipermail/uwsgi/2009-November/000020.html
[3] http://projects.unbit.it/downloads/uwsgi-0.9.3.tar.gz
[4] http://yml-blog.blogspot.com/2009/11/setting-up-django-project-with-cherokee.html

Nov 17, 2009

Continous sphinx build

I have always found the cycle : Edit the documentation source => build the sphinx based documentation [1] => open a browser pointing the updated documentation and re iterate until your are satisfied a bit painful.

Today I have finally found an automated version of this workflow that
gave me one of this nice WAHOUUUU feeling that happens after you remove this little stone from your shoe.

The idea is to continuously build the documentation using watch [2]

watch -n 2 make htlm
This will update the generated documentation every 2 seconds. This is nice in itself and it leads me to discover that Epiphany automatically reload the page when it changes.

[1] http://sphinx.pocoo.org/
[2] http://linux.die.net/man/1/watch
[3] http://projects.gnome.org/epiphany/

Nov 12, 2009

Setting up a django project with Cherokee, uWSGI and vitualenv (continued)

In my previous post [1] I have started to talk about this stack but I have avoided to mention that the django project was indeed encapsulated into a virtualenv. This brought a couple small quirks and questions to the surface. So I spent some times investigating and exchanging with the community behind uWSGI and cherokee in order to solve them. The enhancements I discuss here require that you use the tip of uWSGI [2] because some of them are new capability added in the past days or small improvements to my recipe of using these products.

Before I start to dive into the enhancements let me sum up what we had to do in order to serve our WSGI project, in our case a django project.

1> I had to add 2 configurations files : uwsgi.conf, django_wsgi.py
The first one is an XML file where you define the PYTHONPATH and the mount point for your application, the second one is a python module where you define your WSGI configuration.

2> I had to use the cherokee's wizard to create the appropriate configuration to serve my django app using uWSGI. This step add an "Information Sources" with the following settings in the interpreter section :

/usr/local/bin/uwsgi -s /tmp/cherokee-source1.sock -C -x /opt/webapps/example-ve/src/project/uwsgi.conf
The first thing that can be improved in this recipe is that you can reduce the configuration to only one python module. This allows you to avoid the uwsgi.conf file. uWSGI comes with an option that allows you to directly specify a python module.

-w     name of wsgi config module (no ROCK_SOLID)
This would give us the following command :

uwsgi -s /tmp/cherokee-source1.sock -C -w project.django_wsgi
The django_wsgi file is very simple and looks like this :

import os
import django.core.handlers.wsgi

# Set the django settings and define the wsgi app
os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'
application = django.core.handlers.wsgi.WSGIHandler()

# Mount the application to the url
applications = {'/':'application', }
This is all good and simple but it only works if "project" is in the PYHTONPATH. I handle all my projects in a dedicated virtualenv. uWSGI handle this gracefully now, there is a new option that allows you to define your Python Home.

-H     set python home/virtualenv
Here it the improved settings for the interpreter section of the information sources :

uwsgi -s /tmp/cherokee-source1.sock -C -w project.django_wsgi -H /opt/webapps/example-ve

I would be glad to hear from you and very happy if you give this setup a spin on your django project and let me know how it performs compare to your actual setup.

[1] http://yml-blog.blogspot.com/2009/11/how-to-set-up-cherokee-and-uwsgi.html
[2] http://projects.unbit.it/uwsgi/changeset/87%3Ae78ef65c833e

Nov 3, 2009

How to set up cherokee and uWSGI

A couple of post ago I have "blogged" about cherokee which is a light weight web server, fast and extremely simple to configure thanks to its great admin interface.

The most common configuration to use cherokee with django is described here and to put is in a nutshell it was based on flup using SCGI or FASTCGI protocol.

However a couple of weeks ago a new contender pop up on my radar. It is called uWSGI and it might become in the near future very popular to power django application. You can find more information about it on on the pages below :
* example [2]
* Documentation [3]

From an architecture stand point it might provide us python web developer an interesting piece of software that could operate with virtually any webserver. There is already an apache module and cherokee handler. The documentation mentions that the core developers have started to develop an nginx adaptor.


Software prerequisites

cherokee 0.99.26
uWSGI (changeset: 85:61dcfd718023)

I have installed cherokee using the PPA on launchpad [1] and uWSGI directly from the mercurial repository.

hg clone http://projects.unbit.it/hg/uwsgi

Installation

Installation of both product is relatively straight forward and well documented. I will not detail it more here but rather point you to cherokee [4] and uWSGI website [5].
Here it is how you would compile uWSGI on Ubuntu 9.04

make -f Makefile.Linux.Py26

Note: The compilation will create a binairy called uwsgi26. It is important to add it into your PATH under the name of uwsgi. If you don't do this cherokee's wizard  will complain about not being able to find uWSGI server. I have created a symbolic link but you could also copy it into /usr/local/bin.

Configuration

The amazing admin interface of cherokee make it dead simple once you know what need to be done. In the latest version of cherokee a wizzard has been added to make the configuration even simpler.

First thing first you need to create 2 configurations file into your django project directory called respectively : django_wsgi.py and uwsgi.conf

------ django_wsgi.py ------------------

  import os import django.core.handlers.wsgi  
  os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings' 
  application = django.core.handlers.wsgi.WSGIHandler() 
 

------ uwsgi.conf --------------------

<uwsgi>

    <pythonpath>/home/yml/workdir/webdev/fabric_factory/20091023/fabric_factory/ve/lib/python2.6/site-packages/</pythonpath>

    <pythonpath>/home/yml/workdir/webdev/fabric_factory/20091023/fabric_factory/src/</pythonpath>

    <app mountpoint="/">

        <script>project.django_wsgi</script>

    </app>

</uwsgi>



Once this is done you can launch cherokee admin : 'cherokee-admin -u' and point your browser to this URL :
* http://127.0.0.1:9090/

Once in cherokee you can use the uWSGI wizard to create automatically your information sources and add a new rule. In order to do so you need to go into the virtual server section on the behavior tab [6]. Below the table there is an hyperlink called wizards. It will take you to a list of wizards, click on uWSGI.




Cherokee asks for the path to the uwsgi.conf file that we have created earlier. The last bit you might want to do is to add a rule to serve the admin media.





Troubleshooting

Here it is some basic technics to understand what is going on and solve your issues. Look at cherokee's log files :


sudo tail -f /var/log/cherokee/cherokee.error
and
sudo tail -f /var/log/cherokee/cherokee.access 

Look into the section called "information sources" in cherokee-admin. Explore the information sources created by the uWSGI wizard. Copy the text in the interpreter field, in my case it is :


/usr/local/bin/uwsgi -s /tmp/cherokee-source1.sock -C -x /[Path to my django project]/uwsgi.conf

Then you can paste it in a terminal this will give you a way to see what it is going on.

This new combination is still very young but it looks promising to me. I am sure that if we get enough people to hammer on it can quickly becomes popular.

[1] https://launchpad.net/~cherokee-webserver/+archive/ppa
[2] http://projects.unbit.it/uwsgi/wiki/Example
[3] http://projects.unbit.it/uwsgi/wiki/Doc
[4] http://www.cherokee-project.com/doc/basics_installation.html
[5] http://projects.unbit.it/uwsgi/wiki/Install
[6] http://127.0.0.1:9090/vserver/10

Oct 22, 2009

django-piston authentication against django auth -- part 4

The previous post ends with one known "Open Issue". The authentication for the ajax call was sitll hard coded [1]. In this post I am going to show you how you can extend django-piston to authenticate your users against django.contrib.auth. This might be important if your web app already take advantage of this module to manage and authenticate users.


A bit of reading of the source code and the documentation of django-piston [2] leads me to understand that django-piston has been designed from the ground to enable you to easily write your own authentication handler. Out of the box it comes with "HttpBasicAuthentication" and OAuthAuthentication. "HttpBasicAuthentication" is a very good example how to implement a DjangoAuthentication.

An authentication handler is a class, which must have 2 methods: is_authenticated, challenge

class DjangoAuthentication(object):
    """
    Django authentication. 
    """
    def __init__(self, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
        if not login_url:
            login_url = settings.LOGIN_URL
        self.login_url = login_url
        self.redirect_field_name = redirect_field_name
        self.request = None
    
    def is_authenticated(self, request):
        """
        This method call the `is_authenticated` method of django
        User in django.contrib.auth.models.
        
        `is_authenticated`: Will be called when checking for
        authentication. It returns True if the user is authenticated
        False otherwise.
        """
        self.request = request
        return request.user.is_authenticated()
        
    def challenge(self):
        """
        `challenge`: In cases where `is_authenticated` returns
        False, the result of this method will be returned.
        This will usually be a `HttpResponse` object with
        some kind of challenge headers and 401 code on it.
        """
        path = urlquote(self.request.get_full_path())
        tup = self.login_url, self.redirect_field_name, path 
        return HttpResponseRedirect('%s?%s=%s' %tup)


The implementation is relatively simple, is_authenticated uses the method with the same name available from a django User and the challenge redirect the user to the login page.

This post will end the series on django-piston it was for me a pretext to get familliar with this great app that make creating a RESTFUL api for django easy. You can find all the modifications I did into my fork on bitbucket [3]


[1] http://bitbucket.org/yml/django-piston/src/a8bcb7f9756e/examples/blogserver/templates/edit_ajaxy_post.html#cl-6
[2] http://bitbucket.org/jespern/django-piston/wiki/Documentation#authentication
[3] http://bitbucket.org/yml/django-piston/

Oct 18, 2009

django-piston form validation -- part 3

In my previous post titled "Exploration of django-piston -- part 2" [1] I end the post by 2 open issues. This post will propose a solution to the first one:

The validation errors are returned as a pseudo xml string.


Bad Request <ul class="errorlist"><li>content<ul class="errorlist"><li>This field is required.</li></ul></li><li>title<ul class="errorlist"><li>This field is required.</li></ul></li></ul>


To put it in other words I would prefer if django-piston return the validation errors of the form in the format specified in the request.

JSON


$ curl -u testuser:foobar -X POST -d content="This post is created using the api" http://127.0.0.1:8000/api/posts/?format=json
Bad Request {"title": ["This field is required."]}


yaml

$ curl -u testuser:foobar -X POST -d content="This post is created using the api" http://127.0.0.1:8000/api/posts/?format=yaml
Bad Request title: [!!python/unicode 'This field is required.']

xml


$ curl -u testuser:foobar -X POST -d content="This post is created using the api" http://127.0.0.1:8000/api/posts/?format=xml
Bad Request <ul class="errorlist"><li>title<ul class="errorlist"><li>This field is required.</li></ul></li></ul>


After a while reading the source of django-piston and poking around I have implemented this feature in my branch.

The biggest hurdle was that for some reasons "form.errors" cannot be serialized directly.


simplejson.dumps(form.errors)
*** TypeError:  is not JSON serializable


In order to work around this issue you need to force python to evaluate this proxy object.


dict((key, [unicode(v) for v in values]) for key,values in form.errors.items())


The last bit is to change the JS in order to adapt it to the fact that now the validation errors are returned as a json string.


    response = response.substring(12, response.length);
    errors = eval("("+response+")");

    $.each($(':input'), function(index, value) {
        field =$(value).attr('id');
        field = field.substring(3, field.length);
        field_errors = errors[field];
        if (field_errors) {
            ul = "<ul class=\"errorlist\">\n";
            $.each(field_errors, function(index, value){
              ul += "<li>"+value+"</li>\n";
            });
            ul += "</ul>\n";
            $(value).parent().prepend(ul);
        };
    });

The complete code related to the implementation of this feature is available in my branch [3].

Open Issue

Authentication is still hardcoded into the html pages. What is the best way to keep HttpBasicAuthentication to the external api that could easily be used by curl and add single sign on with django admin interface ?

The example is still very incomplete. It is missing among other things the capability to update/delete a blog post

I would be glad to read from you on how to improve this example or to get pointer of reusable application that have been built on top of django-piston.


[1] http://yml-blog.blogspot.com/2009/10/exploration-of-django-piston-part-2.html
[2] http://bitbucket.org/yml/django-piston/changeset/a8bcb7f9756e/
[3] http://bitbucket.org/yml/django-piston/

Oct 14, 2009

exploration of django-piston -- part 2

Djangocon 09 was really food for thought and this post follow my previous post about django-piston. I found the talk from Avi Bryant about his experience building Trendly [1].

My take away of this talk was that they have a single HTML page with a lot of JS. The JS is the used to get the JSON data the JSON from the server. Unfortunately I am unable to find a link to a better description of its talk.

My last post hopefully convince you that it is easy relatively easy to create a web api to your django application [2]. This Post aims to show you how you can create a page to list and create a blogpost.

List and create a blogpost

The idea here is to reuse the web api that we have created in the previous article to build a web page. One of the interesting feature of django-piston is the decorator @validate(BlogpostForm, 'PUT'). It enables you to validate the data send to your server and it takes as input a django form. A longuer description of this feature is available here [4].

The view cannot be simpler [5] :

def create_ajaxy_post(request):
    form = BlogpostForm()
    return direct_to_template(request,
                              template='edit_ajaxy_post.html',
                              extra_context={'form':form} )
The template is using jQuery [6] which ease the ajax call and the DOM manipulation. The template [7] has a very simple structure :


{% block content %}

<h1>Ajaxy</h1>

<div id="content"></div>

<h2>Ajaxy form</h1>

<form method="POST" action="{% url posts %}" id="post_form">

    <div id="post_form_error"></div>

    {{ form.as_p }}

    <input type='submit' name='create' onclick="return send_form();" value='Create'></input>

</form>

{% endblock %}
In order to give life to this page you need some JS which will get from the JSON string of the blogposts and POST the DATA to create a new blogpost and add it to the list.

If you try to POST an empty blogpost the data will be validated on the server and the validation errors will be displayed to the user :


* content
  o This field is required.
* title
  o This field is required.


Open Issue

The validation error are returned as a pseudo xml string :


Bad Request <ul class="errorlist"><li>content<ul class="errorlist"><li>This field is required.</li></ul></li><li>title<ul class="errorlist"><li>This field is required.</li></ul></li></ul>


It will be much more convenient to get a json dict with the error. Any one has an idea on how to get this ?

Authentication is still hardcoded into the html pages. What is the best way to keep HttpBasicAuthentication to the external api that could easily be used by curl and add single sign on wiht the admin ?

The example is still very incomplete. It is missing among other things the capability to update/delete a blog post

I would be glad to read from you on how to improve this example or to get pointer of reusable application that have been built on top of django-piston.


[1] http://trendly.com/
[2] http://bitbucket.org/yml/django-piston/src/tip/examples/blogserver/README.txt
[3] http://bitbucket.org/yml/django-piston/src/72c72e0b4b7e/examples/blogserver/templates/edit_ajaxy_post.html
[4] http://bitbucket.org/jespern/django-piston/wiki/Documentation#form-validation
[5] http://bitbucket.org/yml/django-piston/src/tip/examples/blogserver/blog/views.py
[6] http://jquery.com/
[7] http://bitbucket.org/yml/django-piston/src/72c72e0b4b7e/examples/blogserver/templates/edit_ajaxy_post.html

Sep 21, 2009

Add a web api to your app with django-piston

More and more often I found myself in a situation where I would like to add a web api to my django applications. I have recently tried to used django-piston http://bitbucket.org/jespern/django-piston for this and I found the learning curve a bit steep. It is not particularly hard but it requires you to understand few things before being able to enjoy it.

This post should help you to understand how to create the handlers to read, create, update, delete an object and see how you can call this web api from the command line using curl. I have forked django-piston to extend the example "blogserver". I would recommend you to also read the README inside the example

You can grab the code like this :

hg clone http://bitbucket.org/yml/django-piston/


curl is a command line tool to transfer data from or to a server, using one of the supported protocols (HTTP, HTTPS, FTP, FTPS, SCP, SFTP, TFTP, DICT, TELNET, LDAP or FILE). The command is designed to work without user interaction.

Here it is the 4 things that I found a bit hard to understand.

1. @require_extended


This decorator is handy if you want to restrict the access to your handler to only the request that have have one of the header listed below :
• application/json
• application/x-yaml
• text/xml
• application/python-pickle

The direct effect on the curl command line is that you will need to add a -H 'Content-Type:', this will give you something like :

$ curl -u testuser:foobar -H 'Content-Type:application/json' http://127.0.0.1:8000/api/posts/?format=json


If the header is omitted django-piston will return a 'Bad Request'.

2. Request Method

The handler for your resource can be composed of the following method : read, create, update, delete that are respectively mapped to the following request method : GET, POST, PUT, DELETE. "-X " is used to specify the method you want to use. Here it is an example that execute the update method of the blogserver :

$ curl -u testuser:foobar -H 'Content-Type:application/json' -X PUT -d '{"content": "Update test", "title": "Update test"}' http://127.0.0.1:8000/api/post/1/



3. Passing data to your handler

In this example I am going to demonstrate how to pass JSON string with curl. In order to do this you should use the -d followed by the JSON string :

$ curl -u testuser:foobar -H 'Content-Type:application/json' -X PUT -d '{"content": "Update test", "title": "Update test"}' http://127.0.0.1:8000/api/post/1/


django-piston will automatically turn this string into a python dictionary that is ready to be used by your handler. You will find this data in 'request.data' the raw string is available in "request.raw_post_data"

4. How to pass parameter to your handler


It took me a while to understand that the same handler can be mounted to several urls this will allow you to pass additional parameters like the primary key ID or the slug :

....
blogposts = Resource(handler=BlogpostHandler, authentication=auth)
urlpatterns = patterns('',
url(r'^posts/$', blogposts),
url(r'^post/(?P.+)/$', blogposts),
.....


In the example above "blockposts" is mapped to 2 different urls, the second one will be particularly handy to read, update, delete a particular post. Additional parameters from the URL will be passed to the method : read, create, update, delete. I encourage you to check out the code from bitbucket to see how you could take advantage of this technique.

This post barely scratch the surface of how to use django-piston, to put it in a nutshell this reusable application makes creating a web api for your own project simple. It avoids you to write a lot of boilerplate code because it abstracts all the machinery and let you focus on important things.

I would be glad to hear from you how you use it and what is your favorite trick.

Sep 13, 2009

Fabric factory

This is a project I have been working on recently after I spent a day to look at the existing solution to run periodically a test suite. Most of the project I look at were either difficult to setup or require to learn yet another specific domain specific language or had dependency on a larger software stack.

As a reaction to this situation I have decided to see if I could write something simple that achieves gracefully this task. I also try to make it as easy to setup as possible.

I have decided to use cpython as platform, django as web framework for the server and Fabric as library to automate the task execution.

The result of this mix can be found on bitbucket in a project called Fabric Factory. This will eventually become a complete Job Server that could be used to distribute any kind of task scripted in Fabric.


Installation

This assumes that python is installed on your computer and that you have an internet conection.

You can download the code using mercurial:
* hg clone http://bitbucket.org/yml/fabric_factory/
A fabfile will help you to quickly setup your environment.
* fab quickstart

Note : In order to run the command above you will need the latest version of Fabric the following command will take care of this:

pip install -e git://github.com/bitprophet/fabric.git#egg=Fabric

Usage

"quickstart" has created a virtualenv which must be actived before you continue.

. ve/bin/activate


Once the virtualenv is activated you can go inside "src/project". This is a django project so from there you can do several things :

* create an sqlite db : python manage.py syncdb
* run the server : python manage.py runserver
* run the test suite : python manage.py test

The main app of this django project called fabric factory is called "factory".

Once the server is started and that you have created some "Build" in django's admin interface you can open a new terminal and run the client side of the project:

cd src/worker
python run_worker.py --daemon=start
python run_worker.py --daemon=stop


Use case

Now that you have understood the layout of the project. Let us see how we can achieve something useful with it.

We are going to create a Build that will :
* download the Fabric Factory
* setup the environement
* run the test suite
* Report the result

1> Direct your browser to that url http://127.0.0.1:8000/admin/ and key in the username/password you have chosen for your administrator.
2> Add the fabfile recipe store in docs http://127.0.0.1:8000/admin/factory/fabfilerecipe/add/ and call it "fabric factory use case"
3> Replace example.com by 127.0.0.1:8000 in sites : http://127.0.0.1:8000/admin/sites/site/1/
4> Create a Build that will download setup and run the test here : http://127.0.0.1:8000/admin/factory/build/add/

The fabfile recipe that we have downloaded earlier contains a task called : 'download_setup_and_test' This task as been writen to do what we want.

We are now going to configure the client to run this task. However before doing this let us see how the server publish the tasks that need to be executed. Point your browser to this url : http://127.0.0.1:8000/factory/build/oldest_not_executed/

5> Open a new terminal and move into the the worker directory then you can start the worker in daemon mode :

. ve/bin/activate
cd src/worker
python run_worker.py --daemon=start


If you want to look at what is happening in the background you can watch at the log file in realtime

tail -f worker.log

6> You can see the status of the build here : http://127.0.0.1:8000/admin/factory/build/ Keep in mind this task is pretty long to run because we are downloading all the dependancies (django, fabric). It took me almost 5 minutes to execute this task and to see the result in the admin.

Conclusion

This project which is still very immature seems to prove that this stack is well suited to build this kind of tool. I would be glad to hear your experience about this kind of tool. Please do not hesitate to copy, fork, contribute to this project to make sure that soon we have a simple easy to setup yet flexible tool to distribute tasks.

Jun 22, 2009

sphinx autodoc and django app

Today I have been getting my foot wet with autodoc extension from sphinx. Here it is what the documentation of sphinx say about it :

"""
Sphinx is a tool that makes it easy to create intelligent and beautiful documentation, written by Georg Brandl and licensed under the BSD license.
"""

I have to say that before today my experience with sphinx as been excellent. It is relatively straightforward to get started once you pass the first little annoyances that come with the fact that you are learning a new tool.

The module I have been using to conduct this experiment is django-geotagging this reusable app enables you to geotag any object in the database. If you want to know more about this you can read the sphinx based documentation. This is the goal of the documentation, isn't it ? :-)
If adding a manually written documentation is very easy and well documented. The modification required to move from a very primitive documentation written using some rst file to sphinx is shown here. Most of it is automatically generated by : sphinx-quickstart.

Taking it a step further have been more difficult than anticipated and this for several reasons : lack of example, and a bug
Today I wanted to add to the existing documentation in django-geotagging and API section. In order to do so I have spoted in the sphinx documentation an extension called autodoc. It seems to be exactly what I need, here it is an extract from its doc :

"""
This extension can import the modules you are documenting, and pull in documentation from docstrings in a semi-automatic way.
"""

The first modification I add to do in sphinx's conf.py was to setup the settings in my environment. This can be done by adding the following 3 lines to conf.py

"""
from geotagging_demo_project import settings
from django.core.management import setup_environ
setup_environ(settings)
"""

Once this is done you should just be able to create a file that will be used as placeholder to describe the documentation you want to extract. Let us take an example now, since I want to describe the API of "models.py" I am going to create a file called "model.rst". In this file I need to add the following lines :

"""
:mod:`models` -- geotag models
==========================================

.. automodule:: geotags.models
:members:
:show-inheritance:

.. autoclass:: Point
"""

I will let you read the documentation for each of this directive to understand what they are doing. This is were the bug come into play because once you have done this you should be able to enjoy the automatically extrated documentation the next time you build it. Instead of this I have observed this bug. To put it in a nutshell sphinx is complaining about the line 27 of
"/usr/lib/python2.6/django/contrib/gis/db/models/proxy.py"

I am not really sure where the bug is however it seems that doing a small modification there enable me to build the documentation.

I hope that this feedback about my experience will help you to get started with using sphinx and its autodoc extension. If you have an opinion about the django ticket #11353 I am very interested to hear it.

Feb 27, 2009

Serving Django via CherryPy behind cherokee

Almost one year ago "Peter Baumgartner" wrote about Serving Django via cherrypy I am going to explain in this post how to take this approach one step further and to add load and balancing of the requests to several instances of cherrypy. This sounds like a lot of "*.conf" editing, isn't it ? In fact the nice thing about this approach is that the only file you will have to edit is your settings.py to add one line. To do so I am going to use cherokee mainly because it has a user friendly interface called cherokee-admin which provides a very easy way to configure your server. For this article I have used cherokee Version 0.98.1.

I will assume in this article that you have a working django project in a virtualenv. First you will need to install cherrypy and django-cpserver. You have several way to do this the easier is probably to use pip

pip install cherrypy
pip install -e git://github.com/lincolnloop/django-cpserver.git#egg=django-cpserver

Then you need to edit you settings.py to add "django_cpserver" in the list of your INSTALLED_APPS. This will give you a convenient django management command to start cherrypy server.
./manage.py runcpserver port=8089

Believe it or not this was the hardest part of the recipe from now to the end we will use a nice web interface. In order to launch cherokee-admin on ubuntu I use the following command :

sudo cherokee-admin

We need to define 2 remote sources in the admin interface :

127.0.0.1:8088 and 127.0.0.1:8088 are the addresses on which cherokee can contact the cherrypy instances. Several interested things to note here, the adresses can be spread on several computer and several ports.

Then we need to define a new target "/django" (alias) that will load and balance the requests to cherrypy instances.



Then for this target we need to set the handler to "HTTP reverse proxy".


It is time to use the remote sources we have defined earlier.



The last bit is to rewrite the url before passing it to the cherrypy instances



This is the end of the recipe you can now save the modification and restart the cherokee. I would be glad to read from you the enhancements that could be added to this recipe.

Feb 21, 2009

django full text search with solango

2 months ago I wrote a post titled "django fulltext search part-1" this post was explaining how to take advantage of djangosearch to interface between django and solr.

The big advantage of djangosearch is the fact that it comes with a plugable backend architecture. This can be a strength since in theory it enables you to abstract the details of the the full text engine you are using however in practice I ended up writing a patch to bypath the abstraction layer because it was preventing me of doing the query I wanted. So to make a long story short djangosearch was not working out of the box for my needs.

However 2 week ago Sean Creeley released solango and this significantly changed landscape of full text search in the django eco-system. I will not go in the details about solango in this post. It comes with some management commands that are so convenient that I still wonder why I haven't thought at implementing them on top of djangosearch. I will copy below a short extract form the documentation which is excellent :
./manage.py solr --help
#solango schema options
--fields Prints out the fields the schema.xml will create
--flush Will remove the data directory from Solr.
--reindex Will reindex Solr from the registry.
--schema Will create the schema.xml in SOLR_SCHEMA_PATH or in the --path.
--start Start solr running java -jar start.jar
--path=SCHEMA_PATH Tells Solango where to create config file

In the rest of the post I am going to assume that you have installed and configured solango. When I have done this I have not seen any major obstacle. Once again Sean has done an excellent job at documenting this project. one thing that annoys me while implementing solango in one of the project I am working on is the fact that you cannot restrict your search in what is often call an "advanced search". The good news is that solango has been recently improved to be easily extended. The only thing you will have to do is to defined a django form to represent your advanced search and to add an url that use it. This can literally be done in less than 30 lines of code including the comments and the imports.

Let us start by the writing the forms.py :

from django import forms
from solango.solr import get_model_from_key
import solango

def model_choices():
"""
Return a list of tuple with all the models that have been indexed.
This tuple is used in the AdvancedSearchForm to selects the models
you want to search your term in.
"""
models = [(model_key, get_model_from_key(model_key)._meta.verbose_name_plural)
for model_key in solango.registry.keys()]
return models

class AdvancedSearchForm(forms.Form):
"""
Form that represents an advanced search
"""
q = forms.CharField(required=False)
model = forms.MultipleChoiceField(choices=model_choices(), required=False,
widget=forms.CheckboxSelectMultiple)
def clean_q(self):
q = self.cleaned_data.get("q")
if q == '':
raise forms.ValidationError("You cannot query for an empty string")
return q

Nothing really complex there we have defined a form with a charfield named "q" that will be used to enter your search terms and a set of check boxes displaying all the models indexed. The user will be able to select one or several models to restrict its query.
Now that the form is defined we are going to use it. In order to do so you need to add the following url somewhere in your project.

from django.conf.urls.defaults import *

from solango.views import select
from populous.search.forms import AdvancedSearchForm

urlpatterns = patterns('',
url(r'^advanced/$', select,
{
"form_class":AdvancedSearchForm,
"template_name":None,
},name="search-advanced"),
)

You will recognize there the same pattern used in the generic views. You can customize the solango's view, called "select", by passing a "form_class" and a "template_name". In this example I have not over loaded the template_name thus django will used the solango default template.

I would be glad to read from you the customized form you have built on top of solango. Please do not hesitate to post them as comment.

Updated the 22th Feb 2009 : Correct the name of Sean Creeley, sorry for that.

Jan 31, 2009

Enable distutils for a django reusable app

As you might already be aware Pinax move to distutils, this change has been described by James Tauber in this post. Today I have decided to make my feet wet with this approach for a django reusable app. I have been guided by jezdez on #pinax.
Before starting I would recommend you to read this page, yes I know, it is a bit long but very interesting. after this reading you will be well prepared to start to work on you django reusable app.

I have used one of my project django-geotagging to experiment with this approach. The project can be found on launchpad there. The good news about this approach is that all the major rcs are supported : SVN, BZR, HG, ... If your favorite versionning system is missing there is a good chance that I just forget to mention it. This list is not exhaustive.

The core of this approach is a file called setup.py that need to be paced at the root of your repository, most of its argument are self explanatory.

This file enable you to setup a complete env in 4 steps :
The coolness of this increase with the number of reusable apps you have to install in order to build your web project.

I would be glad to learn from you what kind of cool things can be done once this infrastructure is in place.