14 Years in Business
Software Consultants
International Speakers
Training
App Developer (Java, JavaScript, Python, Objective-C)
To our success!
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
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.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)
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!'
$ pip install bottle
$ easy_install bottle
$ apt-get install python-bottle
$ pip install virtualenv
$ virtualenv bottleapi $ source bottleapi/bin/activate $ pip install -U bottle
$ pip freeze > requirements.txt $ cat requirements.txt bottle==0.12.7
. └── 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.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 -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}
Google Chrome plugin for testing HTTP and REST endpoints via a simple interface
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 -x POST --data '{"symbol": "FB"}' http://localhost:8080/stocksConsider appending this to add a functional test using WebTest
http://example.com/resources/42
Method ActionSometimes, 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)
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
@app.route("/stocks", method="PUT") @app.route("/stocks", method="PATCH") @app.route("/stocks", method="DELETE") def not_allowed(): abort(405, "Not Allowed")
@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)
@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')
@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")
More complex CORS request comes with preflight
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)
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)
# 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')
Yes it can be done, and yes it's awesome
kinabalu @ irc://irc.freenode.net
#javascript