EloquentBlog Part 5: OpenID

2011-03-07 02:27:40.687307

I'm a fan of OpenID. The Gawker Media fiasco was enlightening, though I'd been keen on someone else doing the tricky work of web passwords for me since long before that. So let's ditch our primitive auth and make this blog accept OpenID.

We'll start simple, with security.py:

eloquentgeek$ vim app/tools/security.py
GROUPS = {
    'j.random.user':['group:authors']
}

def groupfinder(userid, request):
    return GROUPS.get(userid, [])

For test purposes, set up a simple userid to group pairing. I've removed the USERS portion of this code. The userid will often be the first part of your email address (it is for Google OpenIDs). Next we need to rearrange login.py.

eloquentgeek$ vim app/login.py
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget, authenticated_userid
from pyramid.url import route_url
from google.appengine.api import users

def login(request):
    destination = request.params.get('to')
    if not destination:
        destination = request.route_url('home')

    user = users.get_current_user()
    if user:
        headers = remember(request, user.nickname())
        return HTTPFound(
            headers=headers,
            location=destination,
        )

    # No user, login form submission?
    if 'openid_identifier' in request.params:
        openid_login_url = users.create_login_url(
            dest_url=request.params.get('destination'),
            federated_identity=request.params.get('openid_identifier'),
        )
        return HTTPFound(
            location=openid_login_url
        )

    # No form submission, display form
    return dict(
        action=request.application_url + '/login',
        destination=destination,
    )


def logout(request):
    location = users.create_logout_url(
        dest_url=request.route_url('home')
    )
    headers = forget(request)
    return HTTPFound(
        location=location,
        headers=headers,
    )


def permission_denied(request):
    if authenticated_userid(request):
        # Logged in but no permission
        return HTTPFound(
            location=request.route_url('home'),
        )
    else:
        return HTTPFound(
            location=request.route_url('login') + '?to=' + request.url
        )

There are quite a few changes from the first version. I don't think I've settled on my preferred way of handling this in Pyramid, so hopefully someone more familiar with the framework can give me pointers. This works in the meantime!

There's a new view here: permission_denied. It distinguishes between users who are logged in but do not have the required permission, and those who aren't logged in at all (both trigger pyramid.exceptions.Forbidden). To accomplish this, main.py has also been altered:

eloquentgeek$ vim app/main.py
    config.add_route(
        'login', '/login',
        view='login.login',
        view_renderer='login.mak',
    )
    config.add_route(
        'logout', '/logout',
        view='login.logout'
    )
    config.add_view(
        'login.permission_denied',
        context='pyramid.exceptions.Forbidden',
    )

The thing to note here is that permission_denied and not login is reached on pyramid.exceptions.Forbidden. Ideally we would redirect to a more explanatory error message when a user lacks permissions but is properly logged in of course.

Note that I'm using the jQuery openid-selector, with img_path set to static/images/. My static folder structure is now as follows:

<< snip >>
  |~eloquentgeek/
  | |~app/
  | | |+lib/
  | | |~static/
  | | | |+images.large/
  | | | |+images.small/
  | | | |+images/
  | | | |~js/
  | | | | |-jquery-1.5.min.js
  | | | | |-openid-en.js
  | | | | `-openid-jquery.js
  | | | `-eg.css
<< snip >>

The login template is now as follows:

eloquentgeek$ vim app/templates/login.mak
 
<html> 
<head> 
	<title>EloquentBlog Login</title> 
	<link type="text/css" rel="stylesheet" href="/static/eg.css"/> 
	<script type="text/javascript" src="/static/js/jquery-1.5.min.js"></script> 
	<script type="text/javascript" src="/static/js/openid-jquery.js"></script> 
	<script type="text/javascript" src="/static/js/openid-en.js"></script>
	<script type="text/javascript">
		$(document).ready(function() {
			openid.init('openid_identifier');
		});
	</script>
</head> 
<body> 
	<form action="${action}" method="post" id="openid_form">
		<input type="hidden" name="destination" value="${destination}"/>
		<fieldset>
			<legend>Sign-in or Create New Account</legend>
			<div id="openid_choice">
				<p>Please click your account provider:</p>
				<div id="openid_btns"></div>
			</div>
			<div id="openid_input_area">
				<input id="openid_identifier" name="openid_identifier" type="text" value="http://"/>
				<input id="openid_submit" name="form.openid_submit" type="submit" value="Sign-In"/>
			</div>
			<noscript>
				<p>OpenID is service that allows you to log-on to many different websites using a single indentity.
				Find out <a href="http://openid.net/what/">more about
                OpenID</a>
                and <a href="http://openid.net/get/">how to get an OpenID
                enabled account</a>.</p>
			</noscript>
		</fieldset>
	</form>

</body> 
</html>

Credit Where Credit Due

2011-02-12 23:44:07.748239
Although I mentioned (and linked) Tim Hoffman's work in a previous post, I seem to have somehow snipped the part where I made it clear that he'd shown me this method of deploying Pyramid projects to App Engine. His tutorial, which was my initial source of help in this area, is located here.

EloquentBlog Supplemental: Directory Structure

2011-02-12 23:21:45.390333

I thought it might be useful at this point to show what my project directory looks like at this point, in case there's any confusion. Choice of project layout can be a fairly contested topic, but using Pyramid and buildout you can pretty much use whatever works best for you. The following is in no way intended to be prescriptive.

`~appengine/
  |~eloquentgeek/
  | |~app/
  | | |+lib/
  | | |+static/
  | | |~templates/
  | | | |-home.mak
  | | | |-login.mak
  | | | `-post.mak
  | | |~tools/
  | | | |-__init__.py
  | | | |-html.py
  | | | `-security.py
  | | |-app.yaml
  | | |-login.py
  | | |-main.py
  | | |-models.py
  | | `-views.py
  | |+bin/
  | |+develop-eggs/
  | |+downloads/
  | |+eggs/
  | |+include/
  | |+lib/
  | |+parts/
  | |-bootstrap.py
  | `-buildout.cfg
  `-tutorial

I've omitted .pyc files for brevity.

EloquentBlog Part 4: Simple Authentication and Authorisation

2011-02-12 22:56:17.446167

In this post I'll apply the URL Dispatch Wiki Authorization Tutorial to my little app. Not to knock the very thorough and complete Pyramid documentation, but I found that section of the tutorial a little difficult to follow at first so in case you're like me, I'll step through it piece by piece.

First of all, I'm going to need to add a root factory. This will allow me to specify an Access Control List for various resources. The documentation goes into more detail on this concept here. I'll edit my models.py to look like so:

eloquentgeek$ vim app/models.py
from google.appengine.ext import db
from pyramid.security import Allow, Everyone

class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:authors', 'post') ]
    def __init__(self, request):
        pass


class Post(db.Model):
    title = db.StringProperty(required=True)
    author = db.StringProperty(required=True)
    content = db.TextProperty(required=True)
    created = db.DateTimeProperty(auto_now=True)

This is very similar to the example from the documentation, except I've changed the permission to 'post' and the group name associated with it to 'authors'. Next, I'm going to define some users and groups in a very braindead format just to get them up and running:

eloquentgeek$ vim app/tools/security.py
USERS = {
    'myauthor':'abc123',
    'guest':'guest'
}

GROUPS = {
    'myauthor':['group:authors']
}

def groupfinder(userid, request):
    if userid in USERS:
        return GROUPS.get(userid, [])

Obviously, this isn't a real-world example! Please don't use this for an actual, implemented product: it's intended to demonstrate basic concepts. You can see I've provided two usernames and passwords. Hopefully it should be clear that one of them has been assigned to the 'authors' group, and that this klunky effort could be replaced by a more sensible datastore-backed solution. I will also be going into how to offer OpenID authentication.

groupfinder is an authentication policy callback. Its job is to take the provided userid and return the associated groups. Pretty straightforward so far?

A login template is required. This is very similar to what the Pyramid tutorial suggests:

eloquentgeek$ vim app/templates/login.mak
 
<html> 
<head> 
	<title>Eloquent Geek Login</title> 
	<link type="text/css" rel="stylesheet" href="/static/eg.css"/> 
</head> 
<body> 
<div>${message}</div>
    <form action="${url}" method="post">
        <input type="hidden" name="came_from" value="${came_from}"/>
        <input type="text" name="login" value="${login}"/><br/>
        <input type="password" name="password" value="${password}"/><br/>
        <input type="submit" name="form.submitted" value="Log In"/>
    </form>
</body> 
</html>

There shouldn't be too many surprises here. Now, view callables for login and logout. Rather than bundle these in with our views.py, I'll create login.py:

eloquentgeek$ vim app/login.py
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from pyramid.url import route_url

from tools.security import USERS

def login(request):
    login_url = route_url('login', request)
    referrer = request.url
    if referrer == login_url:
        referrer = '/' # never use the login form itself as came_from
    came_from = request.params.get('came_from', referrer)
    message = 'You'll need to log in to access that page.'
    login = ''
    password = ''
    if 'form.submitted' in request.params:
        login = request.params['login']
        password = request.params['password']
        if USERS.get(login) == password:
            headers = remember(request, login)
            return HTTPFound(
                location = came_from,
                headers = headers
            )
        message = 'Login failed.'

    return dict(
        message = message,
        url = request.application_url + '/login',
        came_from = came_from,
        login = login,
        password = password,
    )


def logout(request):
    headers = forget(request)
    return HTTPFound(
        location = route_url('home', request),
        headers = headers
    )

Again, very similar to the one in the tutorial. Note that the logout redirect points to my 'home' route. Note also that USERS is in tools.security. Once again, this is not a production-level security scheme but demonstrates simple authentication.

Adding a logout link is probably a good idea. In practice this could be added to a header template so it displays on all pages. In my simple system I'll just place it in the home template:

eloquentgeek$ vim app/templates/home.mak
% if logged_in:
<div>Logout</div>
% endif

logged_in must be provided to the template. Views that will need to know if a user is authenticated need the following code:

from pyramid.security import authenticated_userid
logged_in = authenticated_userid(request)

Accordingly, my views.py needs updating. Here's what it looks like now:

eloquentgeek$ vim app/views.py
from cgi import escape

from pyramid.url import route_url
from pyramid.httpexceptions import HTTPFound
from pyramid.security import authenticated_userid

from models import Post
from tools.html import HTMLWhitelist

def home(request):
    logged_in = authenticated_userid(request)

    allposts = Post.all()
    allposts.order("-created")
    posts = allposts.fetch(100)

    return dict(
        logged_in=logged_in,
        posts=posts,
    )


def post(request):
    logged_in = authenticated_userid(request)

    if 'form.submitted' in request.params:
        filter = HTMLWhitelist()
        clean_content = filter.postBody(request.params['content'])
        newpost = Post(
            title=escape(request.params['title']),
            author=logged_in,
            content=clean_content,
        )
        newpost.put()

        return HTTPFound(location=route_url('home', request))

    save_url = route_url('post', request)

    return dict(
        logged_in=logged_in,
        save_url=save_url,
    )

In the above, I don't test authenticated_userid at all. This is because I'm relying on the access control list rather than direct checking of the user at this point in execution. In other words, by the time execution arrives at this point the user should already be authenticated and have authority to access this view.

Here's how to do that.

eloquentgeek$ vim app/main.py
import sys

sys.path.insert(0,'lib')

from pyramid.configuration import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from google.appengine.ext.webapp.util import run_wsgi_app

from tools.security import groupfinder

settings = {
    'mako.directories': 'templates',
}

def main():
    authn_policy = AuthTktAuthenticationPolicy(
        'sosecret',
        callback=groupfinder
    )
    authz_policy = ACLAuthorizationPolicy()
    config = Configurator(
        settings=settings,
        root_factory='models.RootFactory',
        authentication_policy=authn_policy,
        authorization_policy=authz_policy,
    )

    config.add_route(
        'home', '/',
        view='views.home',
        view_renderer='home.mak',
    )
    config.add_route(
        'post', '/post',
        view='views.post',
        view_renderer='post.mak',
        view_permission='post',
    )
    config.add_route(
        'login', '/login',
        view='login.login',
        view_renderer='login.mak',
    )
    config.add_route(
        'logout', '/logout',
        view='login.logout'
    )
    config.add_view(
        'login.login',
        renderer='login.mak',
        context='pyramid.exceptions.Forbidden',
    )

    app = config.make_wsgi_app()
    run_wsgi_app(app)

if __name__ == '__main__':
    main()

There's quite a lot going on here, so starting from the top: policies for authentication and authorisation are imported, along with our groupfinder callback. I'm not going to spend much time covering policies here, as I'm just learning the ropes myself! This section of the glossary introduces the concepts.

    authn_policy = AuthTktAuthenticationPolicy(
        'sosecret',
        callback=groupfinder
    )
    authz_policy = ACLAuthorizationPolicy()
    config = Configurator(
        settings=settings,
        root_factory='models.RootFactory',
        authentication_policy=authn_policy,
        authorization_policy=authz_policy,
    )

Here I've specified a few additional options to the config. The root factory (which you may recall specifies the Access Control List) is passed as a parameter along with the policies to be used.

    config.add_route(
        'post', '/post',
        view='views.post',
        view_renderer='post.mak',
        view_permission='post',
    )

Notice that the route for 'post' now has an additional parameter, view_permission. This corresponds directly to the permission specified in the ACL and assigned to a user group in tools/security.py. Pyramid will throw Forbidden if any unauthorised user attempts to access this route.

    config.add_route(
        'login', '/login',
        view='login.login',
        view_renderer='login.mak',
    )
    config.add_route(
        'logout', '/logout',
        view='login.logout'
    )

These are pretty standard routes for my login page and logout view. What I suppose might be slightly confusing is that they are contained not in views.py but in login.py, hence 'login.login' and 'login.logout'. In fact, 'login.logout' is almost wilfully confusing, must remember to change that at some point!

    config.add_view(
        'login.login',
        renderer='login.mak',
        context='pyramid.exceptions.Forbidden',
    )

This is new. Here I'm specifying a context for the login view, telling Pyramid that I want to go to this view when Forbidden pops up. This will apply application-wide.

At this stage I can actually deploy the blog in its admittedly rudimentary state. To do this, I use appcfg:

eloquentgeek$ bin/appcfg update app

Hopefully you're reading the result of this command at eloquentgeek.com (if you don't have a domain assigned to your app, it can be found at appname.appspot.com).

I'd like to have my blog recognise OpenID authentication, so I'll tackle that in the next post.

EloquentBlog Part 3: Rudimentary Infrastructure

2011-02-12 22:49:29.781085

I guess I'm going to need a model.

eloquentgeek$ touch app/models.py
from google.appengine.ext import db

class Post(db.Model):
    title = db.StringProperty(required=True)
    author = db.UserProperty(required=True)
    content = db.TextProperty(required=True)
    created = db.DateTimeProperty(auto_now=True)

And a way to insert and retrieve it. To keep things fairly simple, we'll use filtered HTML to mark up the posts. DeWitt Clinton from Google demonstrates a way to do this on App Engine using html5lib in his appengine-html-whitelist app. Along these lines, I'll create a tools directory and html.py within it:

eloquentgeek$ mkdir tools
eloquentgeek$ cd tools
tools$ vim html.py
import html5lib
from html5lib import treebuilders, treewalkers
from html5lib import serializer, sanitizer

class PostContentFilter(sanitizer.HTMLSanitizer):
    allowed_elements = [
        'a', 'b', 'blockquote', 'br', 'code', 'em', 'h3', 'code', 'img', 'li',
        'ol', 'p', 'pre', 'strong', 'ul'
    ]


class HTMLWhitelist(object):
    """Filter content containing HTML.
    Based in large part on the work done by DeWitt Clinton in
    appengine-html-whitelist:
    http://code.google.com/p/appengine-html-whitelist/
    """

    def postBody(self, content):
        """Process content in message body according to specified whitelist."""
        parser = html5lib.HTMLParser(
            tokenizer=PostContentFilter,
            tree=treebuilders.getTreeBuilder("dom")
        )
        tree = parser.parse(content)
        body = tree.getElementsByTagName('body')[0]
        return ''.join([elem.toxml() for elem in body.childNodes])

Not exactly bulletproof, but it'll do for starters. Now, basic views to insert and retrieve posts.

tools$ cd ..
eloquentgeek$ vim app/views.py
from cgi import escape

from pyramid.url import route_url
from pyramid.httpexceptions import HTTPFound

from models import Post
from tools.html import HTMLWhitelist

def home(request):
    allposts = Post.all()
    allposts.order("-created")
    posts = allposts.fetch(100)

    return dict(posts=posts)


