This is an example of how to write a WSGI application using WebOb.
WebOb isn’t itself intended to write applications – it is not a web
framework on its own – but it is possible to write applications
using just WebOb.

Of course all the interesting stuff is in that __call__ method.
WebOb lets you ignore some of the details of WSGI, like what
start_response really is. environ is a CGI-like dictionary,
but webob.Request gives an object interface to it.
webob.Response represents a response, and is itself a WSGI
application. Here’s kind of the hello world of WSGI applications
using these objects:

req.params.get('name','World') gets any query string parameter
(like ?name=Bob), or if it’s a POST form request it will look for
a form parameter name. We instantiate the response with the body
of the response. You could also give keyword arguments like
content_type='text/plain' (text/html is the default content
type and 200OK is the default status).

For the wiki application we’ll support a couple different kinds of
screens, and we’ll make our __call__ method dispatch to different
methods depending on the request. We’ll support an action
parameter like ?action=edit, and also dispatch on the method (GET,
POST, etc, in req.method). We’ll pass in the request and expect a
response object back.

Also, WebOb has a series of exceptions in webob.exc, like
webob.exc.HTTPNotFound, webob.exc.HTTPTemporaryRedirect, etc.
We’ll also let the method raise one of these exceptions and turn it
into a response.

One last thing we’ll do in our __call__ method is create our
Page object, which represents a wiki page.

All this together makes:

fromwebobimportRequest,ResponsefromwebobimportexcclassWikiApp(object):...def__call__(self,environ,start_response):req=Request(environ)action=req.params.get('action','view')# Here's where we get the Page domain object:page=self.get_page(req.path_info)try:try:# The method name is action_{action_param}_{request_method}:meth=getattr(self,'action_%s_%s'%(action,req.method))exceptAttributeError:# If the method wasn't found there must be# something wrong with the request:raiseexc.HTTPBadRequest('No such action %r'%action)resp=meth(req,page)exceptexc.HTTPException,e:# The exception object itself is a WSGI application/response:resp=ereturnresp(environ,start_response)

The Page domain object isn’t really related to the web, but it is
important to implementing this. Each Page is just a file on the
filesystem. Our get_page method figures out the filename given
the path (the path is in req.path_info, which is all the path
after the base path). The Page class handles getting and setting
the title and content.

Here’s the method to figure out the filename:

importosclassWikiApp(object):...defget_page(self,path):path=path.lstrip('/')ifnotpath:# The path was '/', the home pagepath='index'path=os.path.join(self.storage_dir)path=os.path.normpath(path)ifpath.endswith('/'):path+='index'ifnotpath.startswith(self.storage_dir):raiseexc.HTTPBadRequest("Bad path")path+='.html'returnPage(path)

Mostly this is just the kind of careful path construction you have to
do when mapping a URL to a filename. While the server may normalize
the path (so that a path like /../../ can’t be requested), you can
never really be sure. By using os.path.normpath we eliminate
these, and then we make absolutely sure that the resulting path is
under our self.storage_dir with ifnotpath.startswith(self.storage_dir):raiseexc.HTTPBadRequest("Badpath").

Here’s the actual domain object:

classPage(object):def__init__(self,filename):self.filename=filename@propertydefexists(self):returnos.path.exists(self.filename)@propertydeftitle(self):ifnotself.exists:# we need to guess the titlebasename=os.path.splitext(os.path.basename(self.filename))[0]basename=re.sub(r'[_-]',' ',basename)returnbasename.capitalize()content=self.full_contentmatch=re.search(r'<title>(.*?)</title>',content,re.I|re.S)returnmatch.group(1)@propertydeffull_content(self):f=open(self.filename,'rb')try:returnf.read()finally:f.close()@propertydefcontent(self):ifnotself.exists:return''content=self.full_contentmatch=re.search(r'<body[^>]*>(.*?)</body>',content,re.I|re.S)returnmatch.group(1)@propertydefmtime(self):ifnotself.exists:returnNoneelse:returnint(os.stat(self.filename).st_mtime)defset(self,title,content):dir=os.path.dirname(self.filename)ifnotos.path.exists(dir):os.makedirs(dir)new_content="""<html><head><title>%s</title></head><body>%s</body></html>"""%(title,content)f=open(self.filename,'wb')f.write(new_content)f.close()

Basically it provides a .title attribute, a .content
attribute, the .mtime (last modified time), and the page can exist
or not (giving appropriate guesses for title and content when the page
does not exist). It encodes these on the filesystem as a simple HTML
page that is parsed by some regular expressions.

None of this really applies much to the web or WebOb, so I’ll leave it
to you to figure out the details of this.

This is an aside for the tutorial, but an important concept. In WSGI,
and accordingly with WebOb, the URL is split up into several pieces.
Some of these are obvious and some not.

An example:

http://example.com:8080/wiki/article/12?version=10

There are several components here:

req.scheme: http

req.host: example.com:8080

req.server_name: example.com

req.server_port: 8080

req.script_name: /wiki

req.path_info: /article/12

req.query_string: version=10

One non-obvious part is req.script_name and req.path_info.
These correspond to the CGI environmental variables SCRIPT_NAME
and PATH_INFO. req.script_name points to the application.
You might have several applications in your site at different paths:
one at /wiki, one at /blog, one at /. Each application
doesn’t necessarily know about the others, but it has to construct its
URLs properly – so any internal links to the wiki application should
start with /wiki.

Just as there are pieces to the URL, there are several properties in
WebOb to construct URLs based on these:

