Simple API's with bottle.py

Andrew Lombardi / @kinabalu

Mystic Coders, LLC

About Me

About Me

15 Years in Business

Software Consultants

International Speakers

Training

App Developer (Java, JavaScript, Python, Objective-C)

 

To our success!

What we'll cover

  • Installing bottle and dependencies
  • Building a RESTful example
  • Testing calls to REST API
  • Integration with WSGI middleware

Why bottle?

  • Bottle is a fast and lightweight micro framework for Python.
  • Single file module with no dependencies
  • Provides routing, templates, utilities, and server integration

Hello, World! Example

from bottle import route, run

@route('/hello')
def hello():
    return "Hello, World!"

run(host='localhost', port=8080, debug=True)
                        

Save to helloworld.py

$ python helloworld.py

http://localhost:8080/hello

Hello, World

Fun fact:

The initial incarnation of everyones first application in a new language / technology was first written in Kernighan's 1972 A Tutorial Introduction to the Language B, and it was used to illustrate external variables in the language.

Hello, World testable


from bottle import Bottle, run

app = Bottle()

@app.route('/hello')
def hello():
    return "Hello, World!"

if __name__ == '__main__':
    run(app, host='localhost', port=8080, debug=True)

Testing Example


from webtest import TestApp
import helloworld

def test_functional_helloworld():
    app = TestApp(helloworld.app)

    assert app.get('/hello').status == '200 OK'
    assert app.get('/hello').text == 'Hello, World!'

Install bottle.py

$ pip install bottle

$ easy_install bottle

$ apt-get install python-bottle

First install virtualenv

Installing virtualenv

$ pip install virtualenv

Configuring virtualenv for example

$ virtualenv bottleapi
$ source bottleapi/bin/activate
$ pip install -U bottle

requirements.txt


$ pip freeze > requirements.txt
$ cat requirements.txt
bottle==0.12.7

virtualenv Directory Structure


.
└── bottleapi
    ├── bin
    ├── include
    │   └── python2.7
    └── lib
        └── python2.7
            └── site-packages

Segmenting our dependencies and allow testing multiple python environments

Docker is possibly a better option

Our Stocks Example

Stocks Example - GET


from bottle import Bottle, run, response
import json

app = Bottle()

@app.route('/stocks', method='GET')
def list_stocks():
    stocks = {
        'num_results': 3,
        'total_pages': 1,
        'page': 1,
        'objects': [
            {"symbol": "AAPL", "price": 114.18},
            {"symbol": "MSFT", "price": 49.58},
            {"symbol": "GOOG", "price": 544.40}
        ]
    }
    response.content_type = 'application/json'    
    return json.dumps(stocks)

if __name__ == '__main__':
    run(app, host='localhost', port=8080, debug=True)

curl to GET /stocks


$ curl -X GET http://localhost:8080/stocks
{"total_pages": 1, "objects": [{"symbol": "AAPL", "price": 114.18}, {"symbol": "MSFT", "price": 49.58}, {"symbol": "GOOG", "price": 544.4}], "num_results": 3, "page": 1}

Alternative to @route

Original Alternate
@app.route('/sample', method='GET') @app.get('/sample')
@app.route('/sample', method='POST') @app.post('/sample')
@app.route('/sample', method='PUT') @app.put('/sample')
@app.route('/sample', method='DELETE') @app.delete('/sample')

https://www.getpostman.com

Google Chrome plugin for testing HTTP and REST endpoints via a simple interface

Adding new stocks

Adding new stocks


from bottle import Bottle, run, request, response

...

@app.route('/stocks', method='POST')
def add_stock():
    try:
        postdata = request.body.read()
        symbol_request = json.loads(postdata)
        stocks.append({"symbol": symbol_request['symbol'], "price": 75.42})
    except TypeError:
        abort(500, "Invalid content passed")

curl POST test

curl -x POST --data '{"symbol": "FB"}' http://localhost:8080/stocks

Overview of REST

Nouns not verbs

Bad Good
getStocks GET /stocks
addStock POST /stocks
removeStock DELETE /stocks/AAPL
getStock GET /stocks/AAPL

HTTP methods = verbs

http://example.com/resources

Method Action

GET

List the members of the collection

PUT

Replace the entire collection with another collection

POST

Create new entry in collection

DELETE

Delete the entire collection

HTTP methods = verbs

http://example.com/resources/42

Method Action

GET

Return the referenced member of the collection

PUT

Create or update the referenced member of the collection

POST

Generally unused on specific members

DELETE

Delete the referenced member of collection

GET and POST constraints

Sometimes, firewalls, proxies, or just sending a unique HTTP method via an HTTP form is not allowed

First we write a plugin to take _method and replace REQUEST_METHOD with that value