def post(request):
    if 'form.submitted' in request.params:
        filter = HTMLWhitelist()
        clean_content = filter.postBody(request.params['content'])
        newpost = Post(
            title=escape(request.params['title']),
            author=u'Anonymous',
            content=clean_content,
        )
        newpost.put()
        return HTTPFound(location=route_url('home', request))

    save_url = route_url('post', request)
    return dict(save_url=save_url)

Note that any old browser can submit anonymous posts. I'll need a way to identify authorised users of this makeshift blog, which I'll tackle in the next post in this series.

My template needs a little updating to handle the values that the new views provide.

eloquentgeek$ vim templates/home.mak
<html> 
<head> 
<title>Eloquent Geek</title>
<link rel="stylesheet" href="/static/eg.css" type="text/css" media="screen"/>
</head>
<body>
<h1>Eloquent Geek Blog</h1>
% for post in posts:
<h2 class="post_title">${post.title}</h2>
<div class="post_author">${post.author}</div>
<div class="post_created">${post.created}</div>
<div class="post_content">${post.content}</div>
% endfor
</body>
</html>
eloquentgeek$ vim templates/post.mak
<html> 
<head> 
</head>
<body>
<form action="${save_url}" method="post">
	<input type="text" name="title" size="60"/><br />
	<textarea name="content" rows="10" cols="60"></textarea><br />
	<input type="submit" name="form.submitted" value="Post"/>
</form>
</body>
</html>

Finally, a tiny bit of styling:

eloquentgeek$ vim static/eg.css
body {
    margin: 0px 50px 50px 50px;
    font-family: Verdana, sans-serif;
    font-size: 0.9em;
}

.created {
    font-size: 0.7em;
}

pre {
    margin: 20px;
}

You can style however you want, so I won't keep updating the CSS here. I might also add a bit of jQuery to make the code snippets a little nicer, but that can be done straight from the template so no need to involve Pyramid for now.

EloquentBlog Part 2: App Basics

2011-02-12 22:40:08.958564
eloquentgeek$ cd app

All App Engine apps require an app.yaml file. You can read more about them here. Mine will be as follows:

app$ vim app.yaml
application: eloquentgeek
version: 0-1
runtime: python
api_version: 1

handlers:
- url: /static
  static-url: static

- url: /.*
  script: main.py

That's about as simple as app.yaml gets. There are plenty of other options but this'll do for now. All it's really saying is to route all requests to the handler main.py, which I'm creating next. It looks like so:

app$ vim main.py
import sys

sys.path.insert(0,'lib')

from pyramid.configuration import Configurator
from google.appengine.ext.webapp.util import run_wsgi_app

settings = {
    'mako.directories': 'templates',
}

def main():
    config = Configurator(settings=settings)

    config.add_route(
        'home', '/',
        view='views.home',
        view_renderer='home.mak'
    )

    app = config.make_wsgi_app()
    run_wsgi_app(app)

if __name__ == '__main__':
    main()

The libraries have been installed by buildout into project/app/lib, so the first thing main.py does is add that path to sys.path. Then it proceeds as normal for a basic Pyramid application, which you can read more about in the Basic Layout tutorial (except for the WSGI elements which are akin to the method described here.

There are differences to the standard Paster-based Pyramid layout. Notably settings cannot come from .ini files specified with paster serve, so they are included inline. I think it'd be fine to specify them in a .yaml file as well, I'll investigate that option. For now the only setting I need is the location of the Mako template directory.

My new 'home' route requires a view. I'll create an app/views.py file like so:

def home(request): content = None return dict(content=content)

Can't get much simpler than that! Now, a template. I'll create app/templates/home.mak:

app$ vim templates/home.mak
<html> 
<head> 
<title>Eloquent Geek</title>
</head>
<body>
<p>Home.</p>
</body>
</html>

Ok, in theory this should now run. Let's try it:

app$ cd ..
eloquentgeek$ bin/dev_appserver app &

I often run this command with the -c flag to dump the previous contents of the datastore. Who needs all those posts entitled "asdfsldkfjsdlkfj" anyway?

Browse to http://localhost:8080 and the 'Home' template appears. Now, in order to get this post online I need a rudimentary blog infrastructure.

EloquentBlog Part 1: Using Pyramid with Google App Engine

2011-02-12 22:33:42.888426

As an exercise, I'm writing a tutorial on simple, incremental blog development using Python, Google App Engine, and the Pyramid framework. This is not intended to be comprehensive: it's more of an introduction for those just getting to grips with Pyramid and App Engine. Corrections and suggestions for improvement are welcome. Once I implement a comment facility of course...

The first obstacle to creating an App Engine development environment on most systems is installing Python 2.5 without impacting other Python installations on your system. Ubuntu, for example, no longer has 2.5 as part of its repositories. Fortunately this can be solved thanks to the deadsnakes repositories (thanks Felix!):

https://launchpad.net/~fkrull/+archive/deadsnakes

$ sudo add-apt-repository ppa:fkrull/deadsnakes  
$ sudo aptitude update  
$ sudo aptitude install python2.5

(Obviously, substitute apt-get if you prefer.) You'll need Python2.5 and virtualenv to make any of this work.

To begin with I'll start a virtual Python environment with the project name of choice. I'll use my domain name.

$ virtualenv --no-site-packages -p python2.5 eloquentgeek

It's really important that you don't attempt this with a Python version other than 2.5. App Engine uses 2.5, and anything else will lead to your own frustration and heartbreak. It may seem to work at first, but don't be fooled!

$ cd eloquentgeek
$ bin/pip install appfy.recipe.gae

Appfy contains Rodrigo Moraes' zc.buildout recipes for App Engine development. It's a handy way to assemble the packages you need in your virtual environment. Rodrigo also develops Tipfy, a Python framework for App Engine development.

Notice I'm running pip from inside the virtualenv instead of using the default pip, which might be another Python version (2.6.6 in the current Ubuntu 10.10). This ensures everything is installed with Python 2.5. The App Engine development team just added Python 2.7 support to their roadmap, so the outlook is good for a more up to date version to work with in the future.

Now I need somewhere to put the app. Notice that the files that actually make up the application and the scripts and binaries downloaded by buildout are kept separate. The buildout.cfg will install library source in app/lib, so I'll create that directory beforehand. I'll also create templates and static directories for user later on:

eloquentgeek$ mkdir app
eloquentgeek$ mkdir app/lib
eloquentgeek$ mkdir app/static
eloquentgeek$ mkdir app/templates

Time for the buildout.cfg. This tells buildout to use Rodrigo's Appfy recipes, and includes a few configuration instructions. For another version, have a look at Tim's one. I just tried to keep everything as simple as humanly possible for the moment.

[buildout]
parts =
    gae_sdk
    gae_tools
    app_lib

# Relative paths allow the buildout to be moved around.
relative-paths = true

# Unzip eggs automatically, if needed.
unzip = true

# buildout.bootstrap automatically adds bootstrap.py
# requires pip install buildout.bootstrap
extensions = buildout.bootstrap

[gae_sdk]
# Dowloads and extracts the App Engine SDK.
recipe = appfy.recipe.gae:sdk
url = http://googleappengine.googlecode.com/files/google_appengine_1.4.1.zip

[gae_tools]
# appcfg, bulkload_client, bulkloader, dev_appserver,
# remote_api_shell, python executables
recipe = appfy.recipe.gae:tools
# Add these paths to sys.path in the generated scripts.
extra-paths =
    app
    app/lib

[app_lib]
# Sets the library dependencies for the app.
recipe = appfy.recipe.gae:app_lib
lib-directory = app/lib
use-zipimport = false

# Download the following packages:
eggs =
    pyramid
    html5lib

# Don't copy files that match these glob patterns.
ignore-globs =
    *.c
    *.pyc
    *.pyo
    *.so
    */test
    */tests
    */testsuite
    */django
    */sqlalchemy
    */_zope_interface_coptimizations.py
    */_zope_i18nmessageid_message.py
    */_speedups.py

# Don't install these packages or modules.
ignore-packages =
    distribute
    setuptools
    easy_install
    site
    ssl

Save this as 'buildout.cfg' in your project directory (eloquentgeek for me). Then run buildout:

eloquentgeek$ bin/buildout

It'll take awhile to get everything squared away. When it's done, your project directory will look something like this:

eloquentgeek$ ls
app  bootstrap.py  develop-eggs  eggs     lib
bin  buildout.cfg  downloads     include  parts

These steps lay the groundwork, allowing me to hack on my Pyramid application without worrying too much about the Python environment. Now I'd like to get something up and running so I can be reassured that it's all working as expected.