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:
Step 1 - Organizing the project¶
注釈
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 import os
2 import logging
3
4 from pyramid.config import Configurator
5 from pyramid.session import UnencryptedCookieSessionFactoryConfig
6
7 from wsgiref.simple_server import make_server
Then we'll set up logging and the current working directory path.
9 logging.basicConfig()
10 log = logging.getLogger(__file__)
11
12 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 if __name__ == '__main__':
15 # configuration settings
16 settings = {}
17 settings['reload_all'] = True
18 settings['debug_all'] = True
19 # session factory
20 session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
21 # configuration setup
22 config = Configurator(settings=settings, session_factory=session_factory)
23 # serve app
24 app = config.make_wsgi_app()
25 server = make_server('0.0.0.0', 8080, app)
26 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:
1create table if not exists tasks (
2 id integer primary key autoincrement,
3 name char(100) not null,
4 closed bool not null
5);
6
7insert or ignore into tasks (id, name, closed) values (0, 'Start learning Pyramid', 0);
8insert or ignore into tasks (id, name, closed) values (1, 'Do quick tutorial', 0);
9insert 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.
1import os
2import logging
3import sqlite3
4
5from pyramid.config import Configurator
6from pyramid.events import ApplicationCreated
7from pyramid.events import NewRequest
8from 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@subscriber(ApplicationCreated)
22def application_created_subscriber(event):
23 log.warning('Initializing database...')
24 with open(os.path.join(here, 'schema.sql')) as f:
25 stmt = f.read()
26 settings = event.app.registry.settings
27 db = sqlite3.connect(settings['db'])
28 db.executescript(stmt)
29 db.commit()
30
31
32if __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@subscriber(NewRequest)
22def new_request_subscriber(event):
23 request = event.request
24 settings = request.registry.settings
25 request.db = sqlite3.connect(settings['db'])
26 request.add_finished_callback(close_db_connection)
27
28
29def close_db_connection(request):
30 request.db.close()
31
32
33@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()
.
44if __name__ == '__main__':
45 # configuration settings
46 settings = {}
47 settings['reload_all'] = True
48 settings['debug_all'] = True
49 settings['db'] = os.path.join(here, 'tasks.db')
54 # scan for @view_config and @subscriber decorators
55 config.scan()
56 # 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:
8from pyramid.events import subscriber
9from pyramid.httpexceptions import HTTPFound
10from pyramid.session import UnencryptedCookieSessionFactoryConfig
11from 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.
19here = os.path.dirname(os.path.abspath(__file__))
20
21
22# views
23@view_config(route_name='list', renderer='list.mako')
24def list_view(request):
25 rs = request.db.execute('select id, name from tasks where closed = 0')
26 tasks = [dict(id=row[0], name=row[1]) for row in rs.fetchall()]
27 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@view_config(route_name='new', renderer='new.mako')
31def new_view(request):
32 if request.method == 'POST':
33 if request.POST.get('name'):
34 request.db.execute(
35 'insert into tasks (name, closed) values (?, ?)',
36 [request.POST['name'], 0])
37 request.db.commit()
38 request.session.flash('New task was successfully added!')
39 return HTTPFound(location=request.route_url('list'))
40 else:
41 request.session.flash('Please enter a name for the task!')
42 return {}
警告
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@view_config(route_name='close')
46def close_view(request):
47 task_id = int(request.matchdict['id'])
48 request.db.execute('update tasks set closed = ? where id = ?',
49 (1, task_id))
50 request.db.commit()
51 request.session.flash('Task was successfully closed!')
52 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@view_config(context='pyramid.exceptions.NotFound', renderer='notfound.mako')
56def notfound_view(request):
57 request.response.status = '404 Not Found'
58 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 # routes setup
96 config.add_route('list', '/')
97 config.add_route('new', '/new')
98 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()}
.
1# -*- coding: utf-8 -*-
2<!DOCTYPE html>
3<html>
4<head>
5
6 <meta charset="utf-8">
7 <title>Pyramid Task's List Tutorial</title>
8 <meta name="author" content="Pylons Project">
9 <link rel="shortcut icon" href="/static/favicon.ico">
10 <link rel="stylesheet" href="/static/style.css">
11
12</head>
13
14<body>
15
16 % if request.session.peek_flash():
17 <div id="flash">
18 <% flash = request.session.pop_flash() %>
19 % for message in flash:
20 ${message}<br>
21 % endfor
22 </div>
23 % endif
24
25 <div id="page">
26
27 ${next.body()}
28
29 </div>
30
31</body>
32</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.
1# -*- coding: utf-8 -*-
2<%inherit file="layout.mako"/>
3
4<h1>Task's List</h1>
5
6<ul id="tasks">
7% if tasks:
8 % for task in tasks:
9 <li>
10 <span class="name">${task['name']}</span>
11 <span class="actions">
12 [ <a href="${request.route_url('close', id=task['id'])}">close</a> ]
13 </span>
14 </li>
15 % endfor
16% else:
17 <li>There are no open tasks</li>
18% endif
19 <li class="last">
20 <a href="${request.route_url('new')}">Add a new task</a>
21 </li>
22</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.
1# -*- coding: utf-8 -*-
2<%inherit file="layout.mako"/>
3
4<h1>Add a new task</h1>
5
6<form action="${request.route_url('new')}" method="post">
7 <input type="text" maxlength="100" name="name">
8 <input type="submit" name="add" value="ADD" class="button">
9</form>
notfound.mako¶
This template extends the master layout.mako
template. We use it as the
template for our custom NotFound
view.
1# -*- coding: utf-8 -*-
2<%inherit file="layout.mako"/>
3
4<div id="notfound">
5 <h1>404 - PAGE NOT FOUND</h1>
6 The page you're looking for isn't here.
7</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 settings['db'] = os.path.join(here, 'tasks.db')
91 settings['mako.directories'] = os.path.join(here, 'templates')
92 # session factory
93 session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
94 # configuration setup
95 config = Configurator(settings=settings, session_factory=session_factory)
96 # add mako templating
97 config.include('pyramid_mako')
98 # 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:
1body {
2 font-family: sans-serif;
3 font-size: 14px;
4 color: #3e4349;
5}
6
7h1, h2, h3, h4, h5, h6 {
8 font-family: Georgia;
9 color: #373839;
10}
11
12a {
13 color: #1b61d6;
14 text-decoration: none;
15}
16
17input {
18 font-size: 14px;
19 width: 400px;
20 border: 1px solid #bbbbbb;
21 padding: 5px;
22}
23
24.button {
25 font-size: 14px;
26 font-weight: bold;
27 width: auto;
28 background: #eeeeee;
29 padding: 5px 20px 5px 20px;
30 border: 1px solid #bbbbbb;
31 border-left: none;
32 border-right: none;
33}
34
35#flash, #notfound {
36 font-size: 16px;
37 width: 500px;
38 text-align: center;
39 background-color: #e1ecfe;
40 border-top: 2px solid #7a9eec;
41 border-bottom: 2px solid #7a9eec;
42 padding: 10px 20px 10px 20px;
43}
44
45#notfound {
46 background-color: #fbe3e4;
47 border-top: 2px solid #fbc2c4;
48 border-bottom: 2px solid #fbc2c4;
49 padding: 0 20px 30px 20px;
50}
51
52#tasks {
53 width: 500px;
54}
55
56#tasks li {
57 padding: 5px 0 5px 0;
58 border-bottom: 1px solid #bbbbbb;
59}
60
61#tasks li.last {
62 border-bottom: none;
63}
64
65#tasks .name {
66 width: 400px;
67 text-align: left;
68 display: inline-block;
69}
70
71#tasks .actions {
72 width: 80px;
73 text-align: right;
74 display: inline-block;
75}
To cause this static file to be served by the application, we must add a "static view" directive to the application configuration.
101 config.add_route('close', '/close/{id}')
102 # static view setup
103 config.add_static_view('static', os.path.join(here, 'static'))
104 # 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.
1import os
2import logging
3import sqlite3
4
5from pyramid.config import Configurator
6from pyramid.events import ApplicationCreated
7from pyramid.events import NewRequest
8from pyramid.events import subscriber
9from pyramid.httpexceptions import HTTPFound
10from pyramid.session import UnencryptedCookieSessionFactoryConfig
11from pyramid.view import view_config
12
13from wsgiref.simple_server import make_server
14
15
16logging.basicConfig()
17log = logging.getLogger(__file__)
18
19here = os.path.dirname(os.path.abspath(__file__))
20
21
22# views
23@view_config(route_name='list', renderer='list.mako')
24def list_view(request):
25 rs = request.db.execute('select id, name from tasks where closed = 0')
26 tasks = [dict(id=row[0], name=row[1]) for row in rs.fetchall()]
27 return {'tasks': tasks}
28
29
30@view_config(route_name='new', renderer='new.mako')
31def new_view(request):
32 if request.method == 'POST':
33 if request.POST.get('name'):
34 request.db.execute(
35 'insert into tasks (name, closed) values (?, ?)',
36 [request.POST['name'], 0])
37 request.db.commit()
38 request.session.flash('New task was successfully added!')
39 return HTTPFound(location=request.route_url('list'))
40 else:
41 request.session.flash('Please enter a name for the task!')
42 return {}
43
44
45@view_config(route_name='close')
46def close_view(request):
47 task_id = int(request.matchdict['id'])
48 request.db.execute('update tasks set closed = ? where id = ?',
49 (1, task_id))
50 request.db.commit()
51 request.session.flash('Task was successfully closed!')
52 return HTTPFound(location=request.route_url('list'))
53
54
55@view_config(context='pyramid.exceptions.NotFound', renderer='notfound.mako')
56def notfound_view(request):
57 request.response.status = '404 Not Found'
58 return {}
59
60
61# subscribers
62@subscriber(NewRequest)
63def new_request_subscriber(event):
64 request = event.request
65 settings = request.registry.settings
66 request.db = sqlite3.connect(settings['db'])
67 request.add_finished_callback(close_db_connection)
68
69
70def close_db_connection(request):
71 request.db.close()
72
73
74@subscriber(ApplicationCreated)
75def application_created_subscriber(event):
76 log.warning('Initializing database...')
77 with open(os.path.join(here, 'schema.sql')) as f:
78 stmt = f.read()
79 settings = event.app.registry.settings
80 db = sqlite3.connect(settings['db'])
81 db.executescript(stmt)
82 db.commit()
83
84
85if __name__ == '__main__':
86 # configuration settings
87 settings = {}
88 settings['reload_all'] = True
89 settings['debug_all'] = True
90 settings['db'] = os.path.join(here, 'tasks.db')
91 settings['mako.directories'] = os.path.join(here, 'templates')
92 # session factory
93 session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
94 # configuration setup
95 config = Configurator(settings=settings, session_factory=session_factory)
96 # add mako templating
97 config.include('pyramid_mako')
98 # routes setup
99 config.add_route('list', '/')
100 config.add_route('new', '/new')
101 config.add_route('close', '/close/{id}')
102 # static view setup
103 config.add_static_view('static', os.path.join(here, 'static'))
104 # scan for @view_config and @subscriber decorators
105 config.scan()
106 # serve app
107 app = config.make_wsgi_app()
108 server = make_server('0.0.0.0', 8080, app)
109 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.