Getting Started With Pyramid¶
Create a directory to work in.
We’ll be using this directory as the jumping-off point for deploying our learning journals,
so call it something sensible like pyramid_lj
.
Navigate to that directory and create (and activate) a new virtual environment.
Then pip install the most recent versions of pip
, setuptools
, and ipython
.
(pyramid_lj) bash-3.2$ pip install -U pip setuptools
(pyramid_lj) bash-3.2$ pip install ipython
NOTE: YOU WILL NOT PUT YOUR .gitignore, README.md, or LICENSE FILES AT THE SAME LEVEL AS bin THIS TIME. THAT WILL COME LATER WHEN YOU CREATE YOUR SCAFFOLDS.
Installation¶
In order to begin working with Pyramid, we have to install it. We’ll also install the extension that allows Pyramid to interact with iPython:
(pyramid_lj) bash-3.2$ pip install pyramid pyramid_ipython
The version that should be pulled down is the latest version, 1.7.
Note the other packages that get installed along with it, as it has dependencies.
For example, WebOb
provides an HTTP Requeest and Response class, and those you work with in Pyramid inherits from them.
Many other frameworks also use this package.
When it is installed, Pyramid creates a bunch of new shell commands (pcreate
, pshell
, prequest
, etc).
You can see them all in the bin
directory of your virtual environment.
(pyramid_lj) bash-3.2$ ls bin
activate easy_install-3.5 ipython3 pip3 pserve python
activate.csh iptest pcreate pip3.5 pshell python3
activate.fish iptest3 pdistreport prequest ptweens
easy_install ipython pip proutes pviews
Writing a “Hello World” App¶
Source: trypyramid.com.
As is tradition, when using a new bit of technology we test that it works by having it print something like “Hello World”.
This is no different.
Make a directory for your “hello world” app called hello_world
.
Within that directory create a file named app.py
and type the following:
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response("Hello World!")
if __name__ == '__main__':
config = Configurator()
config.add_route('hello', '/')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 6543, app)
server.serve_forever()
Save that file and run the following from the command line:
(pyramid_lj) bash-3.2$ python app.py
Notice how the shell returns nothing. That means that the server you’ve set up through Pyramid is running and listening for requests.
Finally, open http://localhost:6543/ in your browser. This will simply connect you to the port that you told Pyramid to listen to.
This is an almost irresponsibly-simple web app. It proves that the Pyramid framework can handle HTTP requests and generate HTTP responses. We’ll definitely be using Pyramid for significantly more-complicated things. You can see that it is easy to get a simple site up and running with Pyramid. For the more complex stuff, it helps to have some structure set up beforehand.
Using the pcreate
Command to Create a Scaffold¶
The pcreate
command allows us to use a “scaffold” to create a skeleton of code for a web app.
This skeleton includes the basic functionality and reflects the best practices of a Pyramid app.
We’ll use this command to get a start on our own learning journal.
Before using this command, move up by one directory and invoke pcreate
like so:
(pyramid_lj) bash-3.2$ cd ..
(pyramid_lj) bash-3.2$ pcreate -s starter learning_journal_basic
This starter
scaffold will set you up with the base files that you need to run a Pyramid app.
Note that after running, the code generator ends by printing “Sorry for the convenience.”
If you see this line, your skeleton was created just fine.
The entire skeleton will be in the learning_journal_basic
directory that was just created.
Navigate to it and initialize a git repository with .gitignore
, README.md
, and LICENSE
files.
If you use git status
you’ll see all of the new files that were just created in this directory.
We want to make sure we don’t track any .pyc
files or the .DS_Store
file in this directory,
so create a .gitignore
file and add lines to ignore those files.
Add this entire directory to your repository with git add .
.
This project root directory contains a bunch of files. They contain packaging metadata, information for other developers and configuration instructions for our application:
(pyramid_lj) bash-3.2$ tree .
├── CHANGES.txt - here, we can track what changes we make to our app over time
├── MANIFEST.in - controls what files are actually present when we package our stuff together and upload it
├── README.txt - is our README file. If you prefer one in Markdown, edit setup.py accordingly
├── development.ini - discussed later
├── production.ini - discussed later
├── pytest.ini - directs ``pytest`` as to which files to test (presuming any file ending in "``.py``")
├── setup.py - lets our directory become an installable python package
├── .coveragerc - determines which directories get targeted for reports of coverage
Let’s start by inspecting setup.py
.
We can see that this app requires Pyramid, Chameleon
(a templating engine), and a few other packages to work.
It also comes packed ready to install some packages for tests.
Let’s modify it so that it runs with tox
as part of its test suite,
and so that it uses the Jinja2
templating engine (which we’ll get to another time):
# in setup.py
...
requires = [
'pyramid',
'pyramid_chameleon', # <-- DELETE THIS LINE
'pyramid_jinja2',
... # other package dependencies
]
...
tests_require = [
'WebTest >= 1.3.1', # py3 compat
'pytest', # includes virtualenv
'pytest-cov',
'tox', # you have to add this one in
]
...
setup(name='learning_journal_basic',
version='0.0',
... # package metadata
install_requires=requires,
entry_points="""\ # Entry points are ways that we can run our code once it has been installed
[paste.app_factory]
main = learning_journal_basic:main
"""
)
Don’t forget to fill in the appropriate information about author
, author_email
, etc.
Now, let’s install it in editing mode so that the changes we make to this project will be immediately available to us when running the app.
(pyramid_lj) bash-3.2$ pip install -e .
One of the things produced after installing our package is an *.egg-info
directory.
This file is package metadata that should never be versioned.
Let’s modify our .gitignore
to exclude it.
Pyramid is Python¶
Navigate to the learning_journal_basic
directory in your project root and inspect it.
(pyramid_lj) bash-3.2$ ls
__init__.py static templates tests.py views.py
In the __init__.py
file you’ll find a main
function.
This function is the “entry point” for our application.
You can find it registered in setup.py
as a paste.app_factory
.
When you use pserve
to start a web server serving your app, this function is executed.
We’ll have to change a line here to match the templating engine we intend to use (even though we’re not going to use it yet).
1 2 3 4 5 6 7 8 9 10 11 | from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.include('pyramid_jinja2') # <-- this is the line that gets changed.
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
config.scan()
return config.make_wsgi_app()
|
This looks somewhat different from the app.py
file we had created earlier.
The machinery of our framework is now handling some of the stuff we hard coded before.
Let’s look at this in detail.
def main(global_config, **settings):
Configuration is passed into an application after being read from the specified .ini
file (e.g. development.ini
).
The settings come in through, you guessed it, the **settings
kwarg.
The .ini
files contain sections (e.g. [app:main]
) containing name = value
pairs of configuration data.
This data is parsed with the Python ConfigParser module,
which reads the configuration data and returns it as a dictionary.
The name-value pairs in the [app:main]
section of the configuration file are passed in to our app as settings
.
All other information in the configuration file is passed as global_config
.
In the context of our main
function, settings
is a Python dictionary:
{'pyramid.debug_notfound': 'false',
'pyramid.reload_templates': 'true',
'pyramid.default_locale_name': 'en',
...
}
Those settings are used on the next line after the docstring:
config = Configurator(settings=settings)
Here, a Configurator class object is instantiated using the settings for our specific app.
We can also include
configuration from other add-on packages and even other regions of the app we’re inside of.
This allows for including plugin code that changes how Pyramid behaves.
Our app includes configuration from the new package we want to use for templating:
config.include('pyramid_jinja2')
The next line down establishes a directory to hold your static files (css, javascript, images, etc).
config.add_static_view("static", "static", cache_max_age=3600)
The add_static_view
method takes two arguments.
The first is a path to the directory you will use to hold static files, relative to the location of this __init__.py
file.
The second is an initial path segment to be used in URLs.
The latter is used when Pyramid is automatically generating URLs for static files to be served.
The last bit is
config.add_route('home', '/')
config.scan()
return config.make_wsgi_app()
That first line adds a path to your URL of <whatever your domain name is>/
.
The .add_route()
method adds a “route name” to your Pyramid site.
Route names are used to connect the URLs that a client requests to something that produces HTML.
Here, when a client requests <whatever your domain name is>/
, the route named home
will be found.
That name can be used to find some HTML to return.
If instead the second argument was '/new_entry'
, then requesting <whatever your domain name is>/new_entry
would find the home
route.
More on routes shortly.
Lastly config.scan()
finds all configuration and checks it to make sure that there are no problems with how everything is wired together.
Calling config.make_wsgi_app()
builds your Pyramid application and returns it to the framework to be served.
We’ll return to the configuration of our application repeatedly over the next few sessions. For greater detail about configuration in Pyramid, check the configuration chapter of the Pyramid documentation.
Routes and The MVC Controller¶
Let’s go back to thinking for a bit about the Model-View-Controller pattern.
HTTP Request/Response¶
Recall the HTTP server that we built last week. It shows how internet software is driven by the HTTP Request/Response cycle. A client (perhaps a user with a web browser) makes a request. A server receives and handles that request and returns a response. The client receives the response and views it, perhaps making a new request, and so on and so forth.
An HTTP request arrives at a server through the magic of a URL
http://www.codefellows.org/courses/code-401/advanced-software-development-in-python
Let’s break that up into its constituent parts:
http://
:- This part is the protocol, it determines how the request will be sent.
www.codefellows.org
:- This is a domain name. It’s the human-facing address for a server somewhere.
/courses/code-401/advanced-software-development-in-python
:- This part is the path. It serves as a locator for a resource on the server.
In a static website the path identifies a physical location in the server’s file system. Some directory on the server is the home for the web process, and the path is looked up relative to that. Whatever resource (a file, an image, whatever) is located there is returned to the user as a response. If the path leads to a location that doesn’t exist, the server responds with a 404 Not Found error.
In the golden days of yore, this was the only way content was served via HTTP. In today’s world we have dynamic systems, server-side web frameworks like Pyramid. The requests that you send to a server are handled by a software process that assembles a response instead of looking up a physical location. But, we still have URLs, with protocol, domain, and path. What is the role for a path in a process that doesn’t refer to a physical file system?
Routes in Pyramid¶
Most web frameworks now call the path a route, and provide a way of matching routes to the code that will be run to handle requests.
This process is called “dispatch”.
In our Pyramid scaffold, routes are handled as configuration.
As we saw above, they can be configured in the main function in __init__.py
:
# back inside __init__.py
def main(global_config, **settings):
#...
config.add_route('home', '/')
#...
The add_route
method takes a required name
argument for each route added.
Everything else is, to some degree, an optional argument.
Above, we also provide the pattern
that gets appended to the site’s root URL (in this case, “/”).
Anything that we use accessing the specified name
argument in our Pyramid app will be broadcast to the pattern
that we provide.
When a request comes in to a Pyramid application, the framework looks at all the routes that have been configured. One by one, in order, it tries to match the path of the incoming request against the pattern of the route. As soon as a pattern matches the path from the incoming request, that route is used and no further matching is performed. If no route is found that matches, then the framework will automatically generate a 404 Not Found error response.
In a very real sense, the routes defined in an application are the public API. Any route that is present represents something the user can do. Any route that is not present is something the user cannot do.
One can imagine that if we were to build a site with many routes (as we will), it would clutter up this main
function,
causing it to really be handling multiple things instead of being singularly focused (as functions should be).
As a completely hypothetical example:
# a hypothetical __init__.py; DO NOT TYPE THIS
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.include('pyramid_jinja2')
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_static_view('special_styles', 'special_styles', cache_max_age=3600)
config.add_static_view('misc_styles', 'misc_styles', cache_max_age=3600)
config.add_route('home', '/')
config.add_route('about', '/about-me')
config.add_route('create', '/journal/new-entry')
config.add_route('edit', '/journal/edit-entry')
config.add_route('delete', '/journal/delete-entry')
config.add_route('view', '/journal/{id:\d+}')
config.add_route('contact', '/contact-me')
config.add_route('register', '/register')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_route('settings', '/settings')
config.scan()
return config.make_wsgi_app()
Luckily, we can break out our routes and our static views into a routes.py
file in the same directory.
The sole purpose of this file will be to handle all of the routing configuration for our Pyramid site.
We can include the routes into the configuration of __init__.py
by using the include()
method of the Configurator
:
# inside routes.py
def includeme(config):
""" This function adds routes to Pyramid's Configurator """
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
# ...
# inside __init__.py
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.include('pyramid_jinja2')
config.include('.routes')
config.scan()
return config.make_wsgi_app()
Note
The name includeme
for a function that takes a Configurator instance is not just convention.
This is an example of “magic”.
If you provide a dotted path to a Python module to include
, that module must provide a function called includeme
.
We have our route, and so anything we connect to that specific route name will be shown on the home page. However, we do not yet have anything (of substance) to show on that page. We can change all that with Views.
The Pyramid View¶
Let’s imagine that a request has come to our application for the path '/'
.
The framework made a match of that path to a route with the pattern '/'
.
Configuration can connect that route to a view in our application.
Then the view that is connected will be called.
This brings us to the nature of views.
Note
A Pyramid View is a callable that takes request as an argument.
The view can use information from that request to build appropriate data, perhaps using the application’s models (more on that tomorrow). Finally, it returns the data it assembled.
If you recall our hello_world
app, we defined a function named hello_world()
.
It took a request
as an argument and used Pyramid’s Response
object to provide an HTTP response.
If we look inside of the views.py
file provided by Pyramid’s “starter” scaffold, we’ll find something similar.
# views.py
from pyramid.view import view_config
@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
return {'project': 'learning_journal_basic'}
Here, my_view
is the function name, taking a request, and a dictionary is being returned as a response.
This is great and all, but let’s start more simply.
Delete everything in the file and replace it with the following:
# complete code for views.py right now
from pyramid.response import Response
def home_page(request):
return Response("This is my first view!")
def includeme(config):
config.add_view(home_page, route_name='home')
In the includeme
function in this module, we connect this view to our existing home
route.
The add_view
method takes the name of a view callable and the name of a route as arguments.
Finally, we can include this configuration in our main function in __init__.py
:
# __init__.py
#...
def main(global_config, **settings):
# ...
config.include('.views') <-- connects our views
config.scan()
return config.make_wsgi_app()
Now that we’re all wired together, let’s navigate back to our project route and pip install
this Pyramid app.
Do you remember how to do that?
Then, we can use pserve development.ini
to start up a server and investigate the fruits of our labor.
What happens if instead we try to include the text contained within another file?
Let’s set ourselves up for it by creating a file in the same directory called sample.txt
.
(pyramid_lj) bash-3.2$ echo "This is text in an external file." > sample.txt
Now modify the view that we’ve made to read this file into Python, and return that text in the HTTP response object.
# views.py
# ...
import os
HERE = os.path.dirname(__file__)
def home_page(request):
imported_text = open(os.path.join(HERE, 'sample.txt')).read()
return Response(imported_text)
# ...
We don’t just have to work with plain text. Let’s make a new file that contains HTML instead.
(pyramid_lj) bash-3.2$ echo "<h1>This is text in an external file.</h1>" > sample.html
And now modify our view to access this new file
# views.py
# ...
import os
HERE = os.path.dirname(__file__)
def home_page(request):
imported_text = open(os.path.join(HERE, 'sample.html')).read()
return Response(imported_text)
# ...
Re-launch the server and voila, html appears!
Recap¶
Today we got Pyramid working and set up to run a simple “Hello World” app.
We went from there to using Pyramid’s pcreate
command to set up a slightly more complex skeleton using a scaffold,
complete with the files we’d need to start work toward a larger project.
We learned how to connect incoming requests to routes using configuration.
We learned how to write view callables to take in a request and return a response.
We also learned how to use configuration to connect those view callables to routes.
First we used views to simply write a message onto a browser page. We soon saw that we could also use views to display the contents of an external file, and even display HTML within that file.
Tonight you will use views to display your own HTML, complete with whatever CSS styles your project. Tomorrow, we’ll learn about a better way to use Pyramid to serve up HTML via templates, and we’ll begine to write robust tests for our Pyramid app.