Simple API's with bottle.py – About Me – Hello, World! Example



Simple API's with bottle.py – About Me – Hello, World! Example

0 1


simple_apis_bottlepy_presentation-

Simple API's with bottle.py

On Github kinabalu / simple_apis_bottlepy_presentation-

Simple API's with bottle.py

Andrew Lombardi / @kinabalu

Mystic Coders, LLC

About Me

About Me

14 Years in Business

Software Consultants

International Speakers

Training

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

 

To our success!

pian kato svinia

Known side effects: - You may start to sing. - You can't find your way home. - It's possible you may forget your name and/or get lost in your own home. - "pian kato svinia" - more than 1 liter - "drunk like a pig"

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

Lots of reasons to hate virtualenv, pip, etc but we won't go into detail here. This configuration will work. Can be done with a true virtualized environment such as Docker.

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

Let's open up Postman and run the demo and the test together so they can see it working

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
Consider appending this to add a functional test using WebTest

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) 
6 constraints of REST https://blog.apigee.com/detail/api_design_ruminating_over_rest

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
This was shown to be a problem and head scratcher because certain javascript libraries were sending a custom header 'X-Requested-With' when issuing a request and it triggered the preflight.

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