Table of Contents

Introduction

Bobo is a light-weight framework for creating WSGI web applications.

Its goal is to be easy to learn and remember.

It provides 2 features:

  • Mapping URLs to objects
  • Calling objects to generate HTTP responses

It doesn’t have a templating language, a database integration layer, or a number of other features that can be provided by WSGI middle-ware or application-specific libraries.

Bobo builds on other frameworks, most notably WSGI and WebOb.

Installation

Bobo can be installed in the usual ways, including using the setup.py install command. You can, of course, use Easy Install, Buildout, or pip.

To use the setup.py install command, download and unpack the source distribution and run the setup script:

python setup.py install

To run bobo’s tests, just use the test command:

python setup.py test

You can do this before or after installation.

Bobo works with Python 2.4, 2.5, and 2.6. Python 3.0 support is planned. Of course, when using Python 2.4 and 2.5, class decorator syntax can’t be used. You can still use the decorators by calling them with a class after a class is created.

Getting Started

Let’s create a minimal web application, “hello world”. We’ll put it in a file named “hello.py”:

import bobo

@bobo.query
def hello():
    return "Hello world!"

This application creates a single web resource, “/hello.html”, that simply outputs the text “Hello world”.

Bobo decorators, like bobo.query used in the example above control how URLs are mapped to objects. They also control how functions are called and returned values converted to web responses. If a function returns a string, it’s assumed to be HTML and used to construct a response. You can control the content type used by passing a content_type keyword argument to the decorator.

Let’s try out our application. Assuming that bobo’s installed, you can run the application on port 8080 using [1]:

bobo -f hello.py

This will start a web server running on localhost port 8080. If you visit:

http://localhost:8080/hello.html

you’ll get the greeting:

Hello world!

The URL we used to access the application was determined by the name of the resource function and the content type used by the decorator, which defaults to “text/html; charset=UTF-8”. Let’s change the application so we can use a URL like:

http://localhost:8080/

We’ll do this by providing a URL path:

@bobo.query('/')
def hello():
    return "Hello world!"

Here, we passed a path to the query decorator. We used a ‘/’ string, which makes a URL like the one above work. (We also omitted the import for brevity.)

We don’t need to restart the server to see our changes. The bobo development server automatically reloads the file if it changes.

As its name suggests, the query decorator is meant to work with resources that return information, possibly using form data. Let’s modify the application to allow the name of the person to greet to be given as form data:

@bobo.query('/')
def hello(name="world"):
    return "Hello %s!" % name

If a function accepts named arguments, then data will be supplied from form data. If we visit:

http://localhost:8080/?name=Sally

We’ll get the output:

Hello Sally!

The query decorator will accept GET, POST and HEAD requests. It’s appropriate when server data aren’t modified. To accept form data and modify data on a server, you should use the post decorator. The post decorator works like the query decorator except that it only allows POST requests and won’t pass data provided in a query string as function arguments.

@bobo.post('/')
def hello(name="world"):
    return "Hello %s!" % name

@bobo.put('/')
def put_hello(name="world"):
    return "Hello %s?" % name

The query and post decorators are convenient when you want to just get user input passed as function arguments. If you want a bit more control, you can also get the request object by defining a bobo_request parameter:

@bobo.query('/')
def hello(bobo_request, name="world"):
    return "Hello %s! (method=%s)" % (name, bobo_request.method)

The request object gives full access to all of the form data, as well as other information, such as cookies and input headers.

The query and post decorators introspect the function they’re applied to. This means they can’t be used with callable objects that don’t provide function meta data. There’s a low-level decorator, resource that does no introspection and can be used with any callable:

@bobo.resource('/')
def hello(request):
    name = request.params.get('name', 'world!')
    return "Hello %s!" % name

The resource decorator always passes the request object as the first positional argument to the callable it’s given.

Automatic response generation

The resource(), post(), and query() decorators provide automatic response generation when the value returned by an application isn’t a response object. The generation of the response is controlled by the content type given to the content_type decorator parameter.

If an application returns a string, then a response is constructed using the string with the content type.

If an application doesn’t return a response or a string, then the handling depends on whether or not the content type is 'application/json. For 'application/json, the returned value is marshalled to JSON using the json (or simplejson) module, if present. If the module isn’t importable, or if marshaling fails, then an exception will be raised.

If an application returns a unicode string and the content type isn’t 'application/json', the string is encoded using the character set given in the content_type, or using the UTF-8 encoding, if the content type doesn’t include a charset parameter.

If an application returns a non-response non-string result and the content type isn’t 'application/json', then an exception is raised.

If an application wants greater control over a response, it will generally want to construct a webob.Response object and return that.

Routes

We saw earlier that we could control the URLs used to access resources by passing a path to a decorator. The path we pass can specify a multi-level URL and can have placeholders, which allow us to pass data to the resource as part of the URL.

Here, we modify the hello application to let us pass the name of the greeter in the URL:

@bobo.query('/greeters/:myname')
def hello(name="world", myname='Bobo'):
    return "Hello %s! My name is %s." % (name, myname)

Now, to access the resource, we use a URL like:

http://localhost:8080/greeters/myapp?name=Sally

for which we get the output:

Hello Sally! My name is myapp.

We call these paths routes because they use a syntax inspired loosely by the Ruby on Rails Routing system.

You can have any number of placeholders or constant URL paths in a route. The values associated with the placeholders will be made available as function arguments.

If a placeholder is followed by a question mark, then the route segment is optional. If we change the hello example:

@bobo.query('/greeters/:myname?')
def hello(name="world", myname='Bobo'):
    return "Hello %s! My name is %s." % (name, myname)

we can use the URL:

http://localhost:8080/greeters?name=Sally

for which we get the output:

Hello Sally! My name is Bobo.

Note, however, if we use the URL:

http://localhost:8080/greeters/?name=Sally

we get the output:

Hello Sally! My name is .

Placeholders must be legal Python identifiers. A placeholder may be followed by an extension. For example, we could use:

@bobo.query('/greeters/:myname.html')
def hello(name="world", myname='Bobo'):
    return "Hello %s! My name is %s." % (name, myname)

Here, we’ve said that the name must have an ”.html” suffix. To access the function, we use a URL like:

http://localhost:8080/greeters/myapp.html?name=Sally

And get:

Hello Sally! My name is myapp.

If the placeholder is optional:

@bobo.query('/greeters/:myname?.html')
def hello(name="world", myname='Bobo'):
    return "Hello %s! My name is %s." % (name, myname)

Then we can use a URL like:

http://localhost:8080/greeters?name=Sally

or:

http://localhost:8080/greeters/jim.html?name=Sally

Subroutes

Sometimes, you want to split URL matching into multiple steps. You might do this to provide cleaner abstractions in your application, or to support more flexible resource organization. You can use the subroute decorator to do this. The subroute decorator decorates a callable object that returns a resource. The subroute uses the given route to match the beginning of the request path. The resource returned by the callable is matched against the remainder of the path. Let’s look at an example:

import bobo

database = {
   '1': dict(
        name='Bob',
        documents = {
          'hi.html': "Hi. I'm Bob.",
          'hobbies': {
            'cooking.html': "I like to cook.",
            'sports.html': "I like to ski.",
            },
          },
        ),
}

@bobo.subroute('/employees/:employee_id', scan=True)
class Employees:

    def __init__(self, request, employee_id):
        self.employee_id = employee_id
        self.data = database[employee_id]

    @bobo.resource('')
    def base(self, request):
        return bobo.redirect(request.url+'/')

    @bobo.query('/')
    @bobo.query('/summary.html')
    def summary(self):
        return """
        id: %s
        name: %s
        See my <a href="documents">documents</a>.
        """ % (self.employee_id, self.data['name'])

    @bobo.query('/details.html')
    def details(self):
        "Show employee details"
        # ...

    @bobo.post('/update.html')
    def add(self, name, phone, fav_color):
        "Update employee data"
        # ...

    @bobo.subroute
    def documents(self, request):
        return Folder(self.data['documents'])

With this example, if we visit:

http://localhost:8080/employees/1/summary.html

We’ll get the summary for a user. The URL will be matched in 2 steps. First, the path /employees/1 will match the subroute. The class is called with the request and employee id. Then the routes defined for the individual methods are searched. The remainder of the path, /summary.html, matches the route for the summary method. (Note that we provided two decorators for the summary method, which allows us to get to it two ways.) The methods were scanned for routes because we used the scan keyword argument.

The base method has a route that is an empty string. This is a special case that handles an empty path after matching a subroute. The base method will be called for a URL like:

http://localhost:8080/employees/1

which would redirect to:

http://localhost:8080/employees/1/

The documents method defines another subroute. Because we left off the route path, the method name is used. This returns a Folder instance. Let’s look at the Folder class:

@bobo.scan_class
class Folder:

    def __init__(self, data):
        self.data = data

    @bobo.query('')
    def base(self, bobo_request):
        return bobo.redirect(bobo_request.url+'/')

    @bobo.query('/')
    def index(self):
        return '\n'.join('<a href="%s">%s<a><br>' % (k, k)
                         for k in self.data)

    @bobo.subroute('/:item_id')
    def subitem(self, request, item_id):
        item = self.data[item_id]
        if isinstance(item, dict):
           return Folder(item)
        else:
           return Document(item)

@bobo.scan_class
class Document:

    def __init__(self, text):
        self.text = text

    @bobo.query('')
    def get(self):
        return self.text

The Folder and Document classes use the scan_class decorator. The scan_class class decorator scans a class to make routes defined for it’s methods available. Using the scan_class decorator is equivalent to using the scan keyword with subroute decorator [2]. Now consider a URL:

http://localhost:8080/employees/1/documents/hobbies/sports.html

which outputs:

I like to ski.

The URL is matched in multiple steps:

  1. The path /employees/1 matches the Employees class.
  2. The path /documents matches the documents method, which returns a Folder using the employee documents dictionary.
  3. The path /hobbies matches the subitem method of the Folder class, which returns the hobbies dictionary from the documents folder.
  4. The path /sports.html also matches the subitem Folder method, which returns a Document using the text for the sports.html key.

5, The empty path matches the get method of the Document class.

Of course, the employee document tree can be arbitrarily deep.

The subroute decorator can be applied to any callable object that takes a request and route data and returns a resource.

Methods and REST

When we define a resource, we can also specify the HTTP methods it will handle. The resource and query decorators will handle GET, HEAD and POST methods by default. The post decorator handles POST and PUT methods. You can specify one or more methods when using the resource, query, and post decorators:

@bobo.resource(method='GET')
def hello(who='world'):
    return "Hello %s!" % who

@bobo.resource(method=['GET', 'HEAD'])
def hello2(who='world'):
    return "Hello %s!" % who

In addition, there are other decordators, get, head, put, delete, and options that define resources that accept the corresponding HTTP methods.

If multiple resources (resource, query, or post) in a module or class have the same route strings, the resource used will be selected based on both the route and the methods allowed. (If multiple resources match a request, the first one defined will be used [3].)

@bobo.subroute('/employees/:employeeid')
class Employee:

    def __init__(self, request, employee_id):
        self.request = request
        self.id = employee_id

    @bobo.resource('', 'PUT')
    def put(self, request):
        "Save employee data"

    @bobo.post('')
    def new_employee(self):
        "Add an employee"

    @bobo.query('', 'GET')
    def get(self, request):
        "Get employee data"

    @bobo.resource('/resume', 'PUT')
    def save_resume(self, request):
        "Save employee data"

    @bobo.query('/resume')
    def resume(self):
        "Save employee data"

The ability to provide handlers for specific methods provides support for the REST architectural style.

JSON Request Bodies

If you use a JSON request body, with content type application/json, defining a JSON object, bobo will pass properties from the JSON body as resource function arguments.

Beyond the bobo development server

The bobo server makes it easy to get started. Just run it with a source file and off you go. When you’re ready to deploy your application, you’ll want to put your source code in an importable Python module (or package). Bobo publishes modules, not source files. The bobo server provides the convenience of converting a source file to a module.

The bobo command-line server is convenient for getting started, but production applications will usually be configured with selected servers and middleware using Paste Deployment. Bobo includes a Paste Deployment application implementation. To use bobo with Paste Deployment, simply define an application section using the bobo egg:

[app:main]
use = egg:bobo
bobo_resources = helloapp
bobo_configure = helloapp:config
employees_database = /home/databases/employees.db

[server:main]
use = egg:Paste#http
host = localhost
port = 8080

In this example, we’re using the HTTP server that is built into Paste.

The application section (app:main) contains bobo options, as well as application-specific options. In this example, we used the bobo_resources option to specify that we want to use resources found in the helloapp module, and the bobo_configure option to specify a configuration handler to be called with configuration data.

You can put application-specific options in the application section, which can be used by configuration handlers. You can provide one or more configuration handlers using the bobo_configure option. Each configuration handler is specified as a module name and global name [4] separated by a colon.

Configuration handlers are called with a mapping object containing options from the application section and from the DEFAULT section, if present, with application options taking precedence.

To start the server, you’ll run the paster script installed with PasteScript and specify the name of your configuration file:

paster serve app.ini

You’ll need to install Paste Script to use bobo with Paste Deployment.

See Assembling and running the example with Paste Deployment and Paste Script for a complete example.

[1]You can use the -p option to control the port used. To find out more about the bobo server, use the -h option or see The bobo server.
[2]You might be wondering why we require the scan keyword in the subroute decorator to scan methods for resources. The reason is that scan_class is somewhat invasive. It adds a instance method to the class, which may override an existing method. This should not be done implicitly.
[3]More precisely, the resource with the lowest order will be used. By default, a resources order is determined by the order of definition. You can override the order by passing an order keyword argument to a decorator. See Ordering Resources.
[4]The name can be any Python expression that doesn’t contain spaces. It will be evaluated using the module globals.

Questions and bug reports

If you have questions, or want to discuss bobo, use the bobo mailing list. Send email to mailto:bobo-web@googlegroups.com.

Report bugs using the bobo bug tracker at Launchpad.

Additional topics

Check functions

When using the query, post, and resource decorators, you can define a check function. Before calling the decorated function, the check function is called. If the check function returns a response, the check function’s response is used rather than calling the decorated function. A common use of check functions is for authorization:

import bobo, webob

data = {'x': 'some text'}

def authenticated(inst, request, func):
    if not request.remote_user:
        return webob.Response(status=401)

@bobo.post('/:name', check=authenticated)
def update(name, body):
    data[name] = body
    return 'Updated'

In this example, we use a very simple authorization model. We can update data if the user is authenticated. Check functions take 3 positional arguments:

  • an instance
  • a request
  • the decorated function (or callable)

If a resource is a method, the first argument passed to the check function will be the instance the method is applied to. Otherwise, it will be None.

Decorated objects can be used directly

Functions or callables decorated by the query, post, resource and subroute decorators can be called as if they were undecorated. For example, with:

@bobo.query('/:name', check=authenticated)
def get(name):
    return data[name]

We can call the get function directly:

>>> get('x')
'some text'

Similarly, classes decorated with the subroute decorator can be used normally. The subroute decorator simply adds a bobo_response class method that allows the class to be used as a resource.

Configured routes

For simplicity, you normally specify routes in your application code. For example, in:

@bobo.query('/greeters/:myname')
def hello(name="world", myname='Bobo'):
    return "Hello %s! My name is %s." % (name, myname)

You specify 2 things:

  1. Which URLs should be handled by the hello function.
  2. How to call the function.

In most cases, being able to specify this information one place is convenient.

Sometimes, however, you may want to separate routes from your implementation to:

  • Manage the routes in one place,
  • Omit some routes defined in the implementation,
  • Change the routes or search order from what’s given in the implementation.

Bobo provides a way to explicitly configure the routes as part of configuration. When you specify resources, you can control the order resources are searched and override the routes used.

The bobo_resources option takes a number of resources separated by newlines. Resources take one of 4 forms:

modulename
Use all of the resources found in the module.
modulename:expression
Use the given :term:resource. The resource is specified using a module name and an expression (typically just a global name) that’s executed in the module’s global scope.
route -> modulename:expression

Use the given object with the given route. The object is specified using a module name and an expression (typically just a global name) that’s executed in the module’s global scope.

The object must have a bobo_route method, as objects created using one of the query, post, resource or subroute decorators do, or the object must be a class with a constructor that takes a request and route data and returns a resource.

route +> modulename:expression

Use a resource, but add the given route as a prefix of the resources route. The resource is given by a module name and expression.

The given route may not have placeholders.

Resources are separated by newlines. The string ->, or +> at the end of a line acts as a line continuation character.

To show how this works, we’ll look at an example. We’ll create a 2 modules with some resources in them. First, people:

import bobo

@bobo.subroute('/employee/:id', scan=True)
class Employee:
    def __init__(self, request, id):
        self.id = id

    @bobo.query('/')
    def hi(self):
        return "Hi, I'm employee %s" % self.id

@bobo.query('/:name')
def hi(name):
    return "Hi, I'm %s" % name

Then docs:

import bobo

documents = {
    'bobs': {
    'hi.html': "Hi. I'm Bob.",
    'hobbies': {
      'cooking.html': "I like to cook.",
      'sports.html': "I like to ski.",
      },
    },
}

@bobo.subroute('/docs', scan=True)
class Folder:

    def __init__(self, request, data=None):
        if data is None:
            data = documents
        self.data = data

    @bobo.query('')
    def base(self, bobo_request):
        return bobo.redirect(bobo_request.url+'/')

    @bobo.query('/')
    def index(self):
        return '\n'.join('<a href="%s">%s<a><br>' % (k, k)
                         for k in sorted(self.data))

    @bobo.subroute('/:item_id')
    def subitem(self, request, item_id):
        item = self.data[item_id]
        if isinstance(item, dict):
           return Folder(request, item)
        else:
           return Document(item)

@bobo.scan_class
class Document:

    def __init__(self, text):
        self.text = text

    @bobo.query('')
    def get(self):
        return self.text

We use the bobo_resources option to control the URLs we access these with:

[app:main]
use = egg:bobo
bobo_resources =
    # Same routes
    people:Employee # 1
    docs            # 2

    # new routes
    /long/winded/path/:name/lets/get/on/with/it -> # 3
       people:hi                                   # 3 also
    /us/:id -> people:Employee  # 4

    # prefixes
    /folks +> people # 5
    /ho +> people:hi # 6

This example shows a number of things:

  • We can use blank lines and comments. Route configurations can get involved, so comments are useful. In the example, comments are used to assign numbers to the individual routes so we can refer to them.

  • We have several form of resource:

    1. Use an existing resource with its original route.

      If we use a URL like:

      http://localhost:8080/employee/1/
      

      We’ll get output:

      Hi, I'm employee 1
      
    2. Use the resources from a module with their original routes.

      If we use a URL like:

      http://localhost:8080/docs/bobs/hi.html
      

      We’ll get output:

      Hi. I'm Bob.
      
    3. Define a new route for an existing resource.

      If we use a URL like:

      http://localhost:8080/long/winded/path/bobo/lets/get/on/with/it
      

      We’ll get output:

      Hi, I'm bobo
      
    4. Define a new route for an existing subroute.

      If we use a URL like:

      http://localhost:8080/us/1/
      

      We’ll get output:

      Hi, I'm employee 1
      
    5. Use all of the routes from a module with a prefix added.

      If we use a URL like:

      http://localhost:8080/folks/employee/1/
      

      We’ll get output:

      Hi, I'm employee 1
      
    6. Use an existing route adding a prefix.

      If we use a URL like:

      http://localhost:8080/ho/silly
      

      We’ll get output:

      Hi, I'm silly
      

Configuring routes in python

To configure routes in Python, you can use the bobo.resources function:

import bobo

myroutes = bobo.resources((
    # Same routes
    'people:Employee', # 1
    'docs',            # 2

    # new routes
    bobo.reroute(
      '/long/winded/path/:name/lets/get/on/with/it', # 3
      'people:hi'),                                  # 3 also
    bobo.reroute('/us/:id', 'people:Employee'),  # 4

    # prefixes
    bobo.preroute('/folks', 'people'), # 5
    bobo.preroute('/ho', 'people:hi'), # 6
))

The resources function takes an iterable of resources, where the resources can be resource objects, or strings naming resource objects or modules.

The reroute function takes a route and an existing resource and returns a new resource with the given route. The resource must have a bobo_route method, as resources created using one of the query, post, resource or subroute decorators do, or the resource must be a class with a constructor that takes a request and route data and returns a resource.

The preroute function takes a route and a resource and returns a new resource that uses the given route as a subroute to get to the resource.

The example above is almost equivalent to the earlier example. If the module containing the code above is given to the bobo_resources option, then the resources defined by the call will be used. It is slightly different from the earlier example, because if the module defines any other resources, they’ll be used as well.

Resource modules

Rather than defining a resource in a module, we can make a module a resource by defining a bobo_response module attribute:

import bobo, docs, people

bobo_response = bobo.resources((
    # Same routes
    people.Employee, # 1
    docs,            # 2

    # new routes
    bobo.reroute(
      '/long/winded/path/:name/lets/get/on/with/it', # 3
      people.hi),                                    # 3 also
    bobo.reroute('/us/:id', people.Employee),  # 4

    # prefixes
    bobo.preroute('/folks', people), # 5
    bobo.preroute('/ho', people.hi), # 6

)).bobo_response

Here, rather than adding a new resource to the module, we’ve copied the bobo_response method from a new resource to the module, making the module a resource. When bobo scans a module, it first checks whether the module has a bobo_response attribute. If it does, then bobo uses the module as a resource and doesn’t scan the module for resources. This way, we control precisely which resources will be used, given the module.

This example also illustrates that, rather than passing strings to the resources, reroute and preroute functions, we can pass objects directly.

Creating bobo-based WSGI applications from Python

Usually, bobo applications are created using the bobo development server or through a PasteDeployment configuration. You can also create applications in Python using the bobo.Application constructor. You call the constructor with keyword arguments:

bobo_resources

The bobo resources to be used in the application

This is either a string defining resources, or an iterable of modules or resource objects.

bobo_configuration

A list of configuration functions.

This is either a string consistning whitespace-delimited list of configuration callable names, or an iterable of callables. The callables will be called with the keyword arguments passed to bobo.Application. This allows you to pass configuration options when defining an application.

bobo_errors
A custom error-handler object. This is either a string name, of the form 'modulename:expression', or a Python object defining one or more of the error handling functions.
bobo_handle_exceptions
A boolean flag indicating whether bobo should handle uncaught application exceptions. If set to False or 'false', then bobo won’t catch exceptions. This is useful if you want middleware to handle exceptions.

Here’s a somewhat contrived example that illustrates creating an application object from Python, passing objects rather than strings:

import bobo, webob

def config(config):
    global configured_name
    configured_name = config['name']

@bobo.get('/hi')
def hi():
    return configured_name

class Errors:
    @classmethod
    def not_found(self, request, method):
        return webob.Response("missing", 404)

app = bobo.Application(
    bobo_resources=[hi],
    bobo_configure=[config],
    bobo_errors=Errors,
    name="welcome",
    )

Error response generation

There are four cases for which bobo has to generate error responses:

  1. When a resource can’t be found, bobo generates a “404 Not Found” response.
  2. When a resource can be found but it doesn’t allow the request method, bobo generates a “405 Method Not Allowed” response.
  3. When a query or post decorated function requires a parameter and the parameter is isn’t in the given form data, bobo generates a “403 Forbidden” response with a body that indicates the missing parameter.
  4. When a route handler raises an exception, bobo generates a “500 Internal Server Error” response.

For each of these responses, bobo generates a small HTML body.

Applications can take over generating error responses by specifying a bobo_errors option that specified an object or a module defining 3 callable attributes:

not_found(request, method)

Generate a response when a resource can’t be found.

This should return a 404 response.

method_not_allowed(request, method, methods)

Generate a response when the resource found doesn’t allow the request method.

This should return a 405 response and set the Allowed response header to the list of allowed headers.

missing_form_variable(request, method, name)

Generate a response when a form variable is missing.

The proper response in this situation isn’t obvious.

The value given for the bobo_errors option is either a module name, or an object name of the form: “module_name:expression”.

Let’s look at an example. First, an errorsample module:

import bobo, webob

@bobo.query(method='GET')
def hi(who):
    return 'Hi %s' % who

def not_found(request, method):
    return webob.Response("not found", status=404)

def method_not_allowed(request, method, methods):
    return webob.Response(
        "bad method "+method, status=405,
        headerlist=[
            ('Allow', ', '.join(methods)),
            ('Content-Type', 'text/plain'),
            ])

def missing_form_variable(request, method, name):
    return webob.Response("Missing "+name)

Then a configuration file:

[app:main]
use = egg:bobo
bobo_resources = errorsample
bobo_errors = errorsample

If we use the URL:

http://localhost:8080/hi.html?who=you

We’ll get the response:

Response: 200 OK
Content-Type: text/html; charset=UTF-8
Hi you

But if we use:

http://localhost:8080/ho

We’ll get:

Response: 404 Not Found
Content-Type: text/html; charset=UTF-8
not found

If we use:

http://localhost:8080/hi.html

We’ll get:

Response: 200 OK
Content-Type: text/html; charset=UTF-8
Missing who

If we make a POST to the same URL, we’ll get:

Response: 405 Method Not Allowed
Allow: GET
Content-Type: text/plain
bad method POST

We can use an object with methods rather than module-level functions to generate error responses. Here we define an errorsample2 module that defines an class with methods for generating error responses:

import bobo, webob

class Errors:

    def not_found(self, request, method):
        return webob.Response("not found", status=404)

    def method_not_allowed(self, request, method, methods):
        return webob.Response(
            "bad method "+method, status=405,
            headerlist=[
                ('Allow', ', '.join(methods)),
                ('Content-Type', 'text/plain'),
                ])

    def missing_form_variable(self, request, method, name):
        return webob.Response("Missing "+name)

In the configuration file, we specify an object, rather than a module:

[app:main]
use = egg:bobo
bobo_resources = errorsample
bobo_errors = errorsample2:Errors()

Note that in this example, rather than just using a global name, we use an expression to specify the errors object.

Uncaught exceptions

Normally, bobo does not let uncaught exceptions propagate; however, if the bobo_handle_exceptions option is set to False (or 'false') or if a request environment has the key x-wsgiorg.throw_errors, any uncaught exceptions will be raised. This is useful if you want WSGI middleware to handle exceptions.

If you want to provide custom handling of uncaught exceptions, you can include an exception method in the object you give to bobo_errors.

import bobo, webob

class Errors:

    def not_found(self, request, method):
        return webob.Response("not found", status=404)

    def method_not_allowed(self, request, method, methods):
        return webob.Response(
            "bad method "+method, status=405,
            headerlist=[
                ('Allow', ', '.join(methods)),
                ('Content-Type', 'text/plain'),
                ])

    def missing_form_variable(self, request, method, name):
        return webob.Response("Missing "+name)

    def exception(self, request, method, exc_info):
        return webob.Response("Dang! %s" % exc_info[0].__name__, status=500)

Ordering Resources

When looking for resources (or sub-resources) that match a request, resources are tried in order, where the default order is the order of definition. The order can be overridden by passing an order using the order keyword argument to the bobo decorators [1]. The results of calling the functions bobo.early() and bobo.late() are typically the only values that are useful to pass. It is usually a good idea to use bobo.late() for subroutes that match any path, so that more specific routes are tried earlier. If multiple resources that use bobo.late() (or bobo.early()) match a path, the first one defined will be used.

[1]Advanced applications may provide their own resource implementations. Custom resource implementations must implement the resource interface and will provide an order using the bobo_order attribute. See IResource.

Additional Helpers

In addition to query and post, bobo provides route decorators for handling specific HTTP methods. We’ll construct a simple RESTful app to demonstrate.

import bobo
import webob

items = {}

@bobo.get("/item/:id")
def _get(bobo_request, id):
   if id in items:
      data, ct = items[id]
      return webob.Response(body=data, content_type=ct)
   raise bobo.NotFound

@bobo.head("/item/:id")
def _head(bobo_request, id):
   res = _get(bobo_request, id)
   return res

@bobo.put("/item/:id")
def _put(bobo_request, id):
   items[id] = (bobo_request.body, bobo_request.content_type)
   return webob.Response(status=201)

@bobo.options("/item/:id")
def _options(bobo_request, id):
   _get(bobo_request, id)
   return "options"

@bobo.delete("/item/:id")
def _delete(bobo_request, id):
   items.pop(id, None)
   return "delete"

Backtracking

When handling a request, if bobo finds a resource that matches the route but does not accept the request method, it will continue looking for matching resources; if it eventually finds none, it will then generate a “405 Method Not Allowed” response.

import bobo

@bobo.post("/event/create")
def create(bobo_request):
    return "created event"

@bobo.resource("/event/:action?", method=("GET",))
def catch_all(bobo_request, action=None):
    return "get request for %r" % (action,)

@bobo.scan_class
class User(object):

    @bobo.resource("/:userid", method=("POST",))
    def create(self, bobo_request, userid):
        return "created user with id %r" % (userid,)

    @bobo.resource("/:identifier", method=("HEAD",))
    def head(self, bobo_request, identifier):
        return ""

    @bobo.resource("/:id", method=("GET",))
    def catch_all(self, bobo_request, id):
        return "get user with id %r" % (id,)

@bobo.scan_class
class Thing(object):

    @bobo.resource("/:id", method=("PUT",))
    def put(self, bobo_request, id):
        return "put thing with id %r" % (id,)

@bobo.subroute("/users")
def users(bobo_request):
    return User()

@bobo.subroute("/:thing")
def thing(bobo_request, thing):
    return Thing()

We have a resource that matches the route “/event/create”, but it is for POST requests. If we make a GET request, the second resource with the matching route that can handle GET requests gets called.

>>> print(app.get('/event/create').text)
get request for 'create'

Of course POST requests go to the appropriate resource.

>>> print(app.post('/event/create').text)
created event

If we perform a HEAD request for “/event/create”, we get a 405 response, as no resource is able to handle the method. The “Allow” header indicates all of the request methods that are valid for the particular path.

>>> app.head('/event/create', status=405).headers["Allow"]
'GET, POST, PUT'

The backtracking behavior works with subroutes.

>>> print(app.get('/users/1234').text)
get user with id '1234'
>>> print(app.head('/users/1234').status)
200 OK
>>> print(app.post('/users/1234').text)
created user with id '1234'

If the first matching subroute returns a resource with no handlers for the request method, the next matching subroute is tried.

>>> print(app.put('/users/54321').text)
put thing with id '54321'

If no resource is able to handle the request method, we get a 405 response with an Allow header.

>>> app.request('/users/54321',
...     method="OPTIONS", status=405).headers["Allow"]
'GET, HEAD, POST, PUT'

Automatic encoding of redirect destinations

Since URLs are often computed based on request data, it’s easy for applications to generate Unicode URLs. For this reason, unicode URL’s passed to bobo.redirect are UTF-8 encoded.

Reference

bobo module documentation

boboserver module documentation

The bobo server

The bobo server is a script that runs a development web server with a given source file or modules, and configuration options. The usage is:

bobo [options] [name=value ...]

Command-line arguments are either options, or configuration options of the form optionname=value.

Options:

-f SOURCE, --file SOURCE
 Specify a source file name to be published. It’ll be converted to a module named bobo__main__ and will have its __file__ set to the original file name.
-r RESOURCE, --resource RESOURCE
 Specify a resource, such as a module or global, to publish.
-D, --debug Provide post-mortem debugging. If an uncaught exception is raised, use pdb.post_mortem to debug it.
-p PORT, --port PORT
 Specify the port to listen on.
-c GLOBALNAME, --configure=GLOBALNAME
 

Specify the name of a global to call with configuration data. This is shorthand for bobo_configure=globalname. This is normally a name of the form modulename:expression, however, if you supply just an expression, the module of the first resource will be used. For example, with a command like:

bobo -f my.py -c config

The config function in my.py will be used.

-s ROUTE=PATH, –static ROUTE=PATH

Publish static files in the directory given by PATH at the route given by ROUTE.

While there are middleware components that are better at publishing static resources for production use, this option makes it easier to get started during development.

After the options, you can give configuration options as name=value pairs. These will be passed as configuration options to bobo and to any configuration handler you specify.

Example:

bobo -f fswiki.py -c config directory=docs

In this example, we run the application in the source file fswiki.py. We pass configuration data to the application’s config function. The options include the setting of 'doc' for the directory option.

Advanced: resource interfaces

Most applications will use the bobo-provided decorators to implement resources. These decorators create objects that provide certain methods and attributes. Applications can implement these methods and attributes themselves to provide specialized behaviors. For example, an application can implement bobo_response to provide a specialized object-look-up mechanism that doesn’t use routes.

The most important method is bobo_response. When bobo scans a module or class for resources, it looks for objects with this method. When handling a request, it calls this method on each of the objects found until a value is returned. See IResource for more details.

The optional methods, bobo_methods, bobo_order and bobo_response are used when scanning a module or class. Resources found in a module or class are ordered within the module or class based on values of their bobo_order attribute. (If a resource doesn’t have a bobo_order attribute, a value is used that is between those returned by bobo.order() and bobo.late().

The bobo_route attribute is used to group resources within a module or class that have the same route. Resources with the same route are treated as a single resource. The route is matched and then a the first resource that accepts the request method is used.

The optional bobo_reroute() method is used by the bobo bobo.reroute() function to compute a new resource from an existing resource and a new route.

IResource

class IResource

IResource is documented here to define an API that can be provided by application-defined resources. Bobo doesn’t actually define an IResource object.

bobo_response(request, path, method)

Find an object to publish at the given path.

If an object is found, call it and return the result.

If no object can be found, return None.

If a resource matches a path but doesn’t accept the request method, a 405, method not allowed, response should be returned.

If the return value isn’t a response, it should be converted to a response.

bobo_methods

This optional attribute specifies the HTTP request methods supported by objects found by the resource. See Advanced: resource interfaces. If present, it muse be a sequence of method names, or None. If it is None, then all methods are accepted.

bobo_order

This optional attribute defines the precedence order for a resource. See Advanced: resource interfaces. If present, it must be an integer. Resources with lower values for bobo_order are used before resources with higher values. If the attribute isn’t present, a very high value is assumed.

Typically, order() is called to get a value for bobo_order when a resource is defined.

bobo_route

This optional attribute defines the complete route for a resource. See Advanced: resource interfaces. If present, it must be an string.

bobo_reroute(route)

Return a new resource for the given route.

Advanced: subclassing bobo.Application

The bobo WSGI application, bobo.Application can be subclassed to handle alternate request implementations. This is to allow applications written for frameworks using request implementations other than Webob to be used with bobo. A subclass should override the __call__() and build_response() methods.

The __call__() method should:

  • Create a request.
  • Call self.bobo_response(request, path, method) to get a response.
  • Return the result of calling the response with the environ and start_response arguments passed to __call__().

The __call__() should look like:

def __call__(self, environ, start_response):
     """Handle a WSGI application request.
     """
     request = ...

     return self.bobo_response(request, request.path_info, request.method
                               )(environ, start_response)

The request should implement as much of the WebOb request API as practical. It must implement the attributes used by bobo: path_info, method, params, and POST.

The build_response() method is used to build a response when an application function returns a value that isn’t a response. See the bobo.Application for more information on this method.

New application implementations will also want to provide matching development servers. The boboserver.server() entry point accepts an alternate application object, making implementation of alternate development servers trivial.

Glossary

order
The order in which a resource is searched relative to other resources.
request
An object that contains information about a web request. This is a Webob request object. See the Webob documentation to get details of its interface.
resource
An object that has a bobo_response method. See Advanced: resource interfaces.
response

An object that represents a web response. This is usually a Webob response, but it may be any callable object that implements the WSGI application interface.

Applications will typically return strings that are converted to responses by bobo, or will construct and return Webob response objects.

route

A URL pattern expressed as a path with placeholders, as in:

/greeters/:name/:page?.html

Routes are inspired by the Ruby on Rails Routing system.

Placeholders are Python identifiers preceded by “/:”. If a placeholder is followed by a question mark, it is optional. A placeholder may be followed by an extension. When a route matches a URL, the URL text corresponding to the placeholders is passed to the application as keyword parameters.

route data
Values for placeholders resulting from matching a URL against a route. For example, matching the URL: http://localhost/a/b against the route /:x/:y/:z? results in the route data {'x': 'a', 'y': 'b'}.
routes
See route.

Examples

File-system-based wiki

In this section, we present a wiki implementation that stores wiki documents in a file-system directory:

 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
import bobo, os

def config(config):
    global top
    top = config['directory']
    if not os.path.exists(top):
        os.mkdir(top)

edit_html = os.path.join(os.path.dirname(__file__), 'edit.html')

@bobo.query('/')
def index():
    return """<html><head><title>Bobo Wiki</title></head><body>
    Documents
    <hr />
    %(docs)s
    </body></html>
    """ % dict(
        docs='<br />'.join('<a href="%s">%s</a>' % (name, name)
                           for name in sorted(os.listdir(top)))
        )

@bobo.post('/:name')
def save(bobo_request, name, body):
    open(os.path.join(top, name), 'w').write(body)
    return bobo.redirect(bobo_request.path_url, 303)

@bobo.query('/:name')
def get(name, edit=None):
    path = os.path.join(top, name)
    if os.path.exists(path):
        body = open(path).read()
        if edit:
            return open(edit_html).read() % dict(
                name=name, body=body, action='Edit')

        return '''<html><head><title>%(name)s</title></head><body>
        %(name)s (<a href="%(name)s?edit=1">edit</a>)
        <hr />%(body)s</body></html>
        ''' % dict(name=name, body=body)

    return open(edit_html).read() % dict(
        name=name, body='', action='Create')

We need to know the name of the directory to store the files in. On line 3, we define a configuration function, config.

To run this with the bobo server, we’ll use the command line:

bobo -ffswiki.py -cconfig directory=wikidocs

This tells bobo to:

  • run the file fswiki.py
  • pass configuration information to it’s config function on start up, and
  • pass the configuration directory setting of 'wikidocs'.

On line 11, we define an index method to handle / that lists the documents in the wiki.

On line 22, we define a post resource, save, for a post to a named document that saves the body submitted and redirects to the same URL.

On line 27, we define a query, get, for the named document that displays it if it exists, otherwise, it displays a creation page. Also, if the edit form variable is present, an editing interface is presented. By default, queries will accept POST requests, however, because the save function comes first, it is used for POST requests before the get function.

Both the editing and creation interfaces use an edit template, which is just a Python string read from a file that provides a form. In this case, we use Dojo to provide an HTML editor for the body:

<html>
  <head>
    <title>%(action)s %(name)s</title>

    <style type="text/css">
      @import "http://o.aolcdn.com/dojo/1.3.0/dojo/resources/dojo.css";
      @import "http://o.aolcdn.com/dojo/1.3.0/dijit/themes/tundra/tundra.css";
    </style>

    <script
       type="text/javascript"
       src="http://o.aolcdn.com/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"
       djConfig="parseOnLoad: true"
       ></script>

    <script type="text/javascript">
      dojo.require("dojo.parser");
      dojo.require("dijit.Editor");
      dojo.require("dijit._editor.plugins.LinkDialog")
      dojo.require("dijit._editor.plugins.FontChoice")

      function update_body() {
          dojo.byId('page_body').value = dijit.byId('editor').getValue();
      }

      dojo.addOnLoad(update_body);
    </script>


  </head>
  <body class="tundra">
    <h1>%(action)s %(name)s</h1>

    <div dojoType="dijit.Editor"
         id="editor"
         onChange="update_body"
         extraPlugins="['insertHorizontalRule', 'createLink',
                        'insertImage', 'unlink', 
                        {name:'dijit._editor.plugins.FontChoice',
                         command:'fontName', generic:true}
                         ]"
         >
      %(body)s
    </div>

    <form method="POST">
      <input type="hidden" name="body" id="page_body">
      <input type="submit" value="Save">
    </form>
  </body>
</html>

File-based wiki with authentication and (minimal) authorization

Traditionally, wikis allowed anonymous edits. Sometimes though, you want to require log in to make changes. In this example, we extend the file-based wiki to require authentication to make changes.

Bobo doesn’t provide any authentication support itself. To provide authentication support for bobo applications, you’ll typically use either an application library, or WSGI middleware. Middleware is attractive because there are a number of middleware authentication implementations available and because authentication is generally something you want to apply in blanket fashion to an entire application.

In this example, we’ll use the repoze.who authentication middleware component, in part because it integrates well using PasteDeploy.

 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
import bobo, os, webob

def config(config):
    global top
    top = config['directory']
    if not os.path.exists(top):
        os.mkdir(top)

edit_html = os.path.join(os.path.dirname(__file__), 'edit.html')

@bobo.query('/login.html')
def login(bobo_request, where=None):
    if bobo_request.remote_user:
        return bobo.redirect(where or bobo_request.relative_url('.'))
    return webob.Response(status=401)

@bobo.query('/logout.html')
def logout(bobo_request, where=None):
    response = bobo.redirect(where or bobo_request.relative_url('.'))
    response.delete_cookie('wiki')
    return response

def login_url(request):
    return request.application_url+'/login.html?where='+request.url

def logout_url(request):
    return request.application_url+'/logout.html?where='+request.url

def who(request):
    user = request.remote_user
    if user:
        return '''
        <div style="float:right">Hello: %s
        <a href="%s">log out</a></div>
        ''' % (user, logout_url(request))
    else:
        return '''
        <div style="float:right"><a href="%s">log in</a></div>
        ''' % login_url(request)

@bobo.query('/')
def index(bobo_request):
    return """<html><head><title>Bobo Wiki</title></head><body>
    <div style="float:left">Documents</div>%(who)s
    <hr style="clear:both" />
    %(docs)s
    </body></html>
    """ % dict(
        who=who(bobo_request),
        docs='<br />'.join('<a href="%s">%s</a>' % (name, name)
                           for name in sorted(os.listdir(top))),
        )

def authenticated(self, request, func):
    if not request.remote_user:
        return bobo.redirect(login_url(request))

@bobo.post('/:name', check=authenticated)
def save(bobo_request, name, body):
    with open(os.path.join(top, name), "wb") as f:
        f.write(body.encode('UTF-8'))
    return bobo.redirect(bobo_request.path_url, 303)

@bobo.query('/:name')
def get(bobo_request, name, edit=None):
    user = bobo_request.remote_user

    path = os.path.join(top, name)
    if os.path.exists(path):
        with open(path, "rb") as f:
            body = f.read().decode("utf-8")
        if edit:
            return open(edit_html).read() % dict(
                name=name, body=body, action='Edit')

        if user:
            edit = ' (<a href="%s?edit=1">edit</a>)' % name
        else:
            edit = ''

        return '''<html><head><title>%(name)s</title></head><body>
        <div style="float:left">%(name)s%(edit)s</div>%(who)s
        <hr style="clear:both" />%(body)s</body></html>
        ''' % dict(name=name, body=body, edit=edit, who=who(bobo_request))

    if user:
        return open(edit_html).read() % dict(
            name=name, body='', action='Create')

    return '''<html><head><title>Not found: %(name)s</title></head><body>
        <h1>%(name)s doesn not exist.</h1>
        <a href="%(login)s">Log in</a> to create it.
        </body></html>
        ''' % dict(name=name, login=login_url(bobo_request))

We’ve added 2 new pages, login.html and logout.html, to our application, starting on line 11.

The login page illustrates 2 common properties of authentication middleware:

  1. The authentication user id is provided in the REMOTE_USER environment variable and made available in the remote_user request attribute.
  2. We signal to middleware that it should ask for credentials by returning a response with a 401 status.

The login method uses remote_user to check whether a user is authenticated. If they are, it redirects them back to the URL from which they were sent to the login page. Otherwise, a 401 response is returned, which triggers repoze.who to present a log in form.

The log out form redirects the user back to the page they came from after deleting the authentication cookie. The authentication cookie is configured in the repoze.who configuration file, who.ini.

We’re going to want most pages to have links to the login and logout pages, and to display the logged in user, as appropriate. We provided some helper functions starting on line 23 for getting log in and log out URLs and for rendering a part of a page that either displays a log in link or the logged-in user and a log out link.

The index function is modified to add the user info and log in or log out links.

The save function illustrates a feature of the query, post, and resource decorators that’s especially useful for adding authorization checks. The save function can’t be used at all unless a user is authenticated. We can pass a check function to the decorator that can compute a response if calling the underlying function isn’t appropriate. In this case, we use an authenticated function that returns a redirect response if a user isn’t authenticated.

The save method is modified to check whether the user is authenticated and to redirect to the login page if they’re not.

The get function is modified to:

  • Display user information and log-in/log-out links
  • Present a not-found page with a log-in link if the page doesn’t exist and the user isn’t logged in.

Some notes about this example:

  • The example implements a very simple authorization model. A user can add or edit content if they’re logged in. Otherwise they can’t.
  • All the application knows about a user is their id. The authentication plug-in passes their log in name as their id. A more sophisticated plug-in would pass a less descriptive identifier and it would be up to the application to look up descriptive information from a user database based on this information.

Assembling and running the example with Paste Deployment and Paste Script

To use WSGI middleware, we’ll use Paste Deployment to configure the middleware and our application and to knit them together. Here’s the configuration file:

[app:main]
use = egg:bobo
bobo_resources = bobodoctestumentation.fswikia
bobo_configure = bobodoctestumentation.fswikia:config
directory = wikidocs
filter-with = reload

[filter:reload]
use = egg:bobo#reload
modules = bobodoctestumentation.fswikia
filter-with = who

[filter:who]
use = egg:repoze.who#config
config_file = who.ini
filter-with = debug

[filter:debug]
use = egg:bobo#debug

[server:main]
use = egg:Paste#http
port = 8080

The configuration defines 5 WSGI components, in 5 sections:

server:main
This section configures a simple HTTP server running on port 8080.
app:main

This section configures our application. The options:

use
The use option instructs Paste Deployment to run the bobo main application.
bobo_resources
The bobo_resources option tells bobo to run the application in the module bobodoctestumentation.fswikia.
bobo_configure
The bobo_configure option tells bobo to call the config function with the configuration options.
directory
The directory option is used by the application to determine where to store wiki pages.
filter-with
The filter-with option tells Paste Deployment to apply the reload middleware, defined by the filter:reload section to the application.
filter:reload

The filter:reload section defines a middleware component that reloads given modules when their sources change. It’s provided by the bobo egg under the name reload, as indicated by the use option.

The filter-with option is used to apply yet another filter, who to the reload middleware.

filter:who

The filter:who section configures a repose.who authentication middleware component. It uses the config_file option to specify a repoze.who configuration file, who.ini:

[plugin:form]
use = repoze.who.plugins.form:make_plugin
login_form_qs = __do_login
rememberer_name = auth_tkt

[plugin:auth_tkt]
use = repoze.who.plugins.auth_tkt:make_plugin
secret = s33kr1t
cookie_name = wiki
secure = False
include_ip = False

[plugin:htpasswd]
use = repoze.who.plugins.htpasswd:make_plugin
filename = htpasswd
check_fn = repoze.who.plugins.htpasswd:crypt_check

[general]
request_classifier = repoze.who.classifiers:default_request_classifier
challenge_decider = repoze.who.classifiers:default_challenge_decider
remote_user_key = REMOTE_USER

[identifiers]
plugins = form;browser auth_tkt

[authenticators]
plugins = auth_tkt htpasswd

[challengers]
plugins = form;browser

See the repoze.who documentation for details of configuring repoze.who.

The filter-with option is used again here to apply a final middleware component, debug.
filter:debug
The filter:debug section defines a post-mortem debugging middleware component that allows us to debug exceptions raised by the application, or by the other 2 middleware components.

In this example, we apply 3 middleware components to the bobo application. When a request comes in:

  1. The server calls the debug component.

  2. The debug component calls the who component. If an exception is raised, the pdb.post_mortem debugger is invoked.

  3. The who component checks for credentials and sets REMOTE_USER in the request environment if they are present. It then calls the reload component. If the response from the reload component has a 401 status, it presents a log in form.

  4. The reload component checks to see if any of it’s configured module sources have changed. If so, it reloads the modules and reinitializes it’s application. (The reload component knows how to reinitialize bobo applications and can only be used with bobo application objects.)

    The reload component calls the bobo application.

The configuration above is intended to support development. A production configuration would omit the reload and debug components:

[app:main]
use = egg:bobo
bobo_resources = bobodoctestumentation.fswikia
bobo_configure = config
directory = wikidocs
filter-with = who

[filter:who]
use = egg:repoze.who#config
config_file = who.ini

[server:main]
use = egg:Paste#http
port = 8080

To run the application in the foreground, we’ll use:

paster serve fswikia.ini

For this to work, the paster script must be installed in such a way that PasteScript, repoze.who, bobo, the wiki application module, and all their dependencies are all importable. This can be done either by installing all of the necessary packages into a (real or virtual) Python, or using zc.buildout.

To run this example, I used a buildout that defined a paste part:

[paste]
recipe = zc.recipe.egg
eggs = PasteScript
       repoze.who
       bobodoctestumentation

The bobodoctestumentation package is a package that includes the examples used in this documentation and depends on bobo. Because the configuration files are in the bobodoctestumentation source directory, I actually ran the application this way:

cd bobodoctestumentation/src/bobodoctestumentation
../../../bin/paster serve fswikia.ini

Ajax calculator

This example shows how the application/json content type can be used in ajax [1] applications. We implement a small (silly) ajax calculator application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import bobo, os

@bobo.query('/')
def html():
    return open(os.path.join(os.path.dirname(__file__),
                             'bobocalc.html')).read()

@bobo.query(content_type='application/json')
def add(value, input):
    value = int(value)+int(input)
    return dict(value=value)

@bobo.query(content_type='application/json')
def sub(value, input):
    value = int(value)-int(input)
    return dict(value=value)

The html method returns the application page:

 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
<html>
  <head>
    <title>Bobocalc</title>

    <style type="text/css">
      @import "http://o.aolcdn.com/dojo/1.3.0/dojo/resources/dojo.css";
      @import "http://o.aolcdn.com/dojo/1.3.0/dijit/themes/tundra/tundra.css";
    </style>

    <script
       type="text/javascript"
       src="http://o.aolcdn.com/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"
       djConfig="parseOnLoad: true, isDebug: true, debugAtAllCosts: true"
       ></script>

    <script type="text/javascript">
      dojo.require("dojo.parser");
      dojo.require("dijit.form.Button");
      dojo.require("dijit.form.ValidationTextBox");

      bobocalc = function () {
          function op(url) {
              dojo.xhrGet({
                  url: url, handleAs: 'json',
                  content: {
                      value: dojo.byId('value').textContent,
                      input: dijit.byId('input').value
                  },
                  load: function(data) {
                      dojo.byId('value').textContent = data.value;
                      dojo.byId('input').value = '';
                  }
              });
          }
          return {
              add: function () { op('add.json'); },
              sub: function () { op('sub.json'); },
              clear: function () { dojo.byId('value').textContent = 0; }
          };
      }();
    </script>

  </head>
  <body class="tundra">
    <h1><em>Bobocalc</em></h1>

    Value: <span id="value">0</span>
    <form>
      <label for="input">Input:</label>
      <input
         type="text" id="input" name="input"
         dojoType="dijit.form.ValidationTextBox" regExp="[0-9]+"
         />
      <button dojoType="dijit.form.Button" onClick="bobocalc.clear">C</button>
      <button dojoType="dijit.form.Button" onClick="bobocalc.add">+</button>
      <button dojoType="dijit.form.Button" onClick="bobocalc.sub">-</button>
    </form>
  </body>
</html>

This page presents a value, and input field and clear (C), add (+) and subtract (-) buttons. When the user selects the add or subtract buttons, an ajax request is made to the server. The ajax request passes the input and current value as form data to the add or sub resources on the server.

The add and sub methods in bobocalc.py simply convert their arguments to integers and compute a new value which they return in a dictionary. Because we used the application/json content type, the dictionaries returned are marshaled as JSON.

Static resources

We provide a resource that serves a static file-system directory. This is useful for serving static resources such as javascript source and CSS.

 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
import bobo, mimetypes, os, webob

@bobo.scan_class
class Directory:

    def __init__(self, root, path=None):
        self.root = os.path.abspath(root)+os.path.sep
        self.path = path or root

    @bobo.query('')
    def base(self, bobo_request):
        return bobo.redirect(bobo_request.url+'/')

    @bobo.query('/')
    def index(self):
        links = []
        for name in sorted(os.listdir(self.path)):
            if os.path.isdir(os.path.join(self.path, name)):
                name += '/'
            links.append('<a href="%s">%s</a>' % (name, name))
        return """<html>
        <head><title>%s</title></head>
        <body>
          %s
        </body>
        </html>
        """ % (self.path[len(self.root):], '<br>\n  '.join(links))

    @bobo.subroute('/:name')
    def traverse(self, request, name):
        path = os.path.abspath(os.path.join(self.path, name))
        if not path.startswith(self.root):
            raise bobo.NotFound
        if os.path.isdir(path):
            return Directory(self.root, path)
        else:
            return File(path)

@bobo.scan_class
class File:
    def __init__(self, path):
        self.path = path

    @bobo.query('')
    def base(self, bobo_request):
        response = webob.Response()
        content_type = mimetypes.guess_type(self.path)[0]
        if content_type is not None:
            response.content_type = content_type
        try:
            with open(self.path, "rb") as f:
                response.body = f.read()
        except IOError:
            raise bobo.NotFound

        return response

This example illustrates:

traversal
The Directory.traverse method enables directories to be traversed with a name to get to sub-directories or files.
use of the bobo.NotFound exception
Rather than construct a not-found ourselves, we simply raise bobo.NotFound, and let bobo generate the response for us.

[1]This isn’t strictly “Ajax”, because there’s no XML involved. The requests we’re making are asynchronous and pass data as form data and generally expect response data to be formatted as JSON.