What’s Pylons?

I’ll assume you’ve heard the buzz about Ruby on Rails. Pylons, like Rails, is an MVC framework, but written in the Python language.

This isn’t meant to be an intro to MVC frameworks, but for those just getting started… MVC frameworks let you keep the presentation, business logic, and data separate. They also take care of the majority of setup work involved with building a new web app. I could write another post just on this topic alone, but many people already have. BetterExplained as a great tutorial entitled, Understanding Models, Views and Controllers.

The Real Intro

I’ve been using Pylons for about 9 months now. I have picked up some nice conventions, and created some of my own along the way. I had initially developed a web app with Pylons 0.9.5, and it was quite impressive. When learning all new frameworks there is a bit of a learning curve, especially when slapping together several pieces of middleware and other various tools.

Last week I upgraded to Pylons 0.9.6 — and things broke. Although it didn’t take nearly as much time to get moving again, there were definitely some hiccups.

In the hopes of alleviating your pain, I will show you how to get Pylons working with an authentication system (AuthKit), the database model (SQLAlchemy 0.4), and some conventions I established with the Mako templating engine.

Getting started

This tutorial assumes that you already have Python 2.x and Pylons installed, and that you have created your first basic project. If not, don’t worry! Just check out the great tutorial put together by the community on Getting started with Pylons.

Ok… now that we are on the same page, you’ll need to grab 1 or 2 more packages before we continue. After that I will walk you through each file you’ll need to edit.

Installation

Use easy install for quick installation of these kits or see Installing Pylons to get easy install working for you. If you just installed Pylons for the first time you’ll probably already have the latest version of SQLAlchemy. This tutorial was written for the versions specified below, so later versions may not always work the same way. If you find a later version doesn’t quite work, please post a comment (with the solution changes if possible). Thanks!

easy_install SQLAlchemy==0.4
easy_install AuthKit==0.3.0

Modifying your config

Note: Replace all occurrences of MYFIRSTAPP with the name of your application.

/config/environment.py

"""Pylons environment configuration"""
import os
 
from pylons import config
from sqlalchemy import engine_from_config
from MYFIRSTAPP.model import init_model
 
import MYFIRSTAPP.lib.app_globals as app_globals
import MYFIRSTAPP.lib.helpers
from MYFIRSTAPP.config.routing import make_map
 
def load_environment(global_conf, app_conf):
    """Configure the Pylons environment via the ``pylons.config``
    object
    """
    print "Loading environment..."
 
    # Pylons paths
    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    paths = dict(root=root,
                 controllers=os.path.join(root, 'controllers'),
                 static_files=os.path.join(root, 'public'),
                 templates=[os.path.join(root, 'templates')])
 
    # Initialize config with the basic options
    config.init_app(global_conf, app_conf, package='MYFIRSTAPP',
                    template_engine='mako', paths=paths)
 
    config['routes.map'] = make_map()
    config['pylons.g'] = app_globals.Globals(global_conf, app_conf)
    config['pylons.h'] = MYFIRSTAPP.lib.helpers
 
    # Customize templating options via this variable
    tmpl_options = config['buffet.template_options']
 
    #
    # CONFIGURATION OPTIONS HERE (note: all config options will override
    # any Pylons config options)
    #
    tmpl_options['mako.imports'] = ['from webhelpers import auto_link as l',
                                    'from webhelpers import simple_format as s',
                                    'from MYFIRSTAPP.lib import myfilters',
                                    'from pylons import config as CONFIG']
 
    # Give support for Unicode in Mako templating system
    # from: http://wiki.pylonshq.com/pages/viewpage.action?pageId=5439551
    tmpl_options['mako.input_encoding'] = 'utf-8'
    tmpl_options['mako.output_encoding'] = 'utf-8'
 
    # Load Database Model
    print "Loading database model from <sqlalchemy.default> config..."
    engine = engine_from_config(config, 'sqlalchemy.default.')
    init_model(engine)

You’ll notice that I am importing several files to use in Mako, my templating engine of choice. I want to make my config file (development.ini or whichever other config file I decide to load my app with) available to the templates. I also import a file I created called myfilters. This gives me the ability to use additional filters that might not be included with WebHelpers (a wonderful library that gets included with Pylons out of the box). I will show you what to do with this file once we take a look at the files in the /lib/ directory.

/config/middleware.py

"""Pylons middleware initialization"""
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool
 
from pylons import config
from pylons.error import error_template
from pylons.middleware import error_mapper, ErrorDocuments, ErrorHandler, \
    StaticJavascripts
from pylons.wsgiapp import PylonsApp
 
from MYFIRSTAPP.config.environment import load_environment
 