class MethodOverride(object):
  def __init__(self, app):
    self.app = app
 
  def __call__(self, environ, start_response):
    method = webapp.Request(environ).get('_method')
    if method:
      environ['REQUEST_METHOD'] = method.upper()
    return self.app(environ, start_response)

GET and POST constraints

Here's a test using the plugin

 
from bottle import Bottle, run

app = Bottle()

@route("/test_override", method="PUT")
def test_put():
  return "PUT worked!"
 
@route("/test_override", method="DELETE")
def test_delete():
  return "DELETE worked!"
 
if __name__ == '__main__':
    override_plugin = MethodOverride(app)
    app.install(override_plugin)
    run(app, host='localhost', port=8080, debug=True) 

What about PUT, PATCH, and DELETE


@app.route("/stocks", method="PUT")
@app.route("/stocks", method="PATCH")
@app.route("/stocks", method="DELETE")
def not_allowed():
    abort(405, "Not Allowed")

Member REST calls

Get referenced stock


@app.route('/stocks/<symbol>', method='GET')
def get_stock(symbol='AAPL'):
    stock = {
        "symbol": "AAPL",
        "price": 114.18
    }
    response.content_type = 'application/json'    
    return json.dumps(stock)

Delete referenced stock


@app.route('/stocks/<symbol>', method='DELETE')
def delete_stock(symbol='AAPL'):
    response.content_type = 'application/json'

    for idx, stock in enumerate(stocks):
        if stock['symbol'] == symbol:
            del stocks[idx]
            response.status = 200
            return ''
    abort(404, 'Stock not found')

What about POST, PUT, and PATCH


@app.route("/stocks/<symbol>", method="POST")
@app.route("/stocks/<symbol>", method="PUT")
@app.route("/stocks/<symbol>", method="PATCH")
def not_allowed():
    abort(405, "Not Allowed")

Rewrite ConTalk API with bottle

Serving our API from different domain

CORS

  • C . ross
  • O . rigin
  • R . esource
  • S . haring

OPTIONS and preflight

More complex CORS request comes with preflight

  • Custom headers are used in the request
  • An HTTP method other than GET, HEAD, or POST used
  • POST used but Content-Type other than application/x-www-form-urlencoded, multipart/form-data, text/plain

CORS via bottle.py decorator


import bottle
from bottle import response

# the decorator
def enable_cors(fn):
    def _enable_cors(*args, **kwargs):
        # set CORS headers
        response.headers['Access-Control-Allow-Origin'] = '*'
        response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
        response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

        if bottle.request.method != 'OPTIONS':
            # actual request; reply with the actual response
            return fn(*args, **kwargs)

    return _enable_cors


app = bottle.app()

@app.route('/cors', method=['OPTIONS', 'GET'])
@enable_cors
def lvambience():
    response.headers['Content-type'] = 'application/json'
    return '[1]'

app.run(port=8001)

CORS via bottle.py Plugin


import bottle
from bottle import response

class EnableCors(object):
    name = 'enable_cors'
    api = 2

    def apply(self, fn, context):
        def _enable_cors(*args, **kwargs):
            # set CORS headers
            response.headers['Access-Control-Allow-Origin'] = '*'
            response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
            response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

            if bottle.request.method != 'OPTIONS':
                # actual request; reply with the actual response
                return fn(*args, **kwargs)

        return _enable_cors


app = bottle.app()

@app.route('/cors', method=['OPTIONS', 'GET'])
def lvambience():
    response.headers['Content-type'] = 'application/json'
    return '[1]'

app.install(EnableCors())

app.run(port=8001)

Bottle handles defaults nicely

Data Type How Bottle Handles
Dict Returns a Content-Type application/json
None/False/Empty String Produces a Content-Length header of 0
Unicode Strings Automatically encoded with the codec specified in the Content-Type, of which utf8 is the default, then treated as a normal Byte String.
Byte Strings Outputs to browser and proper Content-Length header for size
File objects Everything that has a .read() method is treated as File object and passed to the wsgi.file_wrapper callable defined by the WSGI server framework

Middleware Integration

# server.py
run(host='localhost', port=36086, server='cherrypy')

from cherrypy import wsgiserver
from helloworld import app

d = wsgiserver.WSGIPathInfoDispatcher({'/': app})
server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 8080), d)

if __name__ == '__main__':
   try:
      server.start()
   except KeyboardInterrupt:
      server.stop()

# api.py
if __name__ == '__main__':
    run(app, host='localhost', port=8080, server='cherrypy')

Deploy using Docker with nginx

Yes it can be done, and yes it's awesome

What we've covered

  • Installing bottle and dependencies
  • Building a RESTful example
  • Testing calls to REST API
  • Integration with WSGI middleware
  • Deployment with Docker

New WebSocket book

bit.ly/lombardi_websocket_book

Q & A

Andrew Lombardi / @kinabalu

kinabalu @ irc://irc.freenode.net

#javascript

http://kinabalu.github.io/simple_apis_bottlepy_presentation