Introduction to Bobo




Jim Fulton, Zope Corporation

PyOhio 2014

July 27, 2014

About me

Outline

Why Bobo?

Publishing and only publishing

Bobo's one job is to route a Web request to a Python object that can handle it and to take resulting data and return a Web response.

Just publishing.

Micro-Framework?

Abstractions aren't free

These costs accumulate over time.

Benefits of abstractions have to more than justify costs.

Local/application-specific abstractions may be cheaper.

Getting started

Development server

Installation

Routes

Return values

Handler arguments

"Natural" object calls

Bobo passes form and JSON propeties as handler arguments.

This makes handling client calls a lot like handling other Python calls.

This works especially well when handling JSON requests, as you have (somewhat) richer data types.

(Route handlers can be called from Python as if they weren't decorated.)

In the future, Python 3 type annotation will make this work even better:

@bobo.post('/event')
def new_event(title: str, capacity: int, when: mydate) -> bobo.JSON:
    ...

Webob

Basic handlers

Subroutes

Subroutes split route processing into multiple steps.

Employees

database = [dict(name='Bob'), dict(name="Sally")]

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

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

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

    @bobo.get('/', content_type='application/json')
    def get(self):
        return database[self.employee_id]

    @bobo.put('/', content_type='application/json')
    def update(self):
        database[self.employee_id] = self.request.json
        return database[self.employee_id]

Recursive data structures

database = [
  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:
    ...

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

Folder

@bobo.scan_class
class Folder:

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

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

    @bobo.get('/', content_type='application/json')
    def index(self):
        return list(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)

Document

@bobo.scan_class
class Document:

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

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

Checkers

Checkers provide preconditions for resources, typically for performing authorization checks:

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

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

    @bobo.put('/', content_type='application/json', check=authenticated)
    def update(self):
        database[self.employee_id] = self.request.json
        return database[self.employee_id]

WSGI

Pipelined component model for Python web applications.

Before WSGI, no standard way to combine Python applications with web servers.

Architecture:

Bobo development server

The bobo development server creates a WSGI stack with your application at the bottom:

wsgiref.simple_server

      ↑       ↓

  boboserver.Debug

      ↑       ↓

  boboserver.Reload

      ↑       ↓

    application

reacht (reachtapp.com)

   zope.server
       ↑↓
    Paste#lint
       ↑↓
Paste#error_catcher
       ↑↓
  repose.retry
       ↑↓
   zc.zodbwsgi
       ↑↓
   translogger
       ↑↓
 zc.dbconnectuon
       ↑↓
  Paste#urlmap
       ↑↓
     reacht

WebTest

WSGI "server" for functional testing WSGI apps.

Example that tests redirect to login page:

>>> test_app = webtest.TestApp(myapp)

>>> res = app.get('/orgs')
>>> res.body
'See /admin_login?camefrom=%2Forgs'
>>> res.status
'302 Found'
>>> res.location
'http://localhost/admin_login?camefrom=%2Forgs'

Paste Deployment

Framework and configuration format for defining WSGI stacks.

Sample, my.ini:

[app:main]
use = egg:bobo
bobo_resources = mymodule

[filter:reload]
use = egg:bobo#reload
modules = mymodule

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

[pipeline:debug]
pipeline = debug reload main

[server:main]
use = egg:waitress
host = localhost
port = 8080

Running WSGI stacks

Whichever you use, you'll want to run a daemonizer, like ZDaemon or supervisord.

Other options

There are options for specific server environments, such as:

But wait, templates?

You don't need your web framework to invoke templates for you:

from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader('yourapplication', 'templates'))
template = env.get_template('mytemplate.html')

...

@bobo.get("/someroute)
def something():
    return template.render(the='variables', go='here')

Databases?

How you use databases is application dependent, but it's common to want to automate making database connection/transactions for web requests.

Use middlware to manage transactions and connections.

repose.retry, zc.zodbwsgi, ...

Issue:

Need to let middleware handle errors so you can abort on error, or retry on conflicts.

Bobo has an option for that.

History

The death of "bobo" (~2000)

Prefer idiots APIs

Bobo is boring

I like it that way.

Bobo doesn't want to grow.

Hardly ever changes.

Recently:

Future:

Other micro-frameworks

I haven't used these, but:

Neither of these work with WebOb.

Resources, and Questions?