Todo List Application in One File

This tutorial is intended to provide you with a feel of how a Pyramid web application is created. The tutorial is very short, and focuses on the creation of a minimal todo list application using common idioms. For brevity, the tutorial uses a "single-file" application development approach instead of the more complex (but more common) "scaffolds" described in the main Pyramid documentation.

At the end of the tutorial, you'll have a minimal application which:

  • provides views to list, insert and close tasks
  • uses route patterns to match your URLs to view code functions
  • uses Mako Templates to render your views
  • stores data in an SQLite database

Here's a screenshot of the final application:

../_images/single_file_tasks.png

Step 1 - Organizing the project

Note

For help getting Pyramid set up, try the guide Installing Pyramid.

To use Mako templates, you need to install the pyramid_mako add-on as indicated under Major Backwards Incompatibilities under What's New In Pyramid 1.5.

In short, you'll need to have both the pyramid and pyramid_mako packages installed. Use easy_install pyramid pyramid_mako or pip install pyramid and pip install pyramid_mako to install these packages.

Before getting started, we will create the directory hierarchy needed for our application layout. Create the following directory layout on your filesystem:

/tasks
    /static
    /templates

Note that the tasks directory will not be used as a Python package; it will just serve as a container in which we can put our project.

Step 2 - Application setup

To begin our application, start by adding a Python source file named tasks.py to the tasks directory. We'll add a few basic imports within the newly created file.

1
2
3
4
5
6
7
 import os
 import logging

 from pyramid.config import Configurator
 from pyramid.session import UnencryptedCookieSessionFactoryConfig

 from wsgiref.simple_server import make_server

Then we'll set up logging and the current working directory path.

 9
10
11
12
 logging.basicConfig()
 log = logging.getLogger(__file__)

 here = os.path.dirname(os.path.abspath(__file__))

Finally, in a block that runs only when the file is directly executed (i.e., not imported), we'll configure the Pyramid application, establish rudimentary sessions, obtain the WSGI app, and serve it.

14
15
16
17
18
19
20
21
22
23
24
25
26
 if __name__ == '__main__':
     # configuration settings
     settings = {}
     settings['reload_all'] = True
     settings['debug_all'] = True
     # session factory
     session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
     # configuration setup
     config = Configurator(settings=settings, session_factory=session_factory)
     # serve app
     app = config.make_wsgi_app()
     server = make_server('0.0.0.0', 8080, app)
     server.serve_forever()

We now have the basic project layout needed to run our application, but we still need to add database support, routing, views, and templates.

Step 3 - Database and schema

To make things straightforward, we'll use the widely installed SQLite database for our project. The schema for our tasks is simple: an id to uniquely identify the task, a name not longer than 100 characters, and a closed boolean to indicate whether the task is closed.

Add to the tasks directory a file named schema.sql with the following content:

create table if not exists tasks (
    id integer primary key autoincrement,
    name char(100) not null,
    closed bool not null
);

insert or ignore into tasks (id, name, closed) values (0, 'Start learning Pyramid', 0);
insert or ignore into tasks (id, name, closed) values (1, 'Do quick tutorial', 0);
insert or ignore into tasks (id, name, closed) values (2, 'Have some beer!', 0);

Add a few more imports to the top of the tasks.py file as indicated by the emphasized lines.

1
2
3
4
5
6
7
8
import os
import logging
import sqlite3

from pyramid.config import Configurator
from pyramid.events import ApplicationCreated
from pyramid.events import NewRequest
from pyramid.events import subscriber

To make the process of creating the database slightly easier, rather than requiring a user to execute the data import manually with SQLite, we'll create a function that subscribes to a Pyramid system event for this purpose. By subscribing a function to the ApplicationCreated event, for each time we start the application, our subscribed function will be executed. Consequently, our database will be created or updated as necessary when the application is started.

21
22
23
24
25
26
27
28
29
30
31
32
@subscriber(ApplicationCreated)
def application_created_subscriber(event):
    log.warning('Initializing database...')
    with open(os.path.join(here, 'schema.sql')) as f:
        stmt = f.read()
        settings = event.app.registry.settings
        db = sqlite3.connect(settings['db'])
        db.executescript(stmt)
        db.commit()


if __name__ == '__main__':

We also need to make our database connection available to the application. We'll provide the connection object as an attribute of the application's request. By subscribing to the Pyramid NewRequest event, we'll initialize a connection to the database when a Pyramid request begins. It will be available as request.db. We'll arrange to close it down by the end of the request lifecycle using the request.add_finished_callback method.

