Introduction to Bobo




Jim Fulton, Zope Corporation

NoVA Python Meetup

April 17, 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?

Getting started

Development server

Installation

Routes

Return values

Handler arguments

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(self.data['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=auth)
    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.

Bobo has an option for that.

Tweens?

Pyramid has a concept of tweens

Similar to middleware, but inside application, with better access to the application environment.

Probably better for managing database connections and transactions.

Bobo will probably grow something similar.

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.

But some opportunities:

Resources