Sewing Success with Fabric
Recently updated on
I wanted to share a quick practical example of how Fabric, can make your development life easier. If you're unfamiliar with Fabric I recommend checking out its tutorial which describes Fabric thusly;
Fabric is a Python (2.5 or higher) library and command-line tool for streamlining the use of SSH for application deployment or systems administration tasks.
The tutorial is a great resource as Fabric does not have a dense API. In fact, it has a childishly simple API. Today, I found myself using it to simplify a process I had been repeatedly performing manually. A client site of ours required that some ticket vouchers be automatically generated anytime someone signs up for a tour of their new facility.
Using Reportlab, complex and detailed PDF files can be generated programmatically. However, if your PDF is sufficiently detailed, you will quickly run into the iterative tweak-and-check nature of the process to get everything positioned and sized just right. If your PDF generation is coupled to some Django code and say you have no local testing environment this can get quite tiring, fast.
In my case, I was tweaking the source, commiting the change, pushing to the server, restarting the Django process, signing up for a tour, waiting for the email with the attached PDF's, downloading them and then reviewing the result. Yeah. I know.
The first thing I did to smoothen out this process was to setup Django unit-testing, which is not much more than dropping some tests into tests.py in your app directory and using "./manage.py test <appname>" I noticed that the test runner could be changed so I did a quick search to see if there was support for Nose. Nose is nice in that it allows you to write "naked" tests without having to subclass anything. Just drop some functions named "test_some_feature" and you're good to go. I found django-nose which seemed relatively updated and installed it, and Nose, with Pip.
When using the Django test management command, a test database will be created before the tests are run and destroyed when they are finished. As a result, my tests first populate the database with our Tours fixture and a single Participant. They are then used to generate a voucher:
import os from django.core.management import call_command from tours.models import Tour, Participant from tours.vouchers import VoucherGen def test_tours_creation(): ''' Load Tour fixture and check success ''' call_command('generate_data', 'tours.maketours') tours = Tour.objects.all() assert len(tours) > 0 def test_participant_creation(): ''' Create participant and check success ''' p = Participant(full_name='Dustin Lacewell', address='Nowhere', city='Somewhere', state='IL', zip=55555, email='dlacewell@example.com', phone='5555555555') p.save() participants = Participant.objects.all() assert len(participants) == 1 def test_voucher_creation(): ''' Generate voucher PDF and check it exists ''' people = [Participant.objects.get(id=1)] tour = Tour.objects.all()[0] vg = VoucherGen(people, tour, upload_to="/tmp/") for p, filename in vg: assert os.path.isfile(filename)
So now, anytime I execute "./manage.py test tours" a PDF is automatically created at /tmp/1.pdf. If I stopped at this point, the process would be much lighter than before since we no longer require the browser to regenerate the PDF. But I haven't even touched Fabric yet!
Let us use Fabric to automate the remaining steps of the process so that we can view the PDF on our local machine with out much effort. First we'll draw up a basic fabfile.py template with a function that will login to our remote server and switch into the site's Virtualenv:
import os from fabric.api import * env.hosts = ['atlas'] PROJ_PATH = '/iscape/sites/somesite/proj/somesite/' ACTIVATE_FILE = os.path.join(PROJ_PATH, '../../bin/activate') def vrun(command): run("source %s; %s" % (command, ), shell=True)
We start off by importing all of the Fabric API from the helper-submodule. This gives us access to the most used Fabric function calls. We set env.hosts to a list containing the hostname of the target server where this site is deployed. This makes it so we do not have to explicitly name it on the commandline later. I then define some "constants" containing paths to both the Django project root and to the Virtualenv activation script. The only function is "vrun" which wraps "run" by first "sourcing" the Virtualenv activation script. This makes it so commands called through vrun will be under the influence of the Virtualenv.
The next thing we want is a simple function that will let us call Django management commands. This is easy enough:
def manage(command): with settings(warn_only=True): with cd(PROJ_PATH): run('git pull') vrun('python manage.py %s' % (command, ))
This function will change to the Django root directory, perform a "git pull" to ensure up-to-date code and then will call the passed managed command. Notice that we use vrun, the method defined first, so that the management command is run under the influence of the site's Virtualenv. Also notice that the whole thing is wrapped in a "with context" that prevents Fabric from bailing if any remote commands return an error. This is incase the Django management command or the tests we wrote above fail. If they do, Fabric will continue on.
The last thing we need to do is wrap it up all together by invoking the Django test suite, downloading the PDF file and then opening it with the commandline PDF viewer Envice.
def testpdf(): manage('test tours') get('/tmp/1.pdf', '/tmp/') local('evince /tmp/1.pdf')
This one couldn't get any simplier. Especially since we've simplified our own work by creating the "manage" function above. We invoke the Django test suite (which causes the PDF to be created), we download the file to our local /tmp directory. And then we open the PDF to view locally, with evince. To actually invoke this code, we throw it into a fabfile.py and execute the testpdf task with the following commandline:
~/dev/work/iscape/somesite# fab testpdf
To put it all into perspective, here is the complete file:
import os from fabric.api import * env.hosts = ['atlas'] PROJ_PATH = '/iscape/sites/somesite/proj/somesite/' ACTIVATE_FILE = os.path.join(PROJ_PATH, '../../bin/activate') def vrun(command): run("source %s; %s" % (command, ), shell=True) def manage(command): with settings(warn_only=True): with cd(PROJ_PATH): run('git pull') vrun('python manage.py %s' % (command, )) def testpdf(): manage('test tours') get('/tmp/vouchers/1.pdf', '/tmp/') local('evince /tmp/1.pdf')