def make_app(global_conf, full_stack=True, **app_conf):
    # Configure the Pylons environment
    load_environment(global_conf, app_conf)
 
    # The Pylons WSGI app
    app = PylonsApp()
 
    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
 
    if asbool(full_stack):
        # Handle Python exceptions
        app = ErrorHandler(app, global_conf, error_template=error_template,
                           **config['pylons.errorware'])
 
        # import AuthKit
        import authkit.authenticate
        app = authkit.authenticate.middleware(app, app_conf)
 
        # Display error documents for 401, 403, 404 status codes (and
        # 500 when debug is disabled)
        app = ErrorDocuments(app, global_conf, mapper=error_mapper, **app_conf)
 
    # Establish the Registry for this application
    app = RegistryManager(app)
 
    # Static files
    javascripts_app = StaticJavascripts()
    static_app = StaticURLParser(config['pylons.paths']['static_files'])
    app = Cascade([static_app, javascripts_app, app])
    return app

Nothing to interesting here. This is where we are adding the AuthKit to our middleware stack.

Modifying lib

Let’s turn our attention over to the /lib folder of the project.

/lib/app_globals.py

All we’re adding here is a line to import the model.

from MYFIRSTAPP.model import init_model
 
class Globals(object):
    def __init__(self, global_conf, app_conf, **extra):
        """
        Globals acts as a container for objects available throughout
        the life of the application.
 
        One instance of Globals is created by Pylons during
        application initialization and is available during requests
        via the 'g' variable.
 
        ``global_conf``
            The same variable used throughout ``config/middleware.py``
            namely, the variables from the ``[DEFAULT]`` section of the
            configuration file.
 
        ``app_conf``
            The same ``kw`` dictionary used throughout
            ``config/middleware.py`` namely, the variables from the
            section in the config file for your application.
 
        ``extra``
            The configuration returned from ``load_config`` in 
            ``config/middleware.py`` which may be of use in the setup of
            your global variables.
 
        """
        pass

/lib/base.py

Base is called before entering a controller. AuthKit needs to be integrated into this file as well. The database session also gets initialized at this point. You can take the approach of using AuthKit on a case by case basis, but my app requires the user to be logged in for all tasks.

import md5
 
from pylons import c, config as CONFIG, g, cache, request, response, session
from pylons.controllers import WSGIController
from pylons.decorators import jsonify, validate
from pylons.templating import render, render
from pylons.controllers.util import abort, redirect_to, etag_cache
from pylons.i18n import N_, _, ungettext
from sqlalchemy import *
 
from authkit.permissions import NotAuthenticatedError
from MYFIRSTAPP.model import meta
from MYFIRSTAPP.model.tables import *
import MYFIRSTAPP.lib.helpers as h
 
class BaseController(WSGIController):
    def __call__(self, environ, start_response):
        # Insert any code to be run per request here. The Routes match
        # is under environ['pylons.routes_dict'] should you want to check
        # the action or route vars here
 
        # Each request explicitly gets its own session
        conn = meta.engine.connect()
        meta.Session.configure(bind=conn)
        try:
            return WSGIController.__call__(self, environ, start_response)
        finally:
            meta.Session.remove()
            conn.close()
 
    def __before__(self, action, **params):
        # Init variables for template
        c.auth_user = {'id':0, 'nickname': '', 'first': '', 'signed_in': False}
 
        # add actions here that do NOT require auth
        if action == 'do_signin' or action == 'login_screen' or action == 'do_signout':
            return
        elif not request.environ.has_key('REMOTE_USER'):
            # Message to pass to template
            c.message = "Please sign in"
            raise NotAuthenticatedError('Not Authenticated')
        else:
            user = meta.Session.query(User).filter_by(nickname=request.environ.get('REMOTE_USER')).first()
            if user:
                # Variables to make available in all templates (and controllers)
                c.auth_user = {'id': user.user_id, 'nickname': user.nickname, 'first': user.first_name, 'signed_in': True}
 
    def login_screen(self):
        return render("/home/login.mako")
 
    def do_signin(self):
        if len(request.params) > 1:
            uname = request.params.get('username', '').strip()
            pword = request.params.get('password', '').strip()
            user = meta.Session.query(User).filter_by(nickname=uname).first()
            entered_password = md5.new(pword)
            if user and (user.password == entered_password.hexdigest()):
                request.environ['paste.auth_tkt.set_user'](uname.encode('utf8'))
 
                h.redirect_to('home')
            else:
                c.message = "Invalid username and/or password" 
                return render('/home/login.mako')
        else:
            c.message = "Please enter your username and password"
            return render('/home/login.mako')
 
    def do_signout(self):
        if not request.environ.has_key('REMOTE_USER'):
            c.message = "You are not logged in"
            return render('/home/login.mako')
        else:
            c.message = "You have been logged out"
            return render('/home/login.mako')
 
# Include the '_' function in the public names
__all__ = [__name for __name in locals().keys() if not __name.startswith('_') \
           or __name == '_']

Here, we are defining the methods that take care of authenticating the user. I will show you the routes I use later to make this possible.

/lib/myfilters.py

As I mentioned above, I am using some custom filters in my Mako templates that WebHelpers doesn’t provide. Since I included the file above, here is a snippet of what the file looks like. I will be showing you how to use these in your templates later in this Zero to 60 with Pylons series.

"""
Template filters
 
All names available in this module will be available in all Mako templates
"""
import MYFIRSTAPP.lib.helpers as h
import cgi
import re
from datetime import time, datetime
from time import strptime
 
#
# Right now the in_format and out_format are predefined
# We should use the decorator pattern and allow caller to specify in and out formats for datetime object
#
# see: http://groups.google.com/group/mako-discuss/browse_thread/thread/ef994d9488e8973a/5918bfc84474fafc?lnk=gst&q=filters&rnum=12#5918bfc84474fafc
#
def transform_date(text):
    dt = datetime(*strptime(text, "%Y-%m-%d %H:%M:%S")[0:6])
    return dt.strftime("%b %d %Y %H:%M:%S %Z")
 
def transform_date_tuple(date_tuple_string):
    date_tuple_string = date_tuple_string.replace('(','')
    date_tuple_string = date_tuple_string.replace(')','')
    date_list = date_tuple_string.split(', ')
    date_tuple = datetime(int(date_list[0]), int(date_list[1]), int(date_list[2]), int(date_list[3]), int(date_list[4]), int(date_list[5]))
    return date_tuple.strftime("%b %d %Y %H:%M:%S %Z")
 
#
# Converts a datetime object into a string representing time past
# Will apply special style to time past if within 24 hours
#
# Output format:
# '4 hours ago'
#
def str_to_time_ago(dt_str):
    dt = str_to_datetime(dt_str)
 
    # Convert date to something like 'about 4 hours ago'
    text = h.time_ago_in_words(dt) + ' ago'
 
    # if within 24 hours then add class
    now_time = datetime.now()
    difference = now_time-dt
 
    #if you wanted to get weeks and days
    #weeks, days = divmod(difference.days, 7)
 
    if difference.days < 1:
        text = '<span class="timestamp_new">' + text + '</span>'
 
    return text
 
#
# Converts a standard datetime string into a datetime object
# Possible input formats:
# '2007-09-05 17:10:27'
# '2007-09-05'
#
def str_to_datetime(dt_str):
    # Parse the string
    pieces = dt_str.split(' ')
 
    # Do we have a date or datetime str?
    if len(pieces) > 1:
        mytime = pieces[1].split(':')
    else:
        mytime = [0, 0, 0]
 
    mydate = pieces[0].split('-')
    dt = datetime(int(mydate[0]), int(mydate[1]), int(mydate[2]), int(mytime[0]), int(mytime[1]), int(mytime[2]))
 
    return dt

A few other files to modify

/websetup.py

This is usually only called when you make a change to the database model (or setting up for the first time). It will tear down your database model and rebuild it. It basically imports your model and calls the create_all method that will build your database model.

 
"""Setup the MYFIRSTAPP application"""
import logging
 
from paste.deploy import appconfig
from pylons import config
 
from MYFIRSTAPP.config.environment import load_environment
 
log = logging.getLogger(__name__)
 
def setup_config(command, filename, section, vars):
    """Place any commands to setup temp here"""
    conf = appconfig('config:' + filename)
    load_environment(conf.global_conf, conf.local_conf)
 
    # Import late.  Otherwise, if anything is wrong in the model, the
    # whole import will fail, and Paste will mistakenly think that
    # websetup doesn't even exist.
 
    from MYFIRSTAPP.model import create_all
 
    print "Creating tables..."
    create_all(conf.local_conf)
 
    print "\n\n-----------------------------------"
    print "         SETUP SUCCESSFUL"
    print "-----------------------------------"

/development.ini

This is the configuration file that you will load when serving up your web app. I show you development.ini here, but you’ll basically do the same thing for production.ini with the exception of a few tweaks. Tweaks could include setting debug to false, changing variables that affect UI, etc. My reason for including this is to show you where to place the AuthKit and SQLAlchemy settings.

These tweaks must be part of the config for [app:main]. I put them under:

#set debug = false
authkit.enable = true
authkit.method = forward
authkit.cookie.secret = a random dtk string
authkit.signin = /signin
authkit.cookie.signout = /signout
#authkit.users.setup = user:pylons
 
# Specify the database for SQLAlchemy to use via
# pylons.database.session_context.
# %(here) may include a ':' character on Windows environments; this can
# invalidate the URI when specifying a SQLite db via path name
sqlalchemy.default.url = mysql://USERNAME:PASSWORD@localhost:PORT/MYFIRSTAPP
sqlalchemy.default.echo = True
sqlalchemy.default.pool_recycle = 14400
sqlalchemy.default.convert_unicode = True
sqlalchemy.default.encoding = utf-8
 
# leave blank or use 'default' if using default seed set
database.seed_set = default

I will discuss the database seed file in the next part of this series.

Read on…

Continue to Part 2 and find out how to modify the model

Feeling anxious? Download the bare bones project now.

Update: Changed ‘models’ to ‘model’ because while I upgraded from ver 0.9.5 and had used the old paster, it got an upgrade and now creates the models directory as ‘model.’ I also changed 2 occurrences of ‘rosetta,’ which was my old app name and renamed them correctly to MYFIRSTAPP.