Templates

Formats for Web Service Responses

In a web service, you are free to return whatever set of responses formats you so choose; however, plain text, JSON, and HTML are probably the most common

FormatHuman-friendlyComputer-friendly
HTMLYesYes, if structured
JSONNoYes
text/plainYesMaybe

Often, a web service may convey information in more than one format. (Example: /portfolio/process.gv.txt, /portfolio/process.gv.txt?format=raw, /portfolio/process.gv.txt?format=svg; you could also use the Accept: header, but you can't make a permalink this way ) (Another example: /my-service/foo.json, /my-service/foo.txt, /my-service/foo.html, etc.)

python and JSON

JSON == JavaScript Object Notation

Returning JSON in python is easy:

>>> import json
>>> foo = { 'hello': 1, 'foobar': 2, 'fleem': {'nested': True, 'error': None}}
>>> json.dumps(foo)
'{"fleem": {"error": null, "nested": true}, "foobar": 2, "hello": 1}'

In a web service, you would just return this as the response.

In recent versions of python, the json module is part of the standard library. In older versions, you will have to easy_install simplejson

How to support both json and simplejson:

try:
  import json
except ImportError:
  import simplejson as json

You should also add simplejson to the install_requires section of your setup.py

Of course, you can only use json.dumps() to serialize objects that are supported in JSON, such as strings, numbers, dictionaries, arrays, True, False, and None. Try to do this with other objects will result in an error.

>>> import sys
>>> json.dumps(sys.stdin)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/json/__init__.py", line 230, in dumps
    return _default_encoder.encode(obj)
  File "/usr/lib/python2.6/json/encoder.py", line 367, in encode
    chunks = list(self.iterencode(o))
  File "/usr/lib/python2.6/json/encoder.py", line 317, in _iterencode
    for chunk in self._iterencode_default(o, markers):
  File "/usr/lib/python2.6/json/encoder.py", line 323, in _iterencode_default
    newobj = self.default(o)
  File "/usr/lib/python2.6/json/encoder.py", line 344, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <open file '<stdin>', mode 'r' at 0xb78c2020> is not JSON serializable

Loading JSON is doable with json.loads, but it is picky

What is a template?

A template is a prototype of a document that will be rendered with variables (and possibly logic) to create a final document.

How to create a python package

Let's say you have a python file, that you want to package. We'll use a8e.py from last class. But before we start ...

Why bother creating a python package?

If you're just going to run a script from the command line, then you might not want to package it. However, making a real package has certain advantages:

So how do I create one?

PasteScript comes with a basic_package template out of the box. Once you easy_install PasteScript, it should be available to you.

Making a8e.py into a package:

  1. paster create a8e
  2. fill out necessary details
  3. cp /path/to/a8e.py a8e/a8e/ # could also override a8e/a8e/__init__
  4. cd a8e; python setup.py develop # in a virtualenv
  5. your module is now importable:
    >>> from a8e import a8e
    >>> a8e.__file__
    '/home/jhammel/stage/src/a8e/a8e/a8e.py'
    >>> dir(a8e)
    ['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a8e', 'main', 'sys', 'urllib2']
    >>> a8e.a8e('hello')
    'h3o'
    >>> 
        
  6. (yes, that's a lot of a8e's)

The filesystem layout looks something like this:

a8e/
|-- a8e
|   |-- a8e.py
|   `-- __init__.py
|-- setup.cfg
`-- setup.py

(and the setup.cfg is pretty optional)

webob_view: a PasteScript template

It is easier to start a new project from an example.

Installing webob_view:

# create a virtualenv if you haven't
cd webob_view
python setup.py develop

Listing available PasteScript templates:

(stage)> paster create --list-templates
Available templates:
  basic_package:   A basic setuptools-enabled package
  command_script:  pastescript template for creating command line
  applications
  console_script:  pastescript template for creating command line
  applications
  genshi_view:     a simple view with webob + genshi
  paste_deploy:    A web application deployed through paste.deploy
  webob_view:      a simple view with webob

Creating a new project with webob_view :

(stage)> paster create -t webob_view hello
Selected and implied templates:
  webob-view#webob_view  a simple view with webob

Variables:
  egg:      hello
  package:  hello
  project:  hello
Enter description (One-line description of the package) ['']: a
  description
Enter author (Author name) ['']: Jeff Hammel
Enter author_email (Author email) ['']: [email protected]
Enter url (URL of homepage) ['']: http://k0s.org/
Enter port (port to serve paste) ['']: 7654
Creating template webob_view
Creating directory ./hello
  Recursing into +package+
    Creating ./hello/hello/
    Copying __init__.py to ./hello/hello/__init__.py
    Copying dispatcher.py to ./hello/hello/dispatcher.py
    Copying factory.py_tmpl to ./hello/hello/factory.py
    Copying handlers.py to ./hello/hello/handlers.py
  Copying +package+.ini_tmpl to ./hello/hello.ini
  Copying README.txt_tmpl to ./hello/README.txt
  Copying setup.py_tmpl to ./hello/setup.py
Running /home/jhammel/stage/bin/python setup.py egg_info

Serving the created project:

cd hello 
python setup.py develop
(stage)> paster serve hello.ini 
Starting server in PID 2050.
serving on 0.0.0.0:7654 view at http://127.0.0.1:7654

Viewing the page:

> curl http://127.0.0.1:7654
<html><body><form method="post">Hello,
<input type="text" name="name" value="world"/></form></body></html>

Or use your browser!

What does webob_view give you?

When you render the webob_view template, what do you get out of it?

Being a template, you are allowed -- nay, encouraged! -- to alter any of the resultant code (pylons has a similar philosophy)

Web templates

Why web templates?

Python web templates:

Templates are typically used by passing in variables. In python, this is usually a dict.

Most template flavors allow simple display-oriented logic (django's does not)

The bad way of doing things

If you don't use templates, your code ends up looking like this:

def application(environ, start_response):
  """a simple application to alphabetize words"""
  request = Request(environ)
  words = sorted(request.GET.keys())
  variables = { 'title': request.GET.get('title', 'Hello World'),
                'body': '<li>' + '</li><li>'.join(words) + '</li>'
                }
  template = """<html><head><title>%(title)s</title><body><h1>%(title)s</h1><div><ul>%(body)s</ul></div></body></html>""" % variables
  response = Response(content_type='text/html',
                      template)
  return response(environ, start_response)

Genshi templates

Genshi is an XML and text templating language that focuses on robustness and streams

The previous example:

from genshi.template import TemplateLoader
loader = TemplateLoader('/path/to/directory')
def application(environ, start_response):
  """a simple application to alphabetize words"""
  request = Request(environ)
  words = sorted(request.GET.keys())
  variables = { 'title': request.GET.get('title', 'Hello World'),
                'words': words
                }
  template = self.loader.load('alphabetize.html')
  content = template.genereate(**variables).render()
  response = Response(content_type='text/html',
                      content)
  return response(environ, start_response)

The template:

${file('/home/jhammel/mozilla/web/craft/alphabetize.html')}
alphabetize.html

Example: the class homepage

Remember the crazy URL that made the page black and blink weird?

http://k0s.org/mozilla/craft/?show_header=Accept,Host,Unicorn&blink=true&black#end

This is all done with a genshi template. The WebOb Request object is passed in. From there, the template does the rest.

${file('/home/jhammel/mozilla/web/craft/index.html')}

http://k0s.org/mozilla/craft/index.html?format=raw

genshi_view: webob_view with Genshi templates

genshi_view extends webob_view with genshi templates

Other features over webob_view:

You may not need these for every project, but they're easy to delete and they might be useful

Yes, this is a system of (web) templates in a (file) template

Genshi example project: SimpleWiki

Steps:
  1. invoke the genshi_view template
  2. add a genshi template renderer
  3. add directory listings
  4. add a POST request handler
  5. add an edit view

Step: Invoke genshi_view

(stage)> paster create -t genshi_view SimpleWiki
Selected and implied templates:
  genshi-view#genshi_view  a simple view with webob + genshi

Variables:
  egg:      SimpleWiki
  package:  simplewiki
  project:  SimpleWiki
Enter description (One-line description of the package) ['']: an example wiki with genshi
Enter author (Author name) ['']: Jeff Hammel
Enter author_email (Author email) ['']: [email protected]
Enter url (URL of homepage) ['']: http://k0s.org/mozilla/craft/
Enter port (port to serve paste) ['']: 12345
Creating template genshi_view
Creating directory ./SimpleWiki
  Recursing into +package+
    Creating ./SimpleWiki/simplewiki/
    Copying __init__.py to ./SimpleWiki/simplewiki/__init__.py
    Copying dispatcher.py to ./SimpleWiki/simplewiki/dispatcher.py
    Copying factory.py_tmpl to ./SimpleWiki/simplewiki/factory.py
    Copying handlers.py to ./SimpleWiki/simplewiki/handlers.py
    Recursing into static
      Creating ./SimpleWiki/simplewiki/static/
      Copying jquery.js to ./SimpleWiki/simplewiki/static/jquery.js
    Recursing into templates
      Creating ./SimpleWiki/simplewiki/templates/
      Copying index.html to ./SimpleWiki/simplewiki/templates/index.html
      Copying navigation.html to ./SimpleWiki/simplewiki/templates/navigation.html
  Copying +package+.ini_tmpl to ./SimpleWiki/simplewiki.ini
  Copying README.txt_tmpl to ./SimpleWiki/README.txt
  Copying setup.py_tmpl to ./SimpleWiki/setup.py
Running /home/jhammel/stage/bin/python setup.py egg_info
(stage)> cd SimpleWiki/
(stage)> python setup.py develop
running develop
running egg_info
writing requirements to SimpleWiki.egg-info/requires.txt
writing SimpleWiki.egg-info/PKG-INFO
writing top-level names to SimpleWiki.egg-info/top_level.txt
writing dependency_links to SimpleWiki.egg-info/dependency_links.txt
writing entry points to SimpleWiki.egg-info/entry_points.txt
writing requirements to SimpleWiki.egg-info/requires.txt
writing SimpleWiki.egg-info/PKG-INFO
writing top-level names to SimpleWiki.egg-info/top_level.txt
writing dependency_links to SimpleWiki.egg-info/dependency_links.txt
writing entry points to SimpleWiki.egg-info/entry_points.txt
reading manifest file 'SimpleWiki.egg-info/SOURCES.txt'
writing manifest file 'SimpleWiki.egg-info/SOURCES.txt'
running build_ext
Creating /home/jhammel/stage/lib/python2.6/site-packages/SimpleWiki.egg-link (link to .)
Adding SimpleWiki 0.0 to easy-install.pth file

Installed /home/jhammel/stage/src/SimpleWiki
Processing dependencies for SimpleWiki==0.0
Searching for Genshi==0.6
Best match: Genshi 0.6
Processing Genshi-0.6-py2.6.egg
Genshi 0.6 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/Genshi-0.6-py2.6.egg
Searching for PasteScript==1.7.3
Best match: PasteScript 1.7.3
Processing PasteScript-1.7.3-py2.6.egg
PasteScript 1.7.3 is already the active version in easy-install.pth
Installing paster script to /home/jhammel/stage/bin
Installing paster script to /home/jhammel/stage/bin

Using /home/jhammel/stage/lib/python2.6/site-packages/PasteScript-1.7.3-py2.6.egg
Searching for Paste==1.7.3.1
Best match: Paste 1.7.3.1
Processing Paste-1.7.3.1-py2.6.egg
Paste 1.7.3.1 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/Paste-1.7.3.1-py2.6.egg
Searching for WebOb==0.9.8
Best match: WebOb 0.9.8
Processing WebOb-0.9.8-py2.6.egg
WebOb 0.9.8 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/WebOb-0.9.8-py2.6.egg
Searching for PasteDeploy==1.3.3
Best match: PasteDeploy 1.3.3
Processing PasteDeploy-1.3.3-py2.6.egg
PasteDeploy 1.3.3 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/PasteDeploy-1.3.3-py2.6.egg
Finished processing dependencies for SimpleWiki==0.0
(stage)>

Step: add a Genshi template renderer

${file('/home/jhammel/mozilla/web/craft/simplewiki-patches/renderer')}
changes to add renderer

diff format

Step: add a directory index handler

${file('/home/jhammel/mozilla/web/craft/simplewiki-patches/index')}
changes to add index handler

Step: add a POST handler

${file('/home/jhammel/mozilla/web/craft/simplewiki-patches/post')}
changes to add POST handler

Step: add a file server

Add a FileApp handler for other content

${file('/home/jhammel/mozilla/web/craft/simplewiki-patches/fileserver')}
changes to add file server

Step: add an edit view

${file('/home/jhammel/mozilla/web/craft/simplewiki-patches/edit')}
changes to add edit view

SimpleWiki: next steps

The code lives here: /hg/SimpleWiki

Defects:

Features:

The point is, its not hard to build an app this way. SimpleWiki is an example application an something to build on, not a comprehensive solution as-is.

See also cousin decoupage

Web services vs. web frameworks, revisited

In order to make a meaningful web service, it must be independent of other web services

Note that each of the handlers is a distinct web service. They don't talk to each other and have standalone functionality.

How you would do this in pylons:

How we could improve this in SimpleWiki: