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:
- The path
/employees/1
matches theEmployees
class. - The path
/documents
matches thedocuments
method, which returns aFolder
using the employee documents dictionary. - The path
/hobbies
matches thesubitem
method of theFolder
class, which returns thehobbies
dictionary from the documents folder. - The path
/sports.html
also matches thesubitem
Folder
method, which returns aDocument
using the text for thesports.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.