Prototyping APIs with Flask

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.

42c77de97965d620ba0b6ae624c3ba7b?s=128

David Baumgold

April 26, 2016
Tweet

Transcript

  1. 1.

    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
  2. 6.

    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
  3. 7.

    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
  4. 9.

    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
  5. 10.

    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()
  6. 11.
  7. 12.

    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()
  8. 13.

    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()
  9. 14.
  10. 15.

    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()
  11. 21.

    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)
  12. 22.

    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
  13. 23.

    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
  14. 24.

    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
  15. 25.

    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
  16. 27.

    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()
  17. 29.

    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()
  18. 31.

    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()
  19. 33.

    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
  20. 34.

    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" }
  21. 36.

    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
  22. 38.

    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
  23. 39.

    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
  24. 40.

    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)
  25. 41.

    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)
  26. 42.

    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
  27. 43.

    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
  28. 44.

    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"})
  29. 45.

    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
  30. 46.

    $ 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)
  31. 47.

    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 }
  32. 48.

    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)
  33. 51.

    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
  34. 52.

    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): [...]
  35. 53.

    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()
  36. 54.

    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})
  37. 55.

    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)
  38. 56.

    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
  39. 57.

    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