15 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
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}
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') |
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/stocks
Bad | Good |
---|---|
getStocks | GET /stocks |
addStock | POST /stocks |
removeStock | DELETE /stocks/AAPL |
getStock | GET /stocks/AAPL |
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://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 |
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)
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)
@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
application/x-www-form-urlencoded
, multipart/form-data
, text/plain
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)
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 |
# 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