21
22
23
24
25
26
27
28
29
30
31
32
33
@subscriber(NewRequest)
def new_request_subscriber(event):
    request = event.request
    settings = request.registry.settings
    request.db = sqlite3.connect(settings['db'])
    request.add_finished_callback(close_db_connection)


def close_db_connection(request):
    request.db.close()


@subscriber(ApplicationCreated)

To make those changes active, we'll have to specify the database location in the configuration settings and make sure our @subscriber decorator is scanned by the application at runtime using config.scan().

44
45
46
47
48
49
if __name__ == '__main__':
    # configuration settings
    settings = {}
    settings['reload_all'] = True
    settings['debug_all'] = True
    settings['db'] = os.path.join(here, 'tasks.db')
54
55
56
    # scan for @view_config and @subscriber decorators
    config.scan()
    # serve app

We now have the basic mechanism in place to create and talk to the database in the application through request.db.

Step 4 - View functions and routes

It's now time to expose some functionality to the world in the form of view functions. We'll start by adding a few imports to our tasks.py file. In particular, we're going to import the view_config decorator, which will let the application discover and register views:

 8
 9
10
11
from pyramid.events import subscriber
from pyramid.httpexceptions import HTTPFound
from pyramid.session import UnencryptedCookieSessionFactoryConfig
from pyramid.view import view_config

Note that our imports are sorted alphabetically within the pyramid Python-dotted name which makes them easier to find as their number increases.

We'll now add some view functions to our application for listing, adding, and closing todos.

List view

This view is intended to show all open entries, according to our tasks table in the database. It uses the list.mako template available under the templates directory by defining it as the renderer in the view_config decorator. The results returned by the query are tuples, but we convert them into a dictionary for easier accessibility within the template. The view function will pass a dictionary defining tasks to the list.mako template.

19
20
21
22
23
24
25
26
27
here = os.path.dirname(os.path.abspath(__file__))


# views
@view_config(route_name='list', renderer='list.mako')
def list_view(request):
    rs = request.db.execute('select id, name from tasks where closed = 0')
    tasks = [dict(id=row[0], name=row[1]) for row in rs.fetchall()]
    return {'tasks': tasks}

When using the view_config decorator, it's important to specify a route_name to match a defined route, and a renderer if the function is intended to render a template. The view function should then return a dictionary defining the variables for the renderer to use. Our list_view above does both.

New view

This view lets the user add new tasks to the application. If a name is provided to the form, a task is added to the database. Then an information message is flashed to be displayed on the next request, and the user's browser is redirected back to the list_view. If nothing is provided, a warning message is flashed and the new_view is displayed again. Insert the following code immediately after the list_view.

30
31
32
33
34
35
36
37
38
39
40
41
42
@view_config(route_name='new', renderer='new.mako')
def new_view(request):
    if request.method == 'POST':
        if request.POST.get('name'):
            request.db.execute(
                'insert into tasks (name, closed) values (?, ?)',
                [request.POST['name'], 0])
            request.db.commit()
            request.session.flash('New task was successfully added!')
            return HTTPFound(location=request.route_url('list'))
        else:
            request.session.flash('Please enter a name for the task!')
    return {}

Warning

Be sure to use question marks when building SQL statements via db.execute, otherwise your application will be vulnerable to SQL injection when using string formatting.

Close view

This view lets the user mark a task as closed, flashes a success message, and redirects back to the list_view page. Insert the following code immediately after the new_view.

45
46
47
48
49
50
51
52
@view_config(route_name='close')
def close_view(request):
    task_id = int(request.matchdict['id'])
    request.db.execute('update tasks set closed = ? where id = ?',
                       (1, task_id))
    request.db.commit()
    request.session.flash('Task was successfully closed!')
    return HTTPFound(location=request.route_url('list'))

NotFound view

This view lets us customize the default NotFound view provided by Pyramid, by using our own template. The NotFound view is displayed by Pyramid when a URL cannot be mapped to a Pyramid view. We'll add the template in a subsequent step. Insert the following code immediately after the close_view.

55
56
57
58
@view_config(context='pyramid.exceptions.NotFound', renderer='notfound.mako')
def notfound_view(request):
    request.response.status = '404 Not Found'
    return {}

Adding routes

We finally need to add some routing elements to our application configuration if we want our view functions to be matched to application URLs. Insert the following code immediately after the configuration setup code.

95
96
97
98
    # routes setup
    config.add_route('list', '/')
    config.add_route('new', '/new')
    config.add_route('close', '/close/{id}')

We've now added functionality to the application by defining views exposed through the routes system.

Step 5 - View templates

The views perform the work, but they need to render something that the web browser understands: HTML. We have seen that the view configuration accepts a renderer argument with the name of a template. We'll use one of the templating engines, Mako, supported by the Pyramid add-on, pyramid_mako.

We'll also use Mako template inheritance. Template inheritance makes it possible to reuse a generic layout across multiple templates, easing layout maintenance and uniformity.

Create the following templates in the templates directory with the respective content:

layout.mako

This template contains the basic layout structure that will be shared with other templates. Inside the body tag, we've defined a block to display flash messages sent by the application, and another block to display the content of the page, inheriting this master layout by using the mako directive ${next.body()}.

# -*- coding: utf-8 -*- 
<!DOCTYPE html>  
<html>
<head>
	
  <meta charset="utf-8">
  <title>Pyramid Task's List Tutorial</title>
  <meta name="author" content="Pylons Project">
  <link rel="shortcut icon" href="/static/favicon.ico">
  <link rel="stylesheet" href="/static/style.css">

</head>

<body>

  % if request.session.peek_flash():
  <div id="flash">
    <% flash = request.session.pop_flash() %>
	% for message in flash:
	${message}<br>
	% endfor
  </div>
  % endif

  <div id="page">
    
    ${next.body()}

  </div>
  
</body>
</html>

list.mako

This template is used by the list_view view function. This template extends the master layout.mako template by providing a listing of tasks. The loop uses the passed tasks dictionary sent from the list_view function using Mako syntax. We also use the request.route_url function to generate a URL based on a route name and its arguments instead of statically defining the URL path.

# -*- coding: utf-8 -*- 
<%inherit file="layout.mako"/>

<h1>Task's List</h1>

<ul id="tasks">
% if tasks:
  % for task in tasks:
  <li>
    <span class="name">${task['name']}</span>
    <span class="actions">
      [ <a href="${request.route_url('close', id=task['id'])}">close</a> ]
    </span>
  </li>
  % endfor
% else:
  <li>There are no open tasks</li>
% endif
  <li class="last">
    <a href="${request.route_url('new')}">Add a new task</a>
  </li>
</ul>

new.mako

This template is used by the new_view view function. The template extends the master layout.mako template by providing a basic form to add new tasks.

# -*- coding: utf-8 -*- 
<%inherit file="layout.mako"/>

<h1>Add a new task</h1>

<form action="${request.route_url('new')}" method="post">
  <input type="text" maxlength="100" name="name">
  <input type="submit" name="add" value="ADD" class="button">
</form>

notfound.mako

This template extends the master layout.mako template. We use it as the template for our custom NotFound view.

# -*- coding: utf-8 -*- 
<%inherit file="layout.mako"/>

<div id="notfound">
  <h1>404 - PAGE NOT FOUND</h1>
  The page you're looking for isn't here.
</div>

Configuring template locations

To make it possible for views to find the templates they need by renderer name, we now need to specify where the Mako templates can be found by modifying the application configuration settings in tasks.py. Insert the emphasized lines as indicated in the following.

90
91
92
93
94
95
96
97
98
    settings['db'] = os.path.join(here, 'tasks.db')
    settings['mako.directories'] = os.path.join(here, 'templates')
    # session factory
    session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
    # configuration setup
    config = Configurator(settings=settings, session_factory=session_factory)
    # add mako templating
    config.include('pyramid_mako')
    # routes setup

Step 6 - Styling your templates

It's now time to add some styling to the application templates by adding a CSS file named style.css to the static directory with the following content:

body {
  font-family: sans-serif;
  font-size: 14px;
  color: #3e4349;
}

h1, h2, h3, h4, h5, h6 {
  font-family: Georgia;
  color: #373839;
}

a {
  color: #1b61d6;
  text-decoration: none;
}

input {
  font-size: 14px;
  width: 400px;
  border: 1px solid #bbbbbb;
  padding: 5px;
}

.button {
  font-size: 14px;
  font-weight: bold;
  width: auto;
  background: #eeeeee;
  padding: 5px 20px 5px 20px;
  border: 1px solid #bbbbbb;
  border-left: none;
  border-right: none;
}

#flash, #notfound {
  font-size: 16px;
  width: 500px;
  text-align: center;
  background-color: #e1ecfe;
  border-top: 2px solid #7a9eec;
  border-bottom: 2px solid #7a9eec;
  padding: 10px 20px 10px 20px;
}

#notfound {
  background-color: #fbe3e4;
  border-top: 2px solid #fbc2c4;
  border-bottom: 2px solid #fbc2c4;
  padding: 0 20px 30px 20px;
}

#tasks {
  width: 500px;
}

#tasks li {
  padding: 5px 0 5px 0;
  border-bottom: 1px solid #bbbbbb;
}

#tasks li.last {
  border-bottom: none;
}

#tasks .name {
  width: 400px;
  text-align: left;
  display: inline-block;
}

#tasks .actions {
  width: 80px;
  text-align: right;
  display: inline-block;
}

To cause this static file to be served by the application, we must add a "static view" directive to the application configuration.

101
102
103
104
    config.add_route('close', '/close/{id}')
    # static view setup
    config.add_static_view('static', os.path.join(here, 'static'))
    # scan for @view_config and @subscriber decorators

Step 7 - Running the application

We have now completed all steps needed to run the application in its final version. Before running it, here's the complete main code for tasks.py for review.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
import os
import logging
import sqlite3

from pyramid.config import Configurator
from pyramid.events import ApplicationCreated
from pyramid.events import NewRequest
from pyramid.events import subscriber
from pyramid.httpexceptions import HTTPFound
from pyramid.session import UnencryptedCookieSessionFactoryConfig
from pyramid.view import view_config

from wsgiref.simple_server import make_server


logging.basicConfig()
log = logging.getLogger(__file__)

here = os.path.dirname(os.path.abspath(__file__))


# views
@view_config(route_name='list', renderer='list.mako')
def list_view(request):
    rs = request.db.execute('select id, name from tasks where closed = 0')
    tasks = [dict(id=row[0], name=row[1]) for row in rs.fetchall()]
    return {'tasks': tasks}


@view_config(route_name='new', renderer='new.mako')
def new_view(request):
    if request.method == 'POST':
        if request.POST.get('name'):
            request.db.execute(
                'insert into tasks (name, closed) values (?, ?)',
                [request.POST['name'], 0])
            request.db.commit()
            request.session.flash('New task was successfully added!')
            return HTTPFound(location=request.route_url('list'))
        else:
            request.session.flash('Please enter a name for the task!')
    return {}


@view_config(route_name='close')
def close_view(request):
    task_id = int(request.matchdict['id'])
    request.db.execute('update tasks set closed = ? where id = ?',
                       (1, task_id))
    request.db.commit()
    request.session.flash('Task was successfully closed!')
    return HTTPFound(location=request.route_url('list'))


@view_config(context='pyramid.exceptions.NotFound', renderer='notfound.mako')
def notfound_view(request):
    request.response.status = '404 Not Found'
    return {}


# subscribers
@subscriber(NewRequest)
def new_request_subscriber(event):
    request = event.request
    settings = request.registry.settings
    request.db = sqlite3.connect(settings['db'])
    request.add_finished_callback(close_db_connection)


def close_db_connection(request):
    request.db.close()


@subscriber(ApplicationCreated)
def application_created_subscriber(event):
    log.warning('Initializing database...')
    with open(os.path.join(here, 'schema.sql')) as f:
        stmt = f.read()
        settings = event.app.registry.settings
        db = sqlite3.connect(settings['db'])
        db.executescript(stmt)
        db.commit()


if __name__ == '__main__':
    # configuration settings
    settings = {}
    settings['reload_all'] = True
    settings['debug_all'] = True
    settings['db'] = os.path.join(here, 'tasks.db')
    settings['mako.directories'] = os.path.join(here, 'templates')
    # session factory
    session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
    # configuration setup
    config = Configurator(settings=settings, session_factory=session_factory)
    # add mako templating
    config.include('pyramid_mako')
    # routes setup
    config.add_route('list', '/')
    config.add_route('new', '/new')
    config.add_route('close', '/close/{id}')
    # static view setup
    config.add_static_view('static', os.path.join(here, 'static'))
    # scan for @view_config and @subscriber decorators
    config.scan()
    # serve app
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

And now let's run tasks.py:

$ python tasks.py
WARNING:tasks.py:Initializing database...

It will be listening on port 8080. Open a web browser to the URL http://localhost:8080/ to view and interact with the app.

Conclusion

This introduction to Pyramid was inspired by Flask and Bottle tutorials with the same minimalistic approach in mind. Big thanks to Chris McDonough, Carlos de la Guardia, and Casey Duncan for their support and friendship.