Share
Embed
Prototyping APIs with Flask
You need to build a new API, but which tools do you use? Flask is a microframework that makes web development a snap, and an ecosystem of extensions and other tools has grown around it to make it perfect for prototyping APIs. In this talk, we'll see how to get started with Flask, and learn the best parts of its ecosystem for API development.
David Baumgold
April 26, 2016
Transcript
58 slides
-
PROTOTYPING NEW APIS WITH FLASK DAVID BAUMGOLD @SINGINGWOLFBOY TUESDAY MAY
31 PYCON 2016 IN PORTLAND slides: bit.ly/flask-api-pycon-2016 code: github.com/singingwolfboy/build-a-flask-api -
WHAT DO WE WANT? • JSON data format • CRUD
operations • REST semantics • Flexible code (create, read, update, delete) (it’s a prototype!) bit.ly/flask-api-pycon-2016 -
WHAT DO WE NOT CARE ABOUT? • Stability & testing
• Long-term maintainability • Edge cases • Operations & deployment IT’S ONLY A PROTOTYPE! bit.ly/flask-api-pycon-2016 -
HELLO WORLD from flask import Flask app = Flask(__name__) @app.route("/")
def hello(): return "Hello World!" if __name__ == "__main__": app.run() bit.ly/flask-api-pycon-2016 -
HELLO JSON bit.ly/flask-api-pycon-2016 from flask import Flask, jsonify app =
Flask(__name__) @app.route("/") def hello(): return jsonify({"message": "Hello, World!"}) if __name__ == "__main__": app.run() -
HELLO ROVER bit.ly/flask-api-pycon-2016 from flask import Flask, jsonify app =
Flask(__name__) @app.route("/") def get_puppy(): puppy = { "name": "Rover", "image_url": "http://example.com/rover.jpg", } return jsonify(puppy) if __name__ == "__main__": app.run() -
from flask import Flask, jsonify, abort app = Flask(__name__) PUPPIES
= [ { "name": "Rover", "image_url": "http://example.com/rover.jpg", }, { "name": "Spot", "image_url": "http://example.com/spot.jpg", }, ] @app.route("/<int:index>") def get_puppy(index): try: puppy = PUPPIES[index] except IndexError: abort(404) return jsonify(puppy) if __name__ == "__main__": app.run() -
from flask import Flask, jsonify, abort app = Flask(__name__) PUPPIES
= { "rover": { "name": "Rover", "image_url": "http://example.com/rover.jpg", }, "spot": { "name": "Spot", "image_url": "http://example.com/spot.jpg", }, } @app.route("/<slug>") def get_puppy(slug): try: puppy = PUPPIES[slug] except KeyError: abort(404) return jsonify(puppy) if __name__ == "__main__": app.run() -
SEPARATION OF CONCERNS Database contains data View transforms data to
JSON bit.ly/flask-api-pycon-2016 -
SQLALCHEMY MODEL models.py bit.ly/flask-api-pycon-2016 fast lookup by slug required values
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Puppy(db.Model): id = db.Column(db.Integer, primary_key=True) slug = db.Column(db.String(64), index=True) name = db.Column(db.String(64), nullable=False) image_url = db.Column(db.String(128), nullable=False) -
INSERT INTO DATABASE # create a Puppy object, and add
it to the session p1 = Puppy(slug="rover", name="Rover", image_url="http://example.com/rover.jpg") db.session.add(p1) # create and add a second Puppy p2 = Puppy(slug="spot", name="Spot", image_url="http://example.com/spot.jpg") db.session.add(p2) # changes won't be saved until committed! db.session.commit() bit.ly/flask-api-pycon-2016 -
QUERY DATABASE # .all() returns a list all_puppies = Puppy.query.all()
len(all_puppies) == 2 # .first() returns the first item that matches spot = Puppy.query.filter(Puppy.slug=="spot").first() spot.name == "Spot" spot.image_url == "http://example.com/spot.jpg" # .filter_by() is a shortcut rover = Puppy.query.filter_by(slug="rover").first() rover.name == "Rover" rover.image_url == "http://example.com/rover.jpg" # if no matches, .first() returns None lassie = Puppy.query.filter_by(slug="lassie").first() lassie == None bit.ly/flask-api-pycon-2016 -
UPDATE DATABASE # query to get a Puppy object spot
= Puppy.query.filter(Puppy.slug=="spot").first() spot.image_url == "http://example.com/spot.jpg" # update image_url spot.image_url = "http://example.com/adorable.jpg" # updates won't be saved until committed db.session.add(spot) db.session.commit() # query again, and it's updated! p2 = Puppy.query.filter(Puppy.slug=="spot").first() p2.image_url == "http://example.com/adorable.jpg" bit.ly/flask-api-pycon-2016 -
DELETE FROM DATABASE # Query to get a Puppy object
rover = Puppy.query.filter_by(slug="rover").first() rover.name == "Rover" # We can delete Rover. Sorry, buddy! db.session.delete(rover) db.session.commit() # Rover is gone! p2 = Puppy.query.filter_by(slug="rover").first() p2 == None # But the local variable still has the old information rover.name == "Rover" # However, all local variables vanish at the end of # the HTTP request bit.ly/flask-api-pycon-2016 -
INTEGRATE FLASK-SQLALCHEMY bit.ly/flask-api-pycon-2016 from flask import Flask, jsonify from models
import db, Puppy app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///puppy.db" db.init_app(app) @app.route("/<slug>") def get_puppy(slug): puppy = Puppy.query.filter(Puppy.slug==slug).first_or_404() output = { "name": puppy.name, "image_url": puppy.image_url, } return jsonify(output) if __name__ == "__main__": app.run() -
NEED TO CREATE DATABASE TABLES $ python puppy.py createdb Database
created! bit.ly/flask-api-pycon-2016 import sys [...] if __name__ == "__main__": if "createdb" in sys.argv: with app.app_context(): db.create_all() print("Database created!") else: app.run() -
SEED DATA INTO DATABASE $ python puppy.py seeddb Database seeded!
if __name__ == "__main__": if "createdb" in sys.argv: with app.app_context(): db.create_all() print("Database created!") elif "seeddb" in sys.argv: with app.app_context(): p1 = Puppy(slug="rover", name="Rover", image_url="http://example.com/rover.jpg") db.session.add(p1) p2 = Puppy(slug="spot", name="Spot", image_url="http://example.com/spot.jpg") db.session.add(p2) db.session.commit() print("Database seeded!") else: app.run() -
pip install slugify from slugify import slugify from flask import
request from flask import url_for @app.route("/", methods=["POST"]) def create_puppy(): # validate attributes name = request.form.get("name") if not name: return "name required", 400 image_url = request.form.get("image_url") if not image_url: return "image_url required", 400 slug = slugify(name) # create in database puppy = Puppy(slug=slug, name=name, image_url=image_url) db.session.add(puppy) db.session.commit() # return HTTP response resp = jsonify({"message": "created"}) resp.status_code = 201 location = url_for("get_puppy", slug=slug) resp.headers["Location"] = location return resp -
LET’S CREATE A PUPPY bit.ly/flask-api-pycon-2016 $ curl -X POST localhost:5000
-d name=Lassie \ -d url=http://example.com/lassie.jpg -i HTTP/1.0 201 CREATED Content-Type: application/json Content-Length: 26 Location: http://localhost:5000/lassie Server: Werkzeug/0.10.4 Python/2.7.11 Date: Mon, 25 Apr 2016 22:49:08 GMT { "message": "created" } -
from flask import Flask, jsonify from models import db, Puppy
app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///puppy.db" db.init_app(app) @app.route("/<slug>") def get_puppy(slug): puppy = Puppy.query.filter(Puppy.slug==slug).first_or_404() output = { "name": puppy.name, "image_url": puppy.image_url, } return jsonify(output) if __name__ == "__main__": app.run() EXAMPLE bit.ly/flask-api-pycon-2016 -
SEPARATION OF CONCERNS Database holds all internal data View communicates
with external clients Transformer converts between internal DB resources and external API resources bit.ly/flask-api-pycon-2016 -
MARSHMALLOW SCHEMA from flask_marshmallow import Marshmallow from models import Puppy
ma = Marshmallow() class PuppySchema(ma.ModelSchema): class Meta: model = Puppy puppy_schema = PuppySchema() puppies_schema = PuppySchema(many=True) schemas.py bit.ly/flask-api-pycon-2016 -
INTEGRATE FLASK-MARSHMALLOW from flask import Flask, jsonify from models import
db, Puppy from schemas import ma, puppy_schema app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///puppy.db" db.init_app(app) ma.init_app(app) @app.route("/<slug>") def get_puppy(slug): puppy = Puppy.query.filter(Puppy.slug==slug).first_or_404() return puppy_schema.jsonify(puppy) -
DEFINE OUTPUT FIELDS schemas.py from flask_marshmallow import Marshmallow from models
import Puppy ma = Marshmallow() class PuppySchema(ma.ModelSchema): class Meta: model = Puppy exclude = ["id", "slug"] puppy_schema = PuppySchema() puppies_schema = PuppySchema(many=True) -
CREATE A PUPPY bit.ly/flask-api-pycon-2016 @app.route("/", methods=["POST"]) def create_puppy(): puppy, errors
= puppy_schema.load(request.form) if errors: resp = jsonify(errors) resp.status_code = 400 return resp puppy.slug = slugify(puppy.name) db.session.add(puppy) db.session.commit() resp = jsonify({"message": "created"}) resp.status_code = 201 location = url_for("get_puppy", slug=puppy.slug) resp.headers["Location"] = location return resp -
EDIT A PUPPY bit.ly/flask-api-pycon-2016 @app.route("/<slug>", methods=["POST"]) def edit_puppy(slug): puppy =
Puppy.query.filter(Puppy.slug==slug).first_or_404() puppy, errors = puppy_schema.load(request.form, instance=puppy) if errors: resp = jsonify(errors) resp.status_code = 400 return resp puppy.slug = slugify(puppy.name) db.session.add(puppy) db.session.commit() resp = jsonify({"message": "updated"}) location = url_for("get_puppy", slug=puppy.slug) resp.headers["Location"] = location return resp -
DELETE A PUPPY $ curl -X DELETE localhost:5000/spot { "message":
"deleted" } $ curl -X DELETE localhost:5000/spot <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>404 Not Found</title> <h1>Not Found</h1> bit.ly/flask-api-pycon-2016 @app.route("/<slug>", methods=["DELETE"]) def delete_puppy(slug): puppy = Puppy.query.filter(Puppy.slug==slug).first_or_404() db.session.delete(puppy) db.session.commit() return jsonify({"message": "deleted"}) -
LET’S JSONIFY THAT ERROR $ curl -X DELETE localhost:5000/spot {
"error": "not found" } bit.ly/flask-api-pycon-2016 @app.errorhandler(404) def page_not_found(error): resp = jsonify({"error": "not found"}) resp.status_code = 404 return resp -
$ curl localhost:5000 [ { "image_url": "http://example.com/rover.jpg", "name": "Rover" },
{ "image_url": "http://example.com/spot.jpg", "name": "Spot" } ] LIST PUPPIES bit.ly/flask-api-pycon-2016 @app.route("/", methods=["GET"]) def list_puppies(): all_puppies = Puppy.query.all() data, errors = puppies_schema.dump(all_puppies) return jsonify(data) -
class Puppy(db.Model): id = db.Column(db.Integer, primary_key=True) slug = db.Column(db.String(64), index=True)
name = db.Column(db.String(64), nullable=False) image_url = db.Column(db.String(128), nullable=False) age = db.Column(db.Integer, default=1) BENEFIT: DATA FLEXIBILITY bit.ly/flask-api-pycon-2016 Modifying the data models just works! models.py $ curl localhost:5000/spot { "image_url": "http://example.com/rover.jpg", "name": "Rover", "age": 1 } -
BENEFIT: DATA VALIDATION bit.ly/flask-api-pycon-2016 Automatic, complete error messages! $ curl
-X POST localhost:5000/ { "image_url": [ "Missing data for required field." ], "name": [ "Missing data for required field." ] } (“age” is optional, so leaving it off is not an error) -
GET /rover HTTP/1.1 Host: localhost:5000 User-Agent: curl/7.43.0 Accept: */* HTTP
Request Who is the user? No idea. Authorized HTTP Request Look up the API key to find the user! HOW DO I LOGIN? bit.ly/flask-api-pycon-2016 GET /rover HTTP/1.1 Host: localhost:5000 User-Agent: curl/7.43.0 Accept: */* Authorized: my-api-key -
USER MODEL models.py bit.ly/flask-api-pycon-2016 from flask_sqlalchemy import SQLAlchemy from flask_login
import UserMixin db = SQLAlchemy() class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) api_key = db.Column(db.String(64), unique=True, index=True) class Puppy(db.Model): [...] -
INTEGRATE FLASK-LOGIN bit.ly/flask-api-pycon-2016 from flask import Flask from flask_login import
LoginManager from models import db, User from schemas import ma app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///puppy.db" db.init_app(app) ma.init_app(app) login_manager = LoginManager() login_manager.init_app(app) @login_manager.request_loader def load_user_from_request(request): api_key = request.headers.get('Authorization') if not api_key: return None return User.query.filter_by(api_key=api_key).first() -
TEST IT OUT $ curl localhost:5000/whoami { "name": "anonymous" }
$ curl localhost:5000/whoami \ -H "Authorization: abc123" { "name": "Foo Bar" } (after creating the User object in the database) bit.ly/flask-api-pycon-2016 from flask_login import current_user @app.route("/whoami") def who_am_i(): if current_user.is_authenticated: name = current_user.name else: name = "anonymous" return jsonify({"name": name}) -
LOGIN-PROTECTED VIEWS $ curl localhost:5000/rover <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML
3.2 Final//EN"> <title>401 Unauthorized</title> <h1>Unauthorized</h1> $ curl localhost:5000/rover -H "Authorization: abc123" { "image_url": "http://example.com/rover.jpg", "name": "Rover", "age": 1 } from flask_login import login_required @app.route("/<slug>") @login_required def get_puppy(slug): puppy = Puppy.query.filter_by(slug=slug).first_or_404() return puppy_schema.jsonify(puppy) -
LET’S JSONIFY THAT ERROR $ curl localhost:5000/rover { "error": "unauthorized"
} bit.ly/flask-api-pycon-2016 @app.errorhandler(401) def unauthorized(error): resp = jsonify({"error": "unauthorized"}) resp.status_code = 401 return resp -
DAY FIVE Pagination with Flask-SQLAlchemy Rate Limiting with Flask-Limiter Swagger
documentation with Flask-APISpec …but I’m out of time! bit.ly/flask-api-pycon-2016