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; charset=UTF-8'),
            ])

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; charset=UTF-8
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; charset=UTF-8'),
                ])

    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.