req.host_url: http://example.com:8080

req.application_url: http://example.com:8080/wiki

req.path_url: http://example.com:8080/wiki/article/12

req.path: /wiki/article/12

req.path_qs: /wiki/article/12?version=10

req.url: http://example.com:8080/wiki/article/12?version10

You can also create URLs with
req.relative_url('some/other/page'). In this example that would
resolve to http://example.com:8080/wiki/article/some/other/page.
You can also create a relative URL to the application URL
(SCRIPT_NAME) like req.relative_url('some/other/page',True) which
would be http://example.com:8080/wiki/some/other/page.

As you can see it’s a simple template using the title and the body,
and a link to the edit screen. We copy the template object into a
class method (view_template=VIEW_TEMPLATE) so that potentially a
subclass could override these templates.

tempita.HTMLTemplate is a template that does automatic HTML
escaping. Our wiki will just be written in plain HTML, so we disable
escaping of the content with {{page.content|html}}.

The template should be pretty obvious now. We create a response with
Response(text), which already has a default Content-Type of
text/html.

To allow conditional responses we set resp.last_modified. You can
set this attribute to a date, None (effectively removing the header),
a time tuple (like produced by time.localtime()), or as in this
case to an integer timestamp. If you get the value back it will
always be a datetime object
(or None). With this header we can process requests with
If-Modified-Since headers, and return 304NotModified if
appropriate. It won’t actually do that unless you set
resp.conditional_response to True.

Note

If you subclass webob.Response you can set the class attribute
default_conditional_response=True and this setting will be
on by default. You can also set other defaults, like the
default_charset ("utf8"), or default_content_type
("text/html").

In <formaction="{{req.path_url}}"method="POST"> we submit to
req.path_url; that’s everything but?action=edit. So we are
POSTing right over the view page. This has the nice side effect of
automatically invalidating any caches of the original page. It also
is vaguely RESTful.

We save the last modified time in a hidden mtime field. This way
we can detect concurrent updates. If start editing the page who’s
mtime is 100000, and someone else edits and saves a revision changing
the mtime to 100010, we can use this hidden field to detect that
conflict. Actually resolving the conflict is a little tricky and
outside the scope of this particular tutorial, we’ll just note the
conflict to the user in an error.

From there we just have a very straight-forward HTML form. Note that
we don’t quote the values because that is done automatically by
HTMLTemplate; if you are using something like string.Template
or a templating language that doesn’t do automatic quoting, you have
to be careful to quote all the field values.

We don’t have any error conditions in our application, but if there
were error conditions we might have to re-display this form with the
input values the user already gave. In that case we’d do something
like:

This way we use the value in the request (req.params is both the
query string parameters and any variables in a POST response), but if
there is no value (e.g., first request) then we use the page values.

The form submits to action_view_POST (view is the default
action). So we have to implement that method:

classWikiApp(object):...defaction_view_POST(self,req,page):submit_mtime=int(req.params.get('mtime')or'0')orNoneifpage.mtime!=submit_mtime:returnexc.HTTPPreconditionFailed("The page has been updated since you started editing it")page.set(title=req.params['title'],content=req.params['content'])resp=exc.HTTPSeeOther(location=req.path_url)returnresp

The first thing we do is check the mtime value. It can be an empty
string (when there’s no mtime, like when you are creating a page) or
an integer. int(req.params.get('time')or'0')orNone basically
makes sure we don’t pass "" to int() (which is an error) then
turns 0 into None (0orNone will evaluate to None in Python –
false_valueorother_value in Python resolves to other_value).
If it fails we just give a not-very-helpful error message, using 412PreconditionFailed (typically preconditions are HTTP headers like
If-Unmodified-Since, but we can’t really get the browser to send
requests like that, so we use the hidden field instead).

Note

Error statuses in HTTP are often under-used because people think
they need to either return an error (useful for machines) or an
error message or interface (useful for humans). In fact you can
do both: you can give any human readable error message with your
error response.

One problem is that Internet Explorer will replace error messages
with its own incredibly unhelpful error messages. However, it
will only do this if the error message is short. If it’s fairly
large (4Kb is large enough) it will show the error message it was
given. You can load your error with a big HTML comment to
accomplish this, like "<!--%s-->"%('x'*4000).

You can change the status of any response with resp.status_int=412, or you can change the body of an exc.HTTPSomething with
resp.body=new_body. The primary advantage of using the
classes in webob.exc is giving the response a clear name and a
boilerplate error message.

After we check the mtime we get the form parameters from
req.params and issue a redirect back to the original view page.
303SeeOther is a good response to give after accepting a POST
form submission, as it gets rid of the POST (no warning messages for the
user if they try to go back).

In this example we’ve used req.params for all the form values. If
we wanted to be specific about where we get the values from, they
could come from req.GET (the query string, a misnomer since the
query string is present even in POST requests) or req.POST (a POST
form body). While sometimes it’s nice to distinguish between these
two locations, for the most part it doesn’t matter. If you want to
check the request method (e.g., make sure you can’t change a page with
a GET request) there’s no reason to do it by accessing these
method-specific getters. It’s better to just handle the method
specifically. We do it here by including the request method in our
dispatcher (dispatching to action_view_GET or
action_view_POST).

req.cookies is just a dictionary, and we also delete the cookie if
it is present (so the message doesn’t keep getting set). The
conditional response stuff only applies when there isn’t any
message, as messages are private. Another alternative would be to
display the message with Javascript, like: