├── 9781484250211.jpg ├── Contributing.md ├── LICENSE.txt ├── README.md ├── ch-1code └── code-1.py ├── ch2-code ├── .vscode │ └── settings.json ├── code-2-1.py ├── code-2-app-mongo.py └── code-2-app.py ├── ch3-code ├── .DS_Store ├── __init__.py ├── api │ ├── .DS_Store │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── config.py │ │ └── config.pyc │ ├── models │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── routes │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ └── utils │ │ ├── __init__.py │ │ ├── database.py │ │ └── responses.py ├── main.py ├── run.py └── text.log ├── ch4-code ├── .DS_Store ├── .vscode │ └── settings.json ├── __init__.py ├── api │ ├── .DS_Store │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── config.py │ │ └── config.pyc │ ├── models │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── routes │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ └── utils │ │ ├── __init__.py │ │ ├── database.py │ │ ├── responses.py │ │ └── token.py ├── main.py ├── run.py └── text.log ├── ch5-code ├── .DS_Store ├── .vscode │ └── settings.json ├── __init__.py ├── api │ ├── .DS_Store │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── config.py │ │ └── config.pyc │ ├── models │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── routes │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── tests │ │ ├── .DS_Store │ │ ├── __init__.py │ │ ├── test_authors.py │ │ └── test_users.py │ └── utils │ │ ├── __init__.py │ │ ├── database.py │ │ ├── responses.py │ │ ├── test_base.py │ │ └── token.py ├── main.py ├── run.py └── text.log ├── ch6-code ├── .DS_Store ├── .vscode │ └── settings.json ├── Procfile ├── __init__.py ├── api │ ├── .DS_Store │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── config.py │ │ └── config.pyc │ ├── models │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── routes │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── tests │ │ ├── .DS_Store │ │ ├── __init__.py │ │ ├── test_authors.py │ │ └── test_users.py │ └── utils │ │ ├── __init__.py │ │ ├── database.py │ │ ├── responses.py │ │ ├── test_base.py │ │ └── token.py ├── code-apache.conf ├── code-app.yaml ├── code-flask-app-gunicorn.service ├── code-flask-app.service ├── code-nginx.conf ├── flask-app.ini ├── main.py ├── run.py └── text.log ├── ch7-code ├── .DS_Store ├── .vscode │ └── settings.json ├── Procfile ├── __init__.py ├── api │ ├── .DS_Store │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── config.py │ │ └── config.pyc │ ├── models │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── routes │ │ ├── __init__.py │ │ ├── authors.py │ │ ├── books.py │ │ └── users.py │ ├── tests │ │ ├── .DS_Store │ │ ├── __init__.py │ │ ├── test_authors.py │ │ └── test_users.py │ └── utils │ │ ├── __init__.py │ │ ├── database.py │ │ ├── responses.py │ │ ├── test_base.py │ │ └── token.py ├── code-apache.conf ├── code-app.yaml ├── code-flask-app-gunicorn.service ├── code-flask-app.service ├── code-newrelic.ini ├── code-nginx.conf ├── flask-app.ini ├── main.py ├── run.py └── text.log └── errata.md /9781484250211.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/9781484250211.jpg -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Freeware License, some rights reserved 2 | 3 | Copyright (c) 2019 Kunal Relan 4 | 5 | Permission is hereby granted, free of charge, to anyone obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to work with the Software within the limits of freeware distribution and fair use. 8 | This includes the rights to use, copy, and modify the Software for personal use. 9 | Users are also allowed and encouraged to submit corrections and modifications 10 | to the Software for the benefit of other users. 11 | 12 | It is not allowed to reuse, modify, or redistribute the Software for 13 | commercial use in any way, or for a user’s educational materials such as books 14 | or blog articles without prior permission from the copyright holder. 15 | 16 | The above copyright notice and this permission notice need to be included 17 | in all copies or substantial portions of the software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Building REST APIs with Flask*](https://www.apress.com/9781484250211) by Kunal Relan (Apress, 2019). 4 | 5 | [comment]: #cover 6 | ![Cover image](9781484250211.jpg) 7 | 8 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 9 | 10 | ## Releases 11 | 12 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 13 | 14 | ## Contributions 15 | 16 | See the file Contributing.md for more information on how you can contribute to this repository. -------------------------------------------------------------------------------- /ch-1code/code-1.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | 7 | def hello_world(): 8 | return 'Hello, From Flask!' 9 | 10 | if __name__== '__main__': 11 | app.run() 12 | -------------------------------------------------------------------------------- /ch2-code/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "autopep8" 3 | } -------------------------------------------------------------------------------- /ch2-code/code-2-1.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | app = Flask(__name__) 5 | 6 | app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://:@:/' 7 | 8 | db = SQLAlchemy(app) 9 | 10 | if __name__ == "__main__": 11 | app.run(debug=True) -------------------------------------------------------------------------------- /ch2-code/code-2-app-mongo.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, make_response 2 | from flask_mongoengine import MongoEngine 3 | from marshmallow import Schema, fields, post_load 4 | from bson import ObjectId 5 | 6 | app = Flask(__name__) 7 | app.config['MONGODB_DB'] = 'DB_NAME' 8 | db = MongoEngine(app) 9 | 10 | Schema.TYPE_MAPPING[ObjectId] = fields.String 11 | 12 | class Authors(db.Document): 13 | name = db.StringField() 14 | specialisation = db.StringField() 15 | 16 | class AuthorsSchema(Schema): 17 | name = fields.String(required=True) 18 | specialisation = fields.String(required=True) 19 | 20 | @app.route('/authors', methods = ['GET']) 21 | def index(): 22 | get_authors = Authors.objects.all() 23 | author_schema = AuthorsSchema(many=True, only=['id', 'name', 'specialisation']) 24 | authors, error = author_schema.dump(get_authors) 25 | return make_response(jsonify({"authors": authors})) 26 | 27 | @app.route('/authors/', methods = ['GET']) 28 | def get_author_by_id(id): 29 | get_author = Authors.objects.get_or_404(id=ObjectId(id)) 30 | author_schema = AuthorsSchema(only=['id', 'name', 'specialisation']) 31 | author, error = author_schema.dump(get_author) 32 | return make_response(jsonify({"author": author})) 33 | 34 | @app.route('/authors/', methods = ['PUT']) 35 | def update_author_by_id(id): 36 | data = request.get_json() 37 | get_author = Authors.objects.get(id=ObjectId(id)) 38 | if data.get('specialisation'): 39 | get_author.specialisation = data['specialisation'] 40 | if data.get('name'): 41 | get_author.name = data['name'] 42 | get_author.save() 43 | get_author.reload() 44 | author_schema = AuthorsSchema(only=['id', 'name', 'specialisation']) 45 | author, error = author_schema.dump(get_author) 46 | return make_response(jsonify({"author": author})) 47 | 48 | @app.route('/authors/', methods = ['DELETE']) 49 | def delete_author_by_id(id): 50 | Authors.objects(id=ObjectId(id)).delete() 51 | return make_response("",204) 52 | 53 | @app.route('/authors', methods = ['POST']) 54 | def create_author(): 55 | data = request.get_json() 56 | author = Authors(name=data['name'],specialisation=data['specialisation']) 57 | author.save() 58 | author_schema = AuthorsSchema(only=['id','name', 'specialisation']) 59 | authors, error = author_schema.dump(author) 60 | return make_response(jsonify({"author": authors}),201) 61 | 62 | if __name__ == "__main__": 63 | app.run(debug=True) -------------------------------------------------------------------------------- /ch2-code/code-2-app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, make_response 2 | from flask_sqlalchemy import SQLAlchemy 3 | from marshmallow_sqlalchemy import ModelSchema 4 | from marshmallow import fields 5 | 6 | app = Flask(__name__) 7 | 8 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' 9 | db = SQLAlchemy(app) 10 | 11 | class Authors(db.Model): 12 | id = db.Column(db.Integer, primary_key=True) 13 | name = db.Column(db.String(20)) 14 | specialisation = db.Column(db.String(50)) 15 | 16 | def create(self): 17 | db.session.add(self) 18 | db.session.commit() 19 | return self 20 | 21 | def __init__(self, name, specialisation): 22 | self.name = name 23 | self.specialisation = specialisation 24 | 25 | def __repr__(self): 26 | return '' % self.id 27 | 28 | db.create_all() 29 | 30 | class AuthorsSchema(ModelSchema): 31 | class Meta(ModelSchema.Meta): 32 | model = Authors 33 | sqla_session = db.session 34 | 35 | id = fields.Number(dump_only=True) 36 | name = fields.String(required=True) 37 | specialisation = fields.String(required=True) 38 | 39 | @app.route('/authors', methods = ['GET']) 40 | def index(): 41 | get_authors = Authors.query.all() 42 | author_schema = AuthorsSchema(many=True) 43 | authors, error = author_schema.dump(get_authors) 44 | return make_response(jsonify({"authors": authors})) 45 | 46 | @app.route('/authors/', methods = ['GET']) 47 | def get_author_by_id(id): 48 | get_author = Authors.query.get(id) 49 | author_schema = AuthorsSchema() 50 | author, error = author_schema.dump(get_author) 51 | return make_response(jsonify({"author": author})) 52 | 53 | @app.route('/authors/', methods = ['PUT']) 54 | def update_author_by_id(id): 55 | data = request.get_json() 56 | get_author = Authors.query.get(id) 57 | if data.get('specialisation'): 58 | get_author.specialisation = data['specialisation'] 59 | if data.get('name'): 60 | get_author.name = data['name'] 61 | 62 | db.session.add(get_author) 63 | db.session.commit() 64 | author_schema = AuthorsSchema(only=['id', 'name', 'specialisation']) 65 | author, error = author_schema.dump(get_author) 66 | return make_response(jsonify({"author": author})) 67 | 68 | @app.route('/authors/', methods = ['DELETE']) 69 | def delete_author_by_id(id): 70 | get_author = Authors.query.get(id) 71 | db.session.delete(get_author) 72 | db.session.commit() 73 | return make_response("",204) 74 | 75 | @app.route('/authors', methods = ['POST']) 76 | def create_author(): 77 | data = request.get_json() 78 | author_schema = AuthorsSchema() 79 | author, error = author_schema.load(data) 80 | result = author_schema.dump(author.create()).data 81 | return make_response(jsonify({"author": result}),200) 82 | 83 | if __name__ == "__main__": 84 | app.run(debug=True) -------------------------------------------------------------------------------- /ch3-code/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/.DS_Store -------------------------------------------------------------------------------- /ch3-code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/__init__.py -------------------------------------------------------------------------------- /ch3-code/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/.DS_Store -------------------------------------------------------------------------------- /ch3-code/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/__init__.py -------------------------------------------------------------------------------- /ch3-code/api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/config/__init__.py -------------------------------------------------------------------------------- /ch3-code/api/config/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/config/__init__.pyc -------------------------------------------------------------------------------- /ch3-code/api/config/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | TESTING = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | 7 | class ProductionConfig(Config): 8 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 9 | SQLALCHEMY_ECHO = False 10 | JWT_SECRET_KEY = 'JWT-SECRET' 11 | SECRET_KEY= 'SECRET-KEY' 12 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 13 | 14 | 15 | 16 | class DevelopmentConfig(Config): 17 | DEBUG = True 18 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 19 | SQLALCHEMY_ECHO = False 20 | JWT_SECRET_KEY = 'JWT-SECRET' 21 | SECRET_KEY= 'SECRET-KEY' 22 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 23 | 24 | 25 | class TestingConfig(Config): 26 | TESTING = True 27 | SQLALCHEMY_ECHO = False 28 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 29 | SQLALCHEMY_ECHO = False 30 | JWT_SECRET_KEY = 'JWT-SECRET' 31 | SECRET_KEY= 'SECRET-KEY' 32 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' -------------------------------------------------------------------------------- /ch3-code/api/config/config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/config/config.pyc -------------------------------------------------------------------------------- /ch3-code/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/models/__init__.py -------------------------------------------------------------------------------- /ch3-code/api/models/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | from api.models.books import BookSchema 8 | 9 | 10 | class Author(db.Model): 11 | __tablename__ = 'authors' 12 | 13 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 14 | first_name = db.Column(db.String(20)) 15 | last_name = db.Column(db.String(20)) 16 | created = db.Column(db.DateTime, server_default=db.func.now()) 17 | books = db.relationship('Book', backref='Author', cascade="all, delete-orphan") 18 | 19 | def __init__(self, first_name, last_name, books=[]): 20 | self.first_name = first_name 21 | self.last_name = last_name 22 | self.books = books 23 | 24 | def create(self): 25 | db.session.add(self) 26 | db.session.commit() 27 | return self 28 | 29 | 30 | class AuthorSchema(ModelSchema): 31 | class Meta(ModelSchema.Meta): 32 | model = Author 33 | sqla_session = db.session 34 | 35 | id = fields.Number(dump_only=True) 36 | first_name = fields.String(required=True) 37 | last_name = fields.String(required=True) 38 | created = fields.String(dump_only=True) 39 | books = fields.Nested(BookSchema, many=True, only=['title','year','id']) 40 | 41 | 42 | -------------------------------------------------------------------------------- /ch3-code/api/models/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | 8 | 9 | class Book(db.Model): 10 | __tablename__ = 'books' 11 | 12 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 13 | title = db.Column(db.String(50)) 14 | year = db.Column(db.Integer) 15 | author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False) 16 | 17 | def __init__(self, title, year, author_id=None): 18 | self.title = title 19 | self.year = year 20 | self.author_id = author_id 21 | 22 | def create(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | return self 26 | 27 | 28 | class BookSchema(ModelSchema): 29 | class Meta(ModelSchema.Meta): 30 | model = Book 31 | sqla_session = db.session 32 | 33 | id = fields.Number(dump_only=True) 34 | title = fields.String(required=True) 35 | year = fields.Integer(required=True) 36 | author_id = fields.Integer() 37 | -------------------------------------------------------------------------------- /ch3-code/api/models/users.py: -------------------------------------------------------------------------------- 1 | from api.utils.database import db 2 | from passlib.hash import pbkdf2_sha256 as sha256 3 | from marshmallow_sqlalchemy import ModelSchema 4 | from marshmallow import fields 5 | 6 | 7 | class User(db.Model): 8 | __tablename__ = 'users' 9 | 10 | id = db.Column(db.Integer, primary_key = True) 11 | username = db.Column(db.String(120), unique = True, nullable = False) 12 | password = db.Column(db.String(120), nullable = False) 13 | def create(self): 14 | db.session.add(self) 15 | db.session.commit() 16 | return self 17 | 18 | @classmethod 19 | def find_by_username(cls, username): 20 | return cls.query.filter_by(username = username).first() 21 | 22 | @staticmethod 23 | def generate_hash(password): 24 | return sha256.hash(password) 25 | 26 | @staticmethod 27 | def verify_hash(password, hash): 28 | return sha256.verify(password, hash) 29 | 30 | class UserSchema(ModelSchema): 31 | class Meta(ModelSchema.Meta): 32 | model = User 33 | sqla_session = db.session 34 | 35 | id = fields.Number(dump_only=True) 36 | username = fields.String(required=True) 37 | email = fields.String(required=True) -------------------------------------------------------------------------------- /ch3-code/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/routes/__init__.py -------------------------------------------------------------------------------- /ch3-code/api/routes/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint, request, url_for, current_app 5 | from api.utils.responses import response_with 6 | from api.utils import responses as resp 7 | from api.models.authors import Author, AuthorSchema 8 | from api.utils.database import db 9 | from flask_jwt_extended import jwt_required 10 | import os 11 | 12 | 13 | @author_routes.route('/', methods=['POST']) 14 | @jwt_required 15 | def create_author(): 16 | try: 17 | data = request.get_json() 18 | author_schema = AuthorSchema() 19 | author, error = author_schema.load(data) 20 | result = author_schema.dump(author.create()).data 21 | return response_with(resp.SUCCESS_201, value={"author": result}) 22 | except Exception as e: 23 | return response_with(resp.INVALID_INPUT_422) 24 | 25 | @author_routes.route('/', methods=['GET']) 26 | def get_author_list(): 27 | fetched = Author.query.all() 28 | author_schema = AuthorSchema(many=True, only=['first_name', 'last_name', 'id']) 29 | authors, error = author_schema.dump(fetched) 30 | return response_with(resp.SUCCESS_200, value={"authors": authors}) 31 | 32 | @author_routes.route('/', methods=['GET']) 33 | def get_author_detail(author_id): 34 | fetched = Author.query.get_or_404(author_id) 35 | author_schema = AuthorSchema() 36 | author, error = author_schema.dump(fetched) 37 | return response_with(resp.SUCCESS_200, value={"author": author}) 38 | 39 | @author_routes.route('/', methods=['PUT']) 40 | @jwt_required 41 | def update_author_detail(id): 42 | data = request.get_json() 43 | get_author = Author.query.get_or_404(id) 44 | get_author.first_name = data['first_name'] 45 | get_author.last_name = data['last_name'] 46 | db.session.add(get_author) 47 | db.session.commit() 48 | author_schema = AuthorSchema() 49 | author, error = author_schema.dump(get_author) 50 | return response_with(resp.SUCCESS_200, value={"author": author}) 51 | 52 | @author_routes.route('/', methods=['PATCH']) 53 | def modify_author_detail(id): 54 | data = request.get_json() 55 | get_author = Author.query.get(id) 56 | if data.get('first_name'): 57 | get_author.first_name = data['first_name'] 58 | if data.get('last_name'): 59 | get_author.last_name = data['last_name'] 60 | db.session.add(get_author) 61 | db.session.commit() 62 | author_schema = AuthorSchema() 63 | author, error = author_schema.dump(get_author) 64 | return response_with(resp.SUCCESS_200, value={"author": author}) 65 | 66 | @author_routes.route('/', methods=['DELETE']) 67 | def delete_author(id): 68 | get_author = Author.query.get_or_404(id) 69 | db.session.delete(get_author) 70 | db.session.commit() 71 | return response_with(resp.SUCCESS_204) -------------------------------------------------------------------------------- /ch3-code/api/routes/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from api.utils.responses import response_with 7 | from api.utils import responses as resp 8 | from api.models.books import Book, BookSchema 9 | from api.utils.database import db 10 | from flask_jwt_extended import (jwt_required, jwt_refresh_token_required, get_jwt_identity, get_raw_jwt) 11 | 12 | book_routes = Blueprint("book_routes", __name__) 13 | 14 | 15 | @book_routes.route('/', methods=['POST']) 16 | @jwt_required 17 | def create_book(): 18 | try: 19 | data = request.get_json() 20 | book_schema = BookSchema() 21 | book, error = book_schema.load(data) 22 | result = book_schema.dump(book.create()).data 23 | return response_with(resp.SUCCESS_201, value={"book": result}) 24 | except Exception as e: 25 | print e 26 | return response_with(resp.INVALID_INPUT_422) 27 | 28 | 29 | @book_routes.route('/', methods=['GET']) 30 | def get_book_list(): 31 | fetched = Book.query.all() 32 | book_schema = BookSchema(many=True, only=['author_id','title', 'year']) 33 | books, error = book_schema.dump(fetched) 34 | return response_with(resp.SUCCESS_200, value={"books": books}) 35 | 36 | 37 | @book_routes.route('/', methods=['GET']) 38 | def get_book_detail(id): 39 | fetched = Book.query.get_or_404(id) 40 | book_schema = BookSchema() 41 | books, error = book_schema.dump(fetched) 42 | return response_with(resp.SUCCESS_200, value={"books": books}) 43 | 44 | @book_routes.route('/', methods=['PUT']) 45 | @jwt_required 46 | def update_book_detail(id): 47 | data = request.get_json() 48 | get_book = Book.query.get_or_404(id) 49 | get_book.title = data['title'] 50 | get_book.year = data['year'] 51 | db.session.add(get_book) 52 | db.session.commit() 53 | book_schema = BookSchema() 54 | book, error = book_schema.dump(get_book) 55 | return response_with(resp.SUCCESS_200, value={"book": book}) 56 | 57 | @book_routes.route('/', methods=['PATCH']) 58 | @jwt_required 59 | def modify_book_detail(id): 60 | data = request.get_json() 61 | get_book = Book.query.get_or_404(id) 62 | if data.get('title'): 63 | get_book.title = data['title'] 64 | if data.get('year'): 65 | get_book.year = data['year'] 66 | db.session.add(get_book) 67 | db.session.commit() 68 | book_schema = BookSchema() 69 | book, error = book_schema.dump(get_book) 70 | return response_with(resp.SUCCESS_200, value={"book": book}) 71 | 72 | @book_routes.route('/', methods=['DELETE']) 73 | @jwt_required 74 | def delete_book(id): 75 | get_book = Book.query.get_or_404(id) 76 | db.session.delete(get_book) 77 | db.session.commit() 78 | return response_with(resp.SUCCESS_204) 79 | -------------------------------------------------------------------------------- /ch3-code/api/routes/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from flask import url_for, render_template_string 7 | from api.utils.responses import response_with 8 | from api.utils import responses as resp 9 | from api.models.users import User, UserSchema 10 | from api.utils.database import db 11 | from flask_jwt_extended import create_access_token 12 | from api.utils.token import generate_verification_token, confirm_verification_token 13 | from api.utils.email import send_email 14 | import datetime 15 | 16 | user_routes = Blueprint("user_routes", __name__) 17 | 18 | @user_routes.route('/', methods=['POST']) 19 | def create_user(): 20 | try: 21 | data = request.get_json() 22 | data['password'] = User.generate_hash(data['password']) 23 | user_schmea = UserSchema() 24 | user, error = user_schmea.load(data) 25 | result = user_schmea.dump(user.create()).data 26 | return response_with(resp.SUCCESS_201) 27 | except Exception as e: 28 | print e 29 | return response_with(resp.INVALID_INPUT_422) 30 | 31 | @user_routes.route('/login', methods=['POST']) 32 | def authenticate_user(): 33 | try: 34 | data = request.get_json() 35 | current_user = User.find_by_username(data['username']) 36 | if not current_user: 37 | return response_with(resp.SERVER_ERROR_404) 38 | if User.verify_hash(data['password'], current_user.password): 39 | access_token = create_access_token(identity = data['username']) 40 | return response_with(resp.SUCCESS_201, value={'message': 'Logged in as {}'.format(current_user.username), "access_token": access_token}) 41 | else: 42 | return response_with(resp.UNAUTHORIZED_401) 43 | except Exception as e: 44 | print e 45 | return response_with(resp.INVALID_INPUT_422) 46 | -------------------------------------------------------------------------------- /ch3-code/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch3-code/api/utils/__init__.py -------------------------------------------------------------------------------- /ch3-code/api/utils/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask_sqlalchemy import SQLAlchemy 5 | db = SQLAlchemy() -------------------------------------------------------------------------------- /ch3-code/api/utils/responses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import make_response, jsonify 5 | 6 | INVALID_FIELD_NAME_SENT_422 = { 7 | "http_code": 422, 8 | "code": "invalidField", 9 | "message": "Invalid fields found" 10 | } 11 | 12 | INVALID_INPUT_422 = { 13 | "http_code": 422, 14 | "code": "invalidInput", 15 | "message": "Invalid input" 16 | } 17 | 18 | MISSING_PARAMETERS_422 = { 19 | "http_code": 422, 20 | "code": "missingParameter", 21 | "message": "Missing parameters." 22 | } 23 | 24 | BAD_REQUEST_400 = { 25 | "http_code": 400, 26 | "code": "badRequest", 27 | "message": "Bad request" 28 | } 29 | 30 | SERVER_ERROR_500 = { 31 | "http_code": 500, 32 | "code": "serverError", 33 | "message": "Server error" 34 | } 35 | 36 | SERVER_ERROR_404 = { 37 | "http_code": 404, 38 | "code": "notFound", 39 | "message": "Resource not found" 40 | } 41 | 42 | FORBIDDEN_403 = { 43 | "http_code": 403, 44 | "code": "notAuthorized", 45 | "message": "You are not authorised to execute this." 46 | } 47 | UNAUTHORIZED_401 = { 48 | "http_code": 401, 49 | "code": "notAuthorized", 50 | "message": "Invalid authentication." 51 | } 52 | 53 | NOT_FOUND_HANDLER_404 = { 54 | "http_code": 404, 55 | "code": "notFound", 56 | "message": "route not found" 57 | } 58 | 59 | SUCCESS_200 = { 60 | 'http_code': 200, 61 | 'code': 'success' 62 | } 63 | 64 | SUCCESS_201 = { 65 | 'http_code': 201, 66 | 'code': 'success' 67 | } 68 | 69 | SUCCESS_204 = { 70 | 'http_code': 204, 71 | 'code': 'success' 72 | } 73 | 74 | 75 | def response_with(response, value=None, message=None, error=None, headers={}, pagination=None): 76 | result = {} 77 | if value is not None: 78 | result.update(value) 79 | 80 | if response.get('message', None) is not None: 81 | result.update({'message': response['message']}) 82 | 83 | result.update({'code': response['code']}) 84 | 85 | if error is not None: 86 | result.update({'errors': error}) 87 | 88 | if pagination is not None: 89 | result.update({'pagination': pagination}) 90 | 91 | headers.update({'Access-Control-Allow-Origin': '*'}) 92 | headers.update({'server': 'Flask REST API'}) 93 | 94 | return make_response(jsonify(result), response['http_code'], headers) 95 | -------------------------------------------------------------------------------- /ch3-code/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask 4 | from flask import jsonify 5 | from apispec.ext.marshmallow import MarshmallowPlugin 6 | from apispec_webframeworks.flask import FlaskPlugin 7 | from api.utils.database import db 8 | from api.utils.responses import response_with 9 | import api.utils.responses as resp 10 | from api.routes.authors import author_routes 11 | from api.routes.books import book_routes 12 | from api.routes.users import user_routes 13 | from flask_jwt_extended import JWTManager 14 | from api.config.config import DevelopmentConfig, ProductionConfig, TestingConfig 15 | 16 | app = Flask(__name__) 17 | 18 | 19 | if os.environ.get('WORK_ENV') == 'PROD': 20 | app_config = ProductionConfig 21 | elif os.environ.get('WORK_ENV') == 'TEST': 22 | app_config = TestingConfig 23 | else: 24 | app_config = DevelopmentConfig 25 | 26 | app.config.from_object(app_config) 27 | 28 | db.init_app(app) 29 | with app.app_context(): 30 | db.create_all() 31 | app.register_blueprint(author_routes, url_prefix='/api/authors') 32 | app.register_blueprint(book_routes, url_prefix='/api/books') 33 | app.register_blueprint(user_routes, url_prefix='/api/users') 34 | 35 | 36 | # START GLOBAL HTTP CONFIGURATIONS 37 | @app.after_request 38 | def add_header(response): 39 | return response 40 | 41 | @app.errorhandler(400) 42 | def bad_request(e): 43 | logging.error(e) 44 | return response_with(resp.BAD_REQUEST_400) 45 | 46 | @app.errorhandler(500) 47 | def server_error(e): 48 | logging.error(e) 49 | return response_with(resp.SERVER_ERROR_500) 50 | 51 | @app.errorhandler(404) 52 | def not_found(e): 53 | logging.error(e) 54 | return response_with(resp.SERVER_ERROR_404) 55 | 56 | # END GLOBAL HTTP CONFIGURATIONS 57 | 58 | 59 | jwt = JWTManager(app) 60 | db.init_app(app) 61 | with app.app_context(): 62 | # from api.models import * 63 | db.create_all() 64 | 65 | if __name__ == "__main__": 66 | app.run(port=5000, host="0.0.0.0", use_reloader=False) -------------------------------------------------------------------------------- /ch3-code/run.py: -------------------------------------------------------------------------------- 1 | from main import app as application 2 | 3 | if __name__ == "__main__": 4 | application.run() -------------------------------------------------------------------------------- /ch3-code/text.log: -------------------------------------------------------------------------------- 1 | *** Starting uWSGI 2.0.18 (64bit) on [Wed Apr 24 17:26:41 2019] *** 2 | compiled with version: 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4) on 23 April 2019 17:13:02 3 | os: Darwin-18.2.0 Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 4 | nodename: Kunals-MacBook-Pro.local 5 | machine: x86_64 6 | clock source: unix 7 | pcre jit disabled 8 | detected number of CPU cores: 4 9 | current working directory: /Users/kunalrelan/Desktop/flask-api-starter/src 10 | detected binary path: /usr/local/bin/uwsgi 11 | *** WARNING: you are running uWSGI without its master process manager *** 12 | your processes number limit is 709 13 | your memory page size is 4096 bytes 14 | detected max file descriptor number: 10240 15 | lock engine: OSX spinlocks 16 | thunder lock: disabled (you can enable it with --thunder-lock) 17 | uwsgi socket 0 bound to TCP address 0.0.0.0:5000 fd 3 18 | Python version: 2.7.10 (default, Aug 17 2018, 19:45:58) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] 19 | *** Python threads support is disabled. You can enable it with --enable-threads *** 20 | Python main interpreter initialized at 0x7ff28df01200 21 | your server socket listen backlog is limited to 100 connections 22 | your mercy for graceful operations on workers is 60 seconds 23 | mapped 72888 bytes (71 KB) for 1 cores 24 | *** Operational MODE: single process *** 25 | unable to load app 0 (mountpoint='') (callable not found or import error) 26 | *** no app loaded. going in full dynamic mode *** 27 | *** uWSGI is running in multiple interpreter mode *** 28 | spawned uWSGI worker 1 (and the only) (pid: 50108, cores: 1) 29 | --- no python application found, check your startup logs for errors --- 30 | [pid: 50108|app: -1|req: -1/1] 127.0.0.1 () {38 vars in 1073 bytes} [Wed Apr 24 17:26:48 2019] GET /api/docs => generated 21 bytes in 4 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 31 | --- no python application found, check your startup logs for errors --- 32 | [pid: 50108|app: -1|req: -1/2] 127.0.0.1 () {40 vars in 1033 bytes} [Wed Apr 24 17:26:48 2019] GET /favicon.ico => generated 21 bytes in 509 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 33 | -------------------------------------------------------------------------------- /ch4-code/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/.DS_Store -------------------------------------------------------------------------------- /ch4-code/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python2.7" 3 | } -------------------------------------------------------------------------------- /ch4-code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/__init__.py -------------------------------------------------------------------------------- /ch4-code/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/.DS_Store -------------------------------------------------------------------------------- /ch4-code/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/__init__.py -------------------------------------------------------------------------------- /ch4-code/api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/config/__init__.py -------------------------------------------------------------------------------- /ch4-code/api/config/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/config/__init__.pyc -------------------------------------------------------------------------------- /ch4-code/api/config/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | TESTING = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | 7 | class ProductionConfig(Config): 8 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 9 | SQLALCHEMY_ECHO = False 10 | JWT_SECRET_KEY = 'JWT-SECRET' 11 | SECRET_KEY= 'SECRET-KEY' 12 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 13 | MAIL_DEFAULT_SENDER= '' 14 | MAIL_SERVER= '' 15 | MAIL_PORT= '' 16 | MAIL_USERNAME= ' 17 | MAIL_PASSWORD= '' 18 | MAIL_USE_TLS= False 19 | MAIL_USE_SSL= True 20 | UPLOAD_FOLDER= '' 21 | 22 | 23 | class DevelopmentConfig(Config): 24 | DEBUG = True 25 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 26 | SQLALCHEMY_ECHO = False 27 | JWT_SECRET_KEY = 'JWT-SECRET' 28 | SECRET_KEY= 'SECRET-KEY' 29 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 30 | MAIL_DEFAULT_SENDER= '' 31 | MAIL_SERVER= '' 32 | MAIL_PORT= '' 33 | MAIL_USERNAME= ' 34 | MAIL_PASSWORD= '' 35 | MAIL_USE_TLS= False 36 | MAIL_USE_SSL= True 37 | UPLOAD_FOLDER= '' 38 | 39 | 40 | class TestingConfig(Config): 41 | TESTING = True 42 | SQLALCHEMY_ECHO = False 43 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 44 | SQLALCHEMY_ECHO = False 45 | JWT_SECRET_KEY = 'JWT-SECRET' 46 | SECRET_KEY= 'SECRET-KEY' 47 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 48 | MAIL_DEFAULT_SENDER= '' 49 | MAIL_SERVER= '' 50 | MAIL_PORT= '' 51 | MAIL_USERNAME= ' 52 | MAIL_PASSWORD= '' 53 | MAIL_USE_TLS= False 54 | MAIL_USE_SSL= True 55 | UPLOAD_FOLDER= '' -------------------------------------------------------------------------------- /ch4-code/api/config/config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/config/config.pyc -------------------------------------------------------------------------------- /ch4-code/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/models/__init__.py -------------------------------------------------------------------------------- /ch4-code/api/models/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | from api.models.books import BookSchema 8 | 9 | 10 | class Author(db.Model): 11 | __tablename__ = 'authors' 12 | 13 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 14 | first_name = db.Column(db.String(20)) 15 | last_name = db.Column(db.String(20)) 16 | avatar = db.Column(db.String(20), nullable=True) 17 | created = db.Column(db.DateTime, server_default=db.func.now()) 18 | books = db.relationship('Book', backref='Author', cascade="all, delete-orphan") 19 | 20 | def __init__(self, first_name, last_name, books=[]): 21 | self.first_name = first_name 22 | self.last_name = last_name 23 | self.books = books 24 | 25 | def create(self): 26 | db.session.add(self) 27 | db.session.commit() 28 | return self 29 | 30 | 31 | class AuthorSchema(ModelSchema): 32 | class Meta(ModelSchema.Meta): 33 | model = Author 34 | sqla_session = db.session 35 | 36 | id = fields.Number(dump_only=True) 37 | first_name = fields.String(required=True) 38 | last_name = fields.String(required=True) 39 | avatar = fields.String(dump_only=True) 40 | created = fields.String(dump_only=True) 41 | books = fields.Nested(BookSchema, many=True, only=['title','year','id']) 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ch4-code/api/models/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | 8 | 9 | class Book(db.Model): 10 | __tablename__ = 'books' 11 | 12 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 13 | title = db.Column(db.String(50)) 14 | year = db.Column(db.Integer) 15 | author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False) 16 | 17 | def __init__(self, title, year, author_id=None): 18 | self.title = title 19 | self.year = year 20 | self.author_id = author_id 21 | 22 | def create(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | return self 26 | 27 | 28 | class BookSchema(ModelSchema): 29 | class Meta(ModelSchema.Meta): 30 | model = Book 31 | sqla_session = db.session 32 | 33 | id = fields.Number(dump_only=True) 34 | title = fields.String(required=True) 35 | year = fields.Integer(required=True) 36 | author_id = fields.Integer() 37 | -------------------------------------------------------------------------------- /ch4-code/api/models/users.py: -------------------------------------------------------------------------------- 1 | class User(db.Model): 2 | __tablename__ = 'users' 3 | 4 | id = db.Column(db.Integer, primary_key = True) 5 | username = db.Column(db.String(120), unique = True, nullable = False) 6 | password = db.Column(db.String(120), nullable = False) 7 | isVerified = db.Column(db.Boolean, nullable=False, default=False) 8 | email = db.Column(db.String(120), unique = True, nullable = False) 9 | def create(self): 10 | db.session.add(self) 11 | db.session.commit() 12 | return self 13 | 14 | @classmethod 15 | def find_by_email(cls, email): 16 | return cls.query.filter_by(email = email).first() 17 | 18 | @classmethod 19 | def find_by_username(cls, username): 20 | print(cls) 21 | print(username) 22 | return cls.query.filter_by(username = username).first() 23 | 24 | @staticmethod 25 | def generate_hash(password): 26 | return sha256.hash(password) 27 | 28 | @staticmethod 29 | def verify_hash(password, hash): 30 | return sha256.verify(password, hash) 31 | 32 | class UserSchema(ModelSchema): 33 | class Meta(ModelSchema.Meta): 34 | model = User 35 | sqla_session = db.session 36 | 37 | id = fields.Number(dump_only=True) 38 | username = fields.String(required=True) 39 | email = fields.String(required=True) -------------------------------------------------------------------------------- /ch4-code/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/routes/__init__.py -------------------------------------------------------------------------------- /ch4-code/api/routes/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint, request, url_for, current_app 5 | from api.utils.responses import response_with 6 | from api.utils import responses as resp 7 | from api.models.authors import Author, AuthorSchema 8 | from api.utils.database import db 9 | from flask_jwt_extended import jwt_required 10 | import os 11 | 12 | 13 | @author_routes.route('/', methods=['POST']) 14 | @jwt_required 15 | def create_author(): 16 | try: 17 | data = request.get_json() 18 | author_schema = AuthorSchema() 19 | author, error = author_schema.load(data) 20 | result = author_schema.dump(author.create()).data 21 | return response_with(resp.SUCCESS_201, value={"author": result}) 22 | except Exception as e: 23 | return response_with(resp.INVALID_INPUT_422) 24 | 25 | @author_routes.route('/avatar/', methods=['POST']) 26 | @jwt_required 27 | def upsert_author_avatar(author_id): 28 | try: 29 | file = request.files['avatar'] 30 | if file and allowed_file(file.filename): 31 | filename = secure_filename(file.filename) 32 | file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename)) 33 | get_author = Author.query.get_or_404(author_id) 34 | get_author.avatar = url_for('uploaded_file', filename=filename, _external=True) 35 | db.session.add(get_author) 36 | db.session.commit() 37 | author_schema = AuthorSchema() 38 | author, error = author_schema.dump(get_author) 39 | return response_with(resp.SUCCESS_200, value={"author": author}) 40 | except Exception as e: 41 | print e 42 | return response_with(resp.INVALID_INPUT_422) 43 | 44 | @author_routes.route('/', methods=['GET']) 45 | def get_author_list(): 46 | fetched = Author.query.all() 47 | author_schema = AuthorSchema(many=True, only=['first_name', 'last_name', 'id']) 48 | authors, error = author_schema.dump(fetched) 49 | return response_with(resp.SUCCESS_200, value={"authors": authors}) 50 | 51 | @author_routes.route('/', methods=['GET']) 52 | def get_author_detail(author_id): 53 | fetched = Author.query.get_or_404(author_id) 54 | author_schema = AuthorSchema() 55 | author, error = author_schema.dump(fetched) 56 | return response_with(resp.SUCCESS_200, value={"author": author}) 57 | 58 | @author_routes.route('/', methods=['PUT']) 59 | @jwt_required 60 | def update_author_detail(id): 61 | data = request.get_json() 62 | get_author = Author.query.get_or_404(id) 63 | get_author.first_name = data['first_name'] 64 | get_author.last_name = data['last_name'] 65 | db.session.add(get_author) 66 | db.session.commit() 67 | author_schema = AuthorSchema() 68 | author, error = author_schema.dump(get_author) 69 | return response_with(resp.SUCCESS_200, value={"author": author}) 70 | 71 | @author_routes.route('/', methods=['PATCH']) 72 | def modify_author_detail(id): 73 | data = request.get_json() 74 | get_author = Author.query.get(id) 75 | if data.get('first_name'): 76 | get_author.first_name = data['first_name'] 77 | if data.get('last_name'): 78 | get_author.last_name = data['last_name'] 79 | db.session.add(get_author) 80 | db.session.commit() 81 | author_schema = AuthorSchema() 82 | author, error = author_schema.dump(get_author) 83 | return response_with(resp.SUCCESS_200, value={"author": author}) 84 | 85 | @author_routes.route('/', methods=['DELETE']) 86 | def delete_author(id): 87 | get_author = Author.query.get_or_404(id) 88 | db.session.delete(get_author) 89 | db.session.commit() 90 | return response_with(resp.SUCCESS_204) -------------------------------------------------------------------------------- /ch4-code/api/routes/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from api.utils.responses import response_with 7 | from api.utils import responses as resp 8 | from api.models.books import Book, BookSchema 9 | from api.utils.database import db 10 | from flask_jwt_extended import (jwt_required, jwt_refresh_token_required, get_jwt_identity, get_raw_jwt) 11 | 12 | book_routes = Blueprint("book_routes", __name__) 13 | 14 | 15 | @book_routes.route('/', methods=['POST']) 16 | @jwt_required 17 | def create_book(): 18 | try: 19 | data = request.get_json() 20 | book_schema = BookSchema() 21 | book, error = book_schema.load(data) 22 | result = book_schema.dump(book.create()).data 23 | return response_with(resp.SUCCESS_201, value={"book": result}) 24 | except Exception as e: 25 | print e 26 | return response_with(resp.INVALID_INPUT_422) 27 | 28 | 29 | @book_routes.route('/', methods=['GET']) 30 | def get_book_list(): 31 | fetched = Book.query.all() 32 | book_schema = BookSchema(many=True, only=['author_id','title', 'year']) 33 | books, error = book_schema.dump(fetched) 34 | return response_with(resp.SUCCESS_200, value={"books": books}) 35 | 36 | 37 | @book_routes.route('/', methods=['GET']) 38 | def get_book_detail(id): 39 | fetched = Book.query.get_or_404(id) 40 | book_schema = BookSchema() 41 | books, error = book_schema.dump(fetched) 42 | return response_with(resp.SUCCESS_200, value={"books": books}) 43 | 44 | @book_routes.route('/', methods=['PUT']) 45 | @jwt_required 46 | def update_book_detail(id): 47 | data = request.get_json() 48 | get_book = Book.query.get_or_404(id) 49 | get_book.title = data['title'] 50 | get_book.year = data['year'] 51 | db.session.add(get_book) 52 | db.session.commit() 53 | book_schema = BookSchema() 54 | book, error = book_schema.dump(get_book) 55 | return response_with(resp.SUCCESS_200, value={"book": book}) 56 | 57 | @book_routes.route('/', methods=['PATCH']) 58 | @jwt_required 59 | def modify_book_detail(id): 60 | data = request.get_json() 61 | get_book = Book.query.get_or_404(id) 62 | if data.get('title'): 63 | get_book.title = data['title'] 64 | if data.get('year'): 65 | get_book.year = data['year'] 66 | db.session.add(get_book) 67 | db.session.commit() 68 | book_schema = BookSchema() 69 | book, error = book_schema.dump(get_book) 70 | return response_with(resp.SUCCESS_200, value={"book": book}) 71 | 72 | @book_routes.route('/', methods=['DELETE']) 73 | @jwt_required 74 | def delete_book(id): 75 | get_book = Book.query.get_or_404(id) 76 | db.session.delete(get_book) 77 | db.session.commit() 78 | return response_with(resp.SUCCESS_204) 79 | -------------------------------------------------------------------------------- /ch4-code/api/routes/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from flask import url_for, render_template_string 7 | from api.utils.responses import response_with 8 | from api.utils import responses as resp 9 | from api.models.users import User, UserSchema 10 | from api.utils.database import db 11 | from flask_jwt_extended import create_access_token 12 | from api.utils.token import generate_verification_token, confirm_verification_token 13 | from api.utils.email import send_email 14 | import datetime 15 | 16 | user_routes = Blueprint("user_routes", __name__) 17 | 18 | @user_routes.route('/', methods=['POST']) 19 | def create_user(): 20 | try: 21 | data = request.get_json() 22 | if(User.find_by_email(data['email']) is not None or User.find_by_username(data['username']) is not None): 23 | return response_with(resp.INVALID_INPUT_422) 24 | data['password'] = User.generate_hash(data['password']) 25 | user_schmea = UserSchema() 26 | user, error = user_schmea.load(data) 27 | token = generate_verification_token(data['email']) 28 | verification_email = url_for('user_routes.verify_email', token=token, _external=True) 29 | html = render_template_string("

Welcome! Thanks for signing up. Please follow this link to activate your account:

{{ verification_email }}


Thanks!

", verification_email=verification_email) 30 | subject = "Please Verify your email" 31 | send_email(user.email, subject, html) 32 | result = user_schmea.dump(user.create()).data 33 | return response_with(resp.SUCCESS_201) 34 | except Exception as e: 35 | print e 36 | return response_with(resp.INVALID_INPUT_422) 37 | 38 | @user_routes.route('/confirm/', methods=['GET']) 39 | def verify_email(token): 40 | try: 41 | email = confirm_verification_token(token) 42 | except Exception as e: 43 | return response_with(resp.SERVER_ERROR_401) 44 | user = User.query.filter_by(email=email).first_or_404() 45 | if user.isVerified: 46 | return response_with(resp.INVALID_INPUT_422) 47 | else: 48 | user.isVerified = True 49 | db.session.add(user) 50 | db.session.commit() 51 | return response_with(resp.SUCCESS_200, value={'message': 'E-mail verified, you can proceed to login now.'}) 52 | 53 | @user_routes.route('/login', methods=['POST']) 54 | def authenticate_user(): 55 | try: 56 | data = request.get_json() 57 | if data.get('email') : 58 | current_user = User.find_by_email(data['email']) 59 | elif data.get('username') : 60 | current_user = User.find_by_username(data['username']) 61 | if not current_user: 62 | return response_with(resp.SERVER_ERROR_404) 63 | if current_user and not current_user.isVerified: 64 | return response_with(resp.BAD_REQUEST_400) 65 | if User.verify_hash(data['password'], current_user.password): 66 | access_token = create_access_token(identity = current_user.username) 67 | return response_with(resp.SUCCESS_200, value={'message': 'Logged in as admin', "access_token": access_token}) 68 | else: 69 | return response_with(resp.UNAUTHORIZED_401) 70 | except Exception as e: 71 | print e 72 | return response_with(resp.INVALID_INPUT_422) -------------------------------------------------------------------------------- /ch4-code/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch4-code/api/utils/__init__.py -------------------------------------------------------------------------------- /ch4-code/api/utils/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask_sqlalchemy import SQLAlchemy 5 | db = SQLAlchemy() -------------------------------------------------------------------------------- /ch4-code/api/utils/responses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import make_response, jsonify 5 | 6 | INVALID_FIELD_NAME_SENT_422 = { 7 | "http_code": 422, 8 | "code": "invalidField", 9 | "message": "Invalid fields found" 10 | } 11 | 12 | INVALID_INPUT_422 = { 13 | "http_code": 422, 14 | "code": "invalidInput", 15 | "message": "Invalid input" 16 | } 17 | 18 | MISSING_PARAMETERS_422 = { 19 | "http_code": 422, 20 | "code": "missingParameter", 21 | "message": "Missing parameters." 22 | } 23 | 24 | BAD_REQUEST_400 = { 25 | "http_code": 400, 26 | "code": "badRequest", 27 | "message": "Bad request" 28 | } 29 | 30 | SERVER_ERROR_500 = { 31 | "http_code": 500, 32 | "code": "serverError", 33 | "message": "Server error" 34 | } 35 | 36 | SERVER_ERROR_404 = { 37 | "http_code": 404, 38 | "code": "notFound", 39 | "message": "Resource not found" 40 | } 41 | 42 | FORBIDDEN_403 = { 43 | "http_code": 403, 44 | "code": "notAuthorized", 45 | "message": "You are not authorised to execute this." 46 | } 47 | UNAUTHORIZED_401 = { 48 | "http_code": 401, 49 | "code": "notAuthorized", 50 | "message": "Invalid authentication." 51 | } 52 | 53 | NOT_FOUND_HANDLER_404 = { 54 | "http_code": 404, 55 | "code": "notFound", 56 | "message": "route not found" 57 | } 58 | 59 | SUCCESS_200 = { 60 | 'http_code': 200, 61 | 'code': 'success' 62 | } 63 | 64 | SUCCESS_201 = { 65 | 'http_code': 201, 66 | 'code': 'success' 67 | } 68 | 69 | SUCCESS_204 = { 70 | 'http_code': 204, 71 | 'code': 'success' 72 | } 73 | 74 | 75 | def response_with(response, value=None, message=None, error=None, headers={}, pagination=None): 76 | result = {} 77 | if value is not None: 78 | result.update(value) 79 | 80 | if response.get('message', None) is not None: 81 | result.update({'message': response['message']}) 82 | 83 | result.update({'code': response['code']}) 84 | 85 | if error is not None: 86 | result.update({'errors': error}) 87 | 88 | if pagination is not None: 89 | result.update({'pagination': pagination}) 90 | 91 | headers.update({'Access-Control-Allow-Origin': '*'}) 92 | headers.update({'server': 'Flask REST API'}) 93 | 94 | return make_response(jsonify(result), response['http_code'], headers) 95 | -------------------------------------------------------------------------------- /ch4-code/api/utils/token.py: -------------------------------------------------------------------------------- 1 | from itsdangerous import URLSafeTimedSerializer 2 | from flask import current_app 3 | 4 | 5 | def generate_verification_token(email): 6 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 7 | return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) 8 | 9 | 10 | def confirm_verification_token(token, expiration=3600): 11 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 12 | try: 13 | email = serializer.loads( 14 | token, 15 | salt=current_app.config['SECURITY_PASSWORD_SALT'], 16 | max_age=expiration 17 | ) 18 | except Exception as e: 19 | return e 20 | return email -------------------------------------------------------------------------------- /ch4-code/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask 4 | from flask import jsonify 5 | from apispec.ext.marshmallow import MarshmallowPlugin 6 | from apispec_webframeworks.flask import FlaskPlugin 7 | from api.utils.database import db 8 | from api.utils.responses import response_with 9 | import api.utils.responses as resp 10 | from api.routes.authors import author_routes 11 | from api.routes.books import book_routes 12 | from api.routes.users import user_routes 13 | from flask_jwt_extended import JWTManager 14 | from api.config.config import DevelopmentConfig, ProductionConfig, TestingConfig 15 | from flask import send_from_directory 16 | from flask_jwt_extended import JWTManager 17 | from flask_swagger import swagger 18 | from flask_swagger_ui import get_swaggerui_blueprint 19 | from apispec import APISpec 20 | 21 | SWAGGER_URL = '/api/docs' 22 | app = Flask(__name__) 23 | 24 | 25 | if os.environ.get('WORK_ENV') == 'PROD': 26 | app_config = ProductionConfig 27 | elif os.environ.get('WORK_ENV') == 'TEST': 28 | app_config = TestingConfig 29 | else: 30 | app_config = DevelopmentConfig 31 | 32 | app.config.from_object(app_config) 33 | 34 | db.init_app(app) 35 | with app.app_context(): 36 | db.create_all() 37 | app.register_blueprint(author_routes, url_prefix='/api/authors') 38 | app.register_blueprint(book_routes, url_prefix='/api/books') 39 | app.register_blueprint(user_routes, url_prefix='/api/users') 40 | 41 | 42 | @app.route('/avatar/') 43 | def uploaded_file(filename): 44 | return send_from_directory(app.config['UPLOAD_FOLDER'],filename) 45 | 46 | @app.after_request 47 | def add_header(response): 48 | return response 49 | 50 | @app.errorhandler(400) 51 | def bad_request(e): 52 | logging.error(e) 53 | return response_with(resp.BAD_REQUEST_400) 54 | 55 | @app.errorhandler(500) 56 | def server_error(e): 57 | logging.error(e) 58 | return response_with(resp.SERVER_ERROR_500) 59 | 60 | @app.errorhandler(404) 61 | def not_found(e): 62 | logging.error(e) 63 | return response_with(resp.SERVER_ERROR_404) 64 | 65 | # END GLOBAL HTTP CONFIGURATIONS 66 | 67 | @app.route("/api/spec") 68 | def spec(): 69 | swag = swagger(app, prefix='/api') 70 | swag['info']['base'] = "http://localhost:5000" 71 | swag['info']['version'] = "1.0" 72 | swag['info']['title'] = "Flask Author DB" 73 | return jsonify(swag) 74 | 75 | swaggerui_blueprint = get_swaggerui_blueprint('/api/docs', '/api/spec', config={'app_name': "Flask Author DB"}) 76 | app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) 77 | jwt = JWTManager(app) 78 | db.init_app(app) 79 | mail.init_app(app) 80 | with app.app_context(): 81 | # from api.models import * 82 | db.create_all() 83 | 84 | if __name__ == "__main__": 85 | app.run(port=5000, host="0.0.0.0", use_reloader=False) -------------------------------------------------------------------------------- /ch4-code/run.py: -------------------------------------------------------------------------------- 1 | from main import app as application 2 | 3 | if __name__ == "__main__": 4 | application.run() -------------------------------------------------------------------------------- /ch4-code/text.log: -------------------------------------------------------------------------------- 1 | *** Starting uWSGI 2.0.18 (64bit) on [Wed Apr 24 17:26:41 2019] *** 2 | compiled with version: 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4) on 23 April 2019 17:13:02 3 | os: Darwin-18.2.0 Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 4 | nodename: Kunals-MacBook-Pro.local 5 | machine: x86_64 6 | clock source: unix 7 | pcre jit disabled 8 | detected number of CPU cores: 4 9 | current working directory: /Users/kunalrelan/Desktop/flask-api-starter/src 10 | detected binary path: /usr/local/bin/uwsgi 11 | *** WARNING: you are running uWSGI without its master process manager *** 12 | your processes number limit is 709 13 | your memory page size is 4096 bytes 14 | detected max file descriptor number: 10240 15 | lock engine: OSX spinlocks 16 | thunder lock: disabled (you can enable it with --thunder-lock) 17 | uwsgi socket 0 bound to TCP address 0.0.0.0:5000 fd 3 18 | Python version: 2.7.10 (default, Aug 17 2018, 19:45:58) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] 19 | *** Python threads support is disabled. You can enable it with --enable-threads *** 20 | Python main interpreter initialized at 0x7ff28df01200 21 | your server socket listen backlog is limited to 100 connections 22 | your mercy for graceful operations on workers is 60 seconds 23 | mapped 72888 bytes (71 KB) for 1 cores 24 | *** Operational MODE: single process *** 25 | unable to load app 0 (mountpoint='') (callable not found or import error) 26 | *** no app loaded. going in full dynamic mode *** 27 | *** uWSGI is running in multiple interpreter mode *** 28 | spawned uWSGI worker 1 (and the only) (pid: 50108, cores: 1) 29 | --- no python application found, check your startup logs for errors --- 30 | [pid: 50108|app: -1|req: -1/1] 127.0.0.1 () {38 vars in 1073 bytes} [Wed Apr 24 17:26:48 2019] GET /api/docs => generated 21 bytes in 4 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 31 | --- no python application found, check your startup logs for errors --- 32 | [pid: 50108|app: -1|req: -1/2] 127.0.0.1 () {40 vars in 1033 bytes} [Wed Apr 24 17:26:48 2019] GET /favicon.ico => generated 21 bytes in 509 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 33 | -------------------------------------------------------------------------------- /ch5-code/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/.DS_Store -------------------------------------------------------------------------------- /ch5-code/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python2.7" 3 | } -------------------------------------------------------------------------------- /ch5-code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/.DS_Store -------------------------------------------------------------------------------- /ch5-code/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/config/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/config/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/config/__init__.pyc -------------------------------------------------------------------------------- /ch5-code/api/config/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | TESTING = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | 7 | class ProductionConfig(Config): 8 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 9 | SQLALCHEMY_ECHO = False 10 | JWT_SECRET_KEY = 'JWT-SECRET' 11 | SECRET_KEY= 'SECRET-KEY' 12 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 13 | MAIL_DEFAULT_SENDER= '' 14 | MAIL_SERVER= '' 15 | MAIL_PORT= '' 16 | MAIL_USERNAME= ' 17 | MAIL_PASSWORD= '' 18 | MAIL_USE_TLS= False 19 | MAIL_USE_SSL= True 20 | UPLOAD_FOLDER= '' 21 | 22 | 23 | class DevelopmentConfig(Config): 24 | DEBUG = True 25 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 26 | SQLALCHEMY_ECHO = False 27 | JWT_SECRET_KEY = 'JWT-SECRET' 28 | SECRET_KEY= 'SECRET-KEY' 29 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 30 | MAIL_DEFAULT_SENDER= '' 31 | MAIL_SERVER= '' 32 | MAIL_PORT= '' 33 | MAIL_USERNAME= ' 34 | MAIL_PASSWORD= '' 35 | MAIL_USE_TLS= False 36 | MAIL_USE_SSL= True 37 | UPLOAD_FOLDER= '' 38 | 39 | 40 | class TestingConfig(Config): 41 | TESTING = True 42 | SQLALCHEMY_ECHO = False 43 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 44 | SQLALCHEMY_ECHO = False 45 | JWT_SECRET_KEY = 'JWT-SECRET' 46 | SECRET_KEY= 'SECRET-KEY' 47 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 48 | MAIL_DEFAULT_SENDER= '' 49 | MAIL_SERVER= '' 50 | MAIL_PORT= '' 51 | MAIL_USERNAME= ' 52 | MAIL_PASSWORD= '' 53 | MAIL_USE_TLS= False 54 | MAIL_USE_SSL= True 55 | UPLOAD_FOLDER= '' -------------------------------------------------------------------------------- /ch5-code/api/config/config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/config/config.pyc -------------------------------------------------------------------------------- /ch5-code/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/models/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/models/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | from api.models.books import BookSchema 8 | 9 | 10 | class Author(db.Model): 11 | __tablename__ = 'authors' 12 | 13 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 14 | first_name = db.Column(db.String(20)) 15 | last_name = db.Column(db.String(20)) 16 | avatar = db.Column(db.String(20), nullable=True) 17 | created = db.Column(db.DateTime, server_default=db.func.now()) 18 | books = db.relationship('Book', backref='Author', cascade="all, delete-orphan") 19 | 20 | def __init__(self, first_name, last_name, books=[]): 21 | self.first_name = first_name 22 | self.last_name = last_name 23 | self.books = books 24 | 25 | def create(self): 26 | db.session.add(self) 27 | db.session.commit() 28 | return self 29 | 30 | 31 | class AuthorSchema(ModelSchema): 32 | class Meta(ModelSchema.Meta): 33 | model = Author 34 | sqla_session = db.session 35 | 36 | id = fields.Number(dump_only=True) 37 | first_name = fields.String(required=True) 38 | last_name = fields.String(required=True) 39 | avatar = fields.String(dump_only=True) 40 | created = fields.String(dump_only=True) 41 | books = fields.Nested(BookSchema, many=True, only=['title','year','id']) 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ch5-code/api/models/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | 8 | 9 | class Book(db.Model): 10 | __tablename__ = 'books' 11 | 12 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 13 | title = db.Column(db.String(50)) 14 | year = db.Column(db.Integer) 15 | author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False) 16 | 17 | def __init__(self, title, year, author_id=None): 18 | self.title = title 19 | self.year = year 20 | self.author_id = author_id 21 | 22 | def create(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | return self 26 | 27 | 28 | class BookSchema(ModelSchema): 29 | class Meta(ModelSchema.Meta): 30 | model = Book 31 | sqla_session = db.session 32 | 33 | id = fields.Number(dump_only=True) 34 | title = fields.String(required=True) 35 | year = fields.Integer(required=True) 36 | author_id = fields.Integer() 37 | -------------------------------------------------------------------------------- /ch5-code/api/models/users.py: -------------------------------------------------------------------------------- 1 | class User(db.Model): 2 | __tablename__ = 'users' 3 | 4 | id = db.Column(db.Integer, primary_key = True) 5 | username = db.Column(db.String(120), unique = True, nullable = False) 6 | password = db.Column(db.String(120), nullable = False) 7 | isVerified = db.Column(db.Boolean, nullable=False, default=False) 8 | email = db.Column(db.String(120), unique = True, nullable = False) 9 | def create(self): 10 | db.session.add(self) 11 | db.session.commit() 12 | return self 13 | 14 | @classmethod 15 | def find_by_email(cls, email): 16 | return cls.query.filter_by(email = email).first() 17 | 18 | @classmethod 19 | def find_by_username(cls, username): 20 | print(cls) 21 | print(username) 22 | return cls.query.filter_by(username = username).first() 23 | 24 | @staticmethod 25 | def generate_hash(password): 26 | return sha256.hash(password) 27 | 28 | @staticmethod 29 | def verify_hash(password, hash): 30 | return sha256.verify(password, hash) 31 | 32 | class UserSchema(ModelSchema): 33 | class Meta(ModelSchema.Meta): 34 | model = User 35 | sqla_session = db.session 36 | 37 | id = fields.Number(dump_only=True) 38 | username = fields.String(required=True) 39 | email = fields.String(required=True) -------------------------------------------------------------------------------- /ch5-code/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/routes/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/routes/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint, request, url_for, current_app 5 | from api.utils.responses import response_with 6 | from api.utils import responses as resp 7 | from api.models.authors import Author, AuthorSchema 8 | from api.utils.database import db 9 | from flask_jwt_extended import jwt_required 10 | import os 11 | 12 | 13 | @author_routes.route('/', methods=['POST']) 14 | @jwt_required 15 | def create_author(): 16 | try: 17 | data = request.get_json() 18 | author_schema = AuthorSchema() 19 | author, error = author_schema.load(data) 20 | result = author_schema.dump(author.create()).data 21 | return response_with(resp.SUCCESS_201, value={"author": result}) 22 | except Exception as e: 23 | return response_with(resp.INVALID_INPUT_422) 24 | 25 | @author_routes.route('/avatar/', methods=['POST']) 26 | @jwt_required 27 | def upsert_author_avatar(author_id): 28 | try: 29 | file = request.files['avatar'] 30 | if file and allowed_file(file.filename): 31 | filename = secure_filename(file.filename) 32 | file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename)) 33 | get_author = Author.query.get_or_404(author_id) 34 | get_author.avatar = url_for('uploaded_file', filename=filename, _external=True) 35 | db.session.add(get_author) 36 | db.session.commit() 37 | author_schema = AuthorSchema() 38 | author, error = author_schema.dump(get_author) 39 | return response_with(resp.SUCCESS_200, value={"author": author}) 40 | except Exception as e: 41 | print e 42 | return response_with(resp.INVALID_INPUT_422) 43 | 44 | @author_routes.route('/', methods=['GET']) 45 | def get_author_list(): 46 | fetched = Author.query.all() 47 | author_schema = AuthorSchema(many=True, only=['first_name', 'last_name', 'id']) 48 | authors, error = author_schema.dump(fetched) 49 | return response_with(resp.SUCCESS_200, value={"authors": authors}) 50 | 51 | @author_routes.route('/', methods=['GET']) 52 | def get_author_detail(author_id): 53 | fetched = Author.query.get_or_404(author_id) 54 | author_schema = AuthorSchema() 55 | author, error = author_schema.dump(fetched) 56 | return response_with(resp.SUCCESS_200, value={"author": author}) 57 | 58 | @author_routes.route('/', methods=['PUT']) 59 | @jwt_required 60 | def update_author_detail(id): 61 | data = request.get_json() 62 | get_author = Author.query.get_or_404(id) 63 | get_author.first_name = data['first_name'] 64 | get_author.last_name = data['last_name'] 65 | db.session.add(get_author) 66 | db.session.commit() 67 | author_schema = AuthorSchema() 68 | author, error = author_schema.dump(get_author) 69 | return response_with(resp.SUCCESS_200, value={"author": author}) 70 | 71 | @author_routes.route('/', methods=['PATCH']) 72 | def modify_author_detail(id): 73 | data = request.get_json() 74 | get_author = Author.query.get(id) 75 | if data.get('first_name'): 76 | get_author.first_name = data['first_name'] 77 | if data.get('last_name'): 78 | get_author.last_name = data['last_name'] 79 | db.session.add(get_author) 80 | db.session.commit() 81 | author_schema = AuthorSchema() 82 | author, error = author_schema.dump(get_author) 83 | return response_with(resp.SUCCESS_200, value={"author": author}) 84 | 85 | @author_routes.route('/', methods=['DELETE']) 86 | def delete_author(id): 87 | get_author = Author.query.get_or_404(id) 88 | db.session.delete(get_author) 89 | db.session.commit() 90 | return response_with(resp.SUCCESS_204) -------------------------------------------------------------------------------- /ch5-code/api/routes/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from api.utils.responses import response_with 7 | from api.utils import responses as resp 8 | from api.models.books import Book, BookSchema 9 | from api.utils.database import db 10 | from flask_jwt_extended import (jwt_required, jwt_refresh_token_required, get_jwt_identity, get_raw_jwt) 11 | 12 | book_routes = Blueprint("book_routes", __name__) 13 | 14 | 15 | @book_routes.route('/', methods=['POST']) 16 | @jwt_required 17 | def create_book(): 18 | try: 19 | data = request.get_json() 20 | book_schema = BookSchema() 21 | book, error = book_schema.load(data) 22 | result = book_schema.dump(book.create()).data 23 | return response_with(resp.SUCCESS_201, value={"book": result}) 24 | except Exception as e: 25 | print e 26 | return response_with(resp.INVALID_INPUT_422) 27 | 28 | 29 | @book_routes.route('/', methods=['GET']) 30 | def get_book_list(): 31 | fetched = Book.query.all() 32 | book_schema = BookSchema(many=True, only=['author_id','title', 'year']) 33 | books, error = book_schema.dump(fetched) 34 | return response_with(resp.SUCCESS_200, value={"books": books}) 35 | 36 | 37 | @book_routes.route('/', methods=['GET']) 38 | def get_book_detail(id): 39 | fetched = Book.query.get_or_404(id) 40 | book_schema = BookSchema() 41 | books, error = book_schema.dump(fetched) 42 | return response_with(resp.SUCCESS_200, value={"books": books}) 43 | 44 | @book_routes.route('/', methods=['PUT']) 45 | @jwt_required 46 | def update_book_detail(id): 47 | data = request.get_json() 48 | get_book = Book.query.get_or_404(id) 49 | get_book.title = data['title'] 50 | get_book.year = data['year'] 51 | db.session.add(get_book) 52 | db.session.commit() 53 | book_schema = BookSchema() 54 | book, error = book_schema.dump(get_book) 55 | return response_with(resp.SUCCESS_200, value={"book": book}) 56 | 57 | @book_routes.route('/', methods=['PATCH']) 58 | @jwt_required 59 | def modify_book_detail(id): 60 | data = request.get_json() 61 | get_book = Book.query.get_or_404(id) 62 | if data.get('title'): 63 | get_book.title = data['title'] 64 | if data.get('year'): 65 | get_book.year = data['year'] 66 | db.session.add(get_book) 67 | db.session.commit() 68 | book_schema = BookSchema() 69 | book, error = book_schema.dump(get_book) 70 | return response_with(resp.SUCCESS_200, value={"book": book}) 71 | 72 | @book_routes.route('/', methods=['DELETE']) 73 | @jwt_required 74 | def delete_book(id): 75 | get_book = Book.query.get_or_404(id) 76 | db.session.delete(get_book) 77 | db.session.commit() 78 | return response_with(resp.SUCCESS_204) 79 | -------------------------------------------------------------------------------- /ch5-code/api/routes/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from flask import url_for, render_template_string 7 | from api.utils.responses import response_with 8 | from api.utils import responses as resp 9 | from api.models.users import User, UserSchema 10 | from api.utils.database import db 11 | from flask_jwt_extended import create_access_token 12 | from api.utils.token import generate_verification_token, confirm_verification_token 13 | from api.utils.email import send_email 14 | import datetime 15 | 16 | user_routes = Blueprint("user_routes", __name__) 17 | 18 | @user_routes.route('/', methods=['POST']) 19 | def create_user(): 20 | try: 21 | data = request.get_json() 22 | if(User.find_by_email(data['email']) is not None or User.find_by_username(data['username']) is not None): 23 | return response_with(resp.INVALID_INPUT_422) 24 | data['password'] = User.generate_hash(data['password']) 25 | user_schmea = UserSchema() 26 | user, error = user_schmea.load(data) 27 | token = generate_verification_token(data['email']) 28 | verification_email = url_for('user_routes.verify_email', token=token, _external=True) 29 | html = render_template_string("

Welcome! Thanks for signing up. Please follow this link to activate your account:

{{ verification_email }}


Thanks!

", verification_email=verification_email) 30 | subject = "Please Verify your email" 31 | send_email(user.email, subject, html) 32 | result = user_schmea.dump(user.create()).data 33 | return response_with(resp.SUCCESS_201) 34 | except Exception as e: 35 | print e 36 | return response_with(resp.INVALID_INPUT_422) 37 | 38 | @user_routes.route('/confirm/', methods=['GET']) 39 | def verify_email(token): 40 | try: 41 | email = confirm_verification_token(token) 42 | except Exception as e: 43 | return response_with(resp.SERVER_ERROR_401) 44 | user = User.query.filter_by(email=email).first_or_404() 45 | if user.isVerified: 46 | return response_with(resp.INVALID_INPUT_422) 47 | else: 48 | user.isVerified = True 49 | db.session.add(user) 50 | db.session.commit() 51 | return response_with(resp.SUCCESS_200, value={'message': 'E-mail verified, you can proceed to login now.'}) 52 | 53 | @user_routes.route('/login', methods=['POST']) 54 | def authenticate_user(): 55 | try: 56 | data = request.get_json() 57 | if data.get('email') : 58 | current_user = User.find_by_email(data['email']) 59 | elif data.get('username') : 60 | current_user = User.find_by_username(data['username']) 61 | if not current_user: 62 | return response_with(resp.SERVER_ERROR_404) 63 | if current_user and not current_user.isVerified: 64 | return response_with(resp.BAD_REQUEST_400) 65 | if User.verify_hash(data['password'], current_user.password): 66 | access_token = create_access_token(identity = current_user.username) 67 | return response_with(resp.SUCCESS_200, value={'message': 'Logged in as admin', "access_token": access_token}) 68 | else: 69 | return response_with(resp.UNAUTHORIZED_401) 70 | except Exception as e: 71 | print e 72 | return response_with(resp.INVALID_INPUT_422) -------------------------------------------------------------------------------- /ch5-code/api/tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/tests/.DS_Store -------------------------------------------------------------------------------- /ch5-code/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/tests/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/tests/test_authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from api.utils.test_base import BaseTestCase 6 | from api.models.authors import Author 7 | from api.models.books import Book 8 | from datetime import datetime 9 | from flask_jwt_extended import create_access_token 10 | import unittest2 as unittest 11 | import io 12 | 13 | def create_authors(): 14 | author1 = Author(first_name="John", last_name="Doe").create() 15 | Book(title="Test Book 1", year=datetime(1976, 1, 1), author_id=author1.id).create() 16 | Book(title="Test Book 2", year=datetime(1992, 12, 1), author_id=author1.id).create() 17 | 18 | author2 = Author(first_name="Jane", last_name="Doe").create() 19 | Book(title="Test Book 3", year=datetime(1986, 1, 3), author_id=author2.id).create() 20 | Book(title="Test Book 4", year=datetime(1992, 12, 1), author_id=author2.id).create() 21 | 22 | 23 | def login(): 24 | access_token = create_access_token(identity = 'kunal.relan@hotmail.com') 25 | return access_token 26 | 27 | 28 | class TestAuthors(BaseTestCase): 29 | def setUp(self): 30 | super(TestAuthors, self).setUp() 31 | create_authors() 32 | 33 | def test_create_author(self): 34 | token = login() 35 | author = { 36 | 'first_name': 'Johny', 37 | 'last_name': 'Doee' 38 | } 39 | 40 | response = self.app.post( 41 | '/api/authors/', 42 | data=json.dumps(author), 43 | content_type='application/json', 44 | headers= { 'Authorization': 'Bearer '+token } 45 | ) 46 | data = json.loads(response.data) 47 | self.assertEqual(201, response.status_code) 48 | self.assertTrue('author' in data) 49 | 50 | def test_create_author_no_authorization(self): 51 | author = { 52 | 'first_name': 'Johny', 53 | 'last_name': 'Doee' 54 | } 55 | 56 | response = self.app.post( 57 | '/api/authors/', 58 | data=json.dumps(author), 59 | content_type='application/json', 60 | ) 61 | data = json.loads(response.data) 62 | self.assertEqual(401, response.status_code) 63 | 64 | def test_create_author_no_name(self): 65 | token = login() 66 | author = { 67 | 'first_name': 'Johny' 68 | } 69 | 70 | response = self.app.post( 71 | '/api/authors/', 72 | data=json.dumps(author), 73 | content_type='application/json', 74 | headers= { 'Authorization': 'Bearer '+token } 75 | ) 76 | data = json.loads(response.data) 77 | self.assertEqual(422, response.status_code) 78 | 79 | def test_upload_avatar(self): 80 | token = login() 81 | response = self.app.post( 82 | '/api/authors/avatar/2', 83 | data=dict(avatar=(io.BytesIO(b'test'), 'test_file.jpg')), 84 | content_type='multipart/form-data', 85 | headers= { 'Authorization': 'Bearer '+ token } 86 | ) 87 | self.assertEqual(200, response.status_code) 88 | 89 | def test_upload_avatar_without_file(self): 90 | token = login() 91 | response = self.app.post( 92 | '/api/authors/avatar/2', 93 | data=dict(file=(io.BytesIO(b'test'), 'test_file.csv')), 94 | content_type='multipart/form-data', 95 | headers= { 'Authorization': 'Bearer '+ token } 96 | ) 97 | self.assertEqual(422, response.status_code) 98 | 99 | def test_get_authors(self): 100 | response = self.app.get( 101 | '/api/authors/', 102 | content_type='application/json' 103 | ) 104 | data = json.loads(response.data) 105 | self.assertEqual(200, response.status_code) 106 | self.assertTrue('authors' in data) 107 | 108 | def test_get_author_detail(self): 109 | response = self.app.get( 110 | '/api/authors/2', 111 | content_type='application/json' 112 | ) 113 | data = json.loads(response.data) 114 | self.assertEqual(200, response.status_code) 115 | self.assertTrue('author' in data) 116 | self.assertTrue('books' in data['author']) 117 | 118 | def test_update_author(self): 119 | token = login() 120 | author = { 121 | 'first_name': 'Joseph' 122 | } 123 | response = self.app.put( 124 | '/api/authors/2', 125 | data=json.dumps(author), 126 | content_type='application/json', 127 | headers= { 'Authorization': 'Bearer '+token } 128 | ) 129 | self.assertEqual(200, response.status_code) 130 | def test_delete_author(self): 131 | token = login() 132 | response = self.app.delete( 133 | '/api/authors/2', 134 | headers= { 'Authorization': 'Bearer '+token } 135 | ) 136 | self.assertEqual(204, response.status_code) 137 | 138 | def test_create_book(self): 139 | token = login() 140 | author = { 141 | 'title': 'Alice in wonderland', 142 | 'year': 1982, 143 | 'author_id': 2 144 | } 145 | 146 | response = self.app.post( 147 | '/api/books/', 148 | data=json.dumps(author), 149 | content_type='application/json', 150 | headers= { 'Authorization': 'Bearer '+token } 151 | ) 152 | data = json.loads(response.data) 153 | self.assertEqual(201, response.status_code) 154 | self.assertTrue('book' in data) 155 | 156 | def test_create_book_no_author(self): 157 | token = login() 158 | author = { 159 | 'title': 'Alice in wonderland', 160 | 'year': 1982 161 | } 162 | 163 | response = self.app.post( 164 | '/api/books/', 165 | data=json.dumps(author), 166 | content_type='application/json', 167 | headers= { 'Authorization': 'Bearer '+token } 168 | ) 169 | data = json.loads(response.data) 170 | self.assertEqual(422, response.status_code) 171 | 172 | def test_create_book_no_authorization(self): 173 | author = { 174 | 'title': 'Alice in wonderland', 175 | 'year': 1982, 176 | 'author_id': 2 177 | } 178 | 179 | response = self.app.post( 180 | '/api/books/', 181 | data=json.dumps(author), 182 | content_type='application/json' 183 | ) 184 | data = json.loads(response.data) 185 | self.assertEqual(401, response.status_code) 186 | 187 | def test_get_books(self): 188 | response = self.app.get( 189 | '/api/books/', 190 | content_type='application/json' 191 | ) 192 | data = json.loads(response.data) 193 | self.assertEqual(200, response.status_code) 194 | self.assertTrue('books' in data) 195 | 196 | def test_get_book_details(self): 197 | response = self.app.get( 198 | '/api/books/2', 199 | content_type='application/json' 200 | ) 201 | data = json.loads(response.data) 202 | self.assertEqual(200, response.status_code) 203 | self.assertTrue('books' in data) 204 | 205 | def test_update_book(self): 206 | token = login() 207 | author = { 208 | 'year': 1992, 209 | 'title': 'Alice' 210 | } 211 | response = self.app.put( 212 | '/api/books/2', 213 | data=json.dumps(author), 214 | content_type='application/json', 215 | headers= { 'Authorization': 'Bearer '+token } 216 | ) 217 | self.assertEqual(200, response.status_code) 218 | 219 | def test_delete_book(self): 220 | token = login() 221 | response = self.app.delete( 222 | '/api/books/2', 223 | headers= { 'Authorization': 'Bearer '+token } 224 | ) 225 | self.assertEqual(204, response.status_code) 226 | 227 | 228 | if __name__ == '__main__': 229 | unittest.main() -------------------------------------------------------------------------------- /ch5-code/api/tests/test_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from datetime import datetime 6 | import unittest2 as unittest 7 | from api.utils.token import generate_verification_token, confirm_verification_token 8 | from api.utils.test_base import BaseTestCase 9 | from api.models.users import User 10 | 11 | def create_users(): 12 | user1 = User(email="kunal.relan12@gmail.com", username='kunalrelan12', password=User.generate_hash('helloworld'), isVerified=True).create() 13 | user2 = User(email="kunal.relan123@gmail.com", username='kunalrelan125', password=User.generate_hash('helloworld')).create() 14 | 15 | 16 | class TestUsers(BaseTestCase): 17 | def setUp(self): 18 | super(TestUsers, self).setUp() 19 | create_users() 20 | 21 | def test_login_user(self): 22 | newuser = { 23 | "email" : "kunal.relan12@gmail.com", 24 | "password" : "helloworld" 25 | } 26 | response = self.app.post( 27 | '/api/users/login', 28 | data=json.dumps(newuser), 29 | content_type='application/json' 30 | ) 31 | data = json.loads(response.data) 32 | self.assertEqual(200, response.status_code) 33 | self.assertTrue('access_token' in data) 34 | 35 | def test_login_user_wrong_credentials(self): 36 | user = { 37 | "email" : "kunal.relan12@gmail.com", 38 | "password" : "helloworld12" 39 | } 40 | response = self.app.post( 41 | '/api/users/login', 42 | data=json.dumps(user), 43 | content_type='application/json' 44 | ) 45 | data = json.loads(response.data) 46 | self.assertEqual(401, response.status_code) 47 | 48 | def test_login_unverified_user(self): 49 | user = { 50 | "email" : "kunal.relan123@gmail.com", 51 | "password" : "helloworld" 52 | } 53 | response = self.app.post( 54 | '/api/users/login', 55 | data=json.dumps(user), 56 | content_type='application/json' 57 | ) 58 | data = json.loads(response.data) 59 | self.assertEqual(400, response.status_code) 60 | 61 | 62 | def test_create_user(self): 63 | user = { 64 | "username" : "kunalrelan2", 65 | "password" : "helloworld", 66 | "email" : "kunal.relan12@hotmail.com" 67 | } 68 | 69 | response = self.app.post( 70 | '/api/users/', 71 | data=json.dumps(user), 72 | content_type='application/json' 73 | ) 74 | data = json.loads(response.data) 75 | self.assertEqual(201, response.status_code) 76 | self.assertTrue('success' in data['code']) 77 | 78 | def test_create_user_without_username(self): 79 | user = { 80 | "password" : "helloworld", 81 | "email" : "kunal.relan12@hotmail.com" 82 | } 83 | 84 | response = self.app.post( 85 | '/api/users/', 86 | data=json.dumps(user), 87 | content_type='application/json' 88 | ) 89 | data = json.loads(response.data) 90 | self.assertEqual(422, response.status_code) 91 | 92 | def test_confirm_email(self): 93 | token = generate_verification_token('kunal.relan123@gmail.com') 94 | 95 | response = self.app.get( 96 | '/api/users/confirm/'+token 97 | ) 98 | data = json.loads(response.data) 99 | print(data) 100 | self.assertEqual(200, response.status_code) 101 | self.assertTrue('success' in data['code']) 102 | 103 | def test_confirm_email_for_verified_user(self): 104 | token = generate_verification_token('kunal.relan12@gmail.com') 105 | 106 | response = self.app.get( 107 | '/api/users/confirm/'+token 108 | ) 109 | data = json.loads(response.data) 110 | print(data) 111 | self.assertEqual(422, response.status_code) 112 | 113 | 114 | def test_confirm_email_with_incorrect_email(self): 115 | token = generate_verification_token('kunal.relan43@gmail.com') 116 | 117 | response = self.app.get( 118 | '/api/users/confirm/'+token 119 | ) 120 | data = json.loads(response.data) 121 | self.assertEqual(404, response.status_code) 122 | 123 | 124 | if __name__ == '__main__': 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /ch5-code/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch5-code/api/utils/__init__.py -------------------------------------------------------------------------------- /ch5-code/api/utils/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask_sqlalchemy import SQLAlchemy 5 | db = SQLAlchemy() -------------------------------------------------------------------------------- /ch5-code/api/utils/responses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import make_response, jsonify 5 | 6 | INVALID_FIELD_NAME_SENT_422 = { 7 | "http_code": 422, 8 | "code": "invalidField", 9 | "message": "Invalid fields found" 10 | } 11 | 12 | INVALID_INPUT_422 = { 13 | "http_code": 422, 14 | "code": "invalidInput", 15 | "message": "Invalid input" 16 | } 17 | 18 | MISSING_PARAMETERS_422 = { 19 | "http_code": 422, 20 | "code": "missingParameter", 21 | "message": "Missing parameters." 22 | } 23 | 24 | BAD_REQUEST_400 = { 25 | "http_code": 400, 26 | "code": "badRequest", 27 | "message": "Bad request" 28 | } 29 | 30 | SERVER_ERROR_500 = { 31 | "http_code": 500, 32 | "code": "serverError", 33 | "message": "Server error" 34 | } 35 | 36 | SERVER_ERROR_404 = { 37 | "http_code": 404, 38 | "code": "notFound", 39 | "message": "Resource not found" 40 | } 41 | 42 | FORBIDDEN_403 = { 43 | "http_code": 403, 44 | "code": "notAuthorized", 45 | "message": "You are not authorised to execute this." 46 | } 47 | UNAUTHORIZED_401 = { 48 | "http_code": 401, 49 | "code": "notAuthorized", 50 | "message": "Invalid authentication." 51 | } 52 | 53 | NOT_FOUND_HANDLER_404 = { 54 | "http_code": 404, 55 | "code": "notFound", 56 | "message": "route not found" 57 | } 58 | 59 | SUCCESS_200 = { 60 | 'http_code': 200, 61 | 'code': 'success' 62 | } 63 | 64 | SUCCESS_201 = { 65 | 'http_code': 201, 66 | 'code': 'success' 67 | } 68 | 69 | SUCCESS_204 = { 70 | 'http_code': 204, 71 | 'code': 'success' 72 | } 73 | 74 | 75 | def response_with(response, value=None, message=None, error=None, headers={}, pagination=None): 76 | result = {} 77 | if value is not None: 78 | result.update(value) 79 | 80 | if response.get('message', None) is not None: 81 | result.update({'message': response['message']}) 82 | 83 | result.update({'code': response['code']}) 84 | 85 | if error is not None: 86 | result.update({'errors': error}) 87 | 88 | if pagination is not None: 89 | result.update({'pagination': pagination}) 90 | 91 | headers.update({'Access-Control-Allow-Origin': '*'}) 92 | headers.update({'server': 'Flask REST API'}) 93 | 94 | return make_response(jsonify(result), response['http_code'], headers) 95 | -------------------------------------------------------------------------------- /ch5-code/api/utils/test_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest2 as unittest 5 | from main import create_app 6 | from api.utils.database import db 7 | from api.config.config import TestingConfig 8 | import tempfile 9 | 10 | class BaseTestCase(unittest.TestCase): 11 | def setUp(self): 12 | app = create_app(TestingConfig) 13 | self.test_db_file = tempfile.mkstemp()[1] 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + self.test_db_file 15 | with app.app_context(): 16 | db.create_all() 17 | app.app_context().push() 18 | self.app = app.test_client() 19 | 20 | def tearDown(self): 21 | db.session.close_all() 22 | db.drop_all() 23 | 24 | -------------------------------------------------------------------------------- /ch5-code/api/utils/token.py: -------------------------------------------------------------------------------- 1 | from itsdangerous import URLSafeTimedSerializer 2 | from flask import current_app 3 | 4 | 5 | def generate_verification_token(email): 6 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 7 | return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) 8 | 9 | 10 | def confirm_verification_token(token, expiration=3600): 11 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 12 | try: 13 | email = serializer.loads( 14 | token, 15 | salt=current_app.config['SECURITY_PASSWORD_SALT'], 16 | max_age=expiration 17 | ) 18 | except Exception as e: 19 | return e 20 | return email -------------------------------------------------------------------------------- /ch5-code/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask 4 | from flask import jsonify 5 | from apispec.ext.marshmallow import MarshmallowPlugin 6 | from apispec_webframeworks.flask import FlaskPlugin 7 | from api.utils.database import db 8 | from api.utils.responses import response_with 9 | import api.utils.responses as resp 10 | from api.routes.authors import author_routes 11 | from api.routes.books import book_routes 12 | from api.routes.users import user_routes 13 | from flask_jwt_extended import JWTManager 14 | from api.config.config import DevelopmentConfig, ProductionConfig, TestingConfig 15 | from flask import send_from_directory 16 | from flask_jwt_extended import JWTManager 17 | from flask_swagger import swagger 18 | from flask_swagger_ui import get_swaggerui_blueprint 19 | from apispec import APISpec 20 | 21 | SWAGGER_URL = '/api/docs' 22 | app = Flask(__name__) 23 | 24 | 25 | if os.environ.get('WORK_ENV') == 'PROD': 26 | app_config = ProductionConfig 27 | elif os.environ.get('WORK_ENV') == 'TEST': 28 | app_config = TestingConfig 29 | else: 30 | app_config = DevelopmentConfig 31 | 32 | app.config.from_object(app_config) 33 | 34 | db.init_app(app) 35 | with app.app_context(): 36 | db.create_all() 37 | app.register_blueprint(author_routes, url_prefix='/api/authors') 38 | app.register_blueprint(book_routes, url_prefix='/api/books') 39 | app.register_blueprint(user_routes, url_prefix='/api/users') 40 | 41 | 42 | @app.route('/avatar/') 43 | def uploaded_file(filename): 44 | return send_from_directory(app.config['UPLOAD_FOLDER'],filename) 45 | 46 | @app.after_request 47 | def add_header(response): 48 | return response 49 | 50 | @app.errorhandler(400) 51 | def bad_request(e): 52 | logging.error(e) 53 | return response_with(resp.BAD_REQUEST_400) 54 | 55 | @app.errorhandler(500) 56 | def server_error(e): 57 | logging.error(e) 58 | return response_with(resp.SERVER_ERROR_500) 59 | 60 | @app.errorhandler(404) 61 | def not_found(e): 62 | logging.error(e) 63 | return response_with(resp.SERVER_ERROR_404) 64 | 65 | # END GLOBAL HTTP CONFIGURATIONS 66 | 67 | @app.route("/api/spec") 68 | def spec(): 69 | swag = swagger(app, prefix='/api') 70 | swag['info']['base'] = "http://localhost:5000" 71 | swag['info']['version'] = "1.0" 72 | swag['info']['title'] = "Flask Author DB" 73 | return jsonify(swag) 74 | 75 | swaggerui_blueprint = get_swaggerui_blueprint('/api/docs', '/api/spec', config={'app_name': "Flask Author DB"}) 76 | app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) 77 | jwt = JWTManager(app) 78 | db.init_app(app) 79 | mail.init_app(app) 80 | with app.app_context(): 81 | # from api.models import * 82 | db.create_all() 83 | 84 | if __name__ == "__main__": 85 | app.run(port=5000, host="0.0.0.0", use_reloader=False) -------------------------------------------------------------------------------- /ch5-code/run.py: -------------------------------------------------------------------------------- 1 | from main import app as application 2 | 3 | if __name__ == "__main__": 4 | application.run() -------------------------------------------------------------------------------- /ch5-code/text.log: -------------------------------------------------------------------------------- 1 | *** Starting uWSGI 2.0.18 (64bit) on [Wed Apr 24 17:26:41 2019] *** 2 | compiled with version: 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4) on 23 April 2019 17:13:02 3 | os: Darwin-18.2.0 Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 4 | nodename: Kunals-MacBook-Pro.local 5 | machine: x86_64 6 | clock source: unix 7 | pcre jit disabled 8 | detected number of CPU cores: 4 9 | current working directory: /Users/kunalrelan/Desktop/flask-api-starter/src 10 | detected binary path: /usr/local/bin/uwsgi 11 | *** WARNING: you are running uWSGI without its master process manager *** 12 | your processes number limit is 709 13 | your memory page size is 4096 bytes 14 | detected max file descriptor number: 10240 15 | lock engine: OSX spinlocks 16 | thunder lock: disabled (you can enable it with --thunder-lock) 17 | uwsgi socket 0 bound to TCP address 0.0.0.0:5000 fd 3 18 | Python version: 2.7.10 (default, Aug 17 2018, 19:45:58) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] 19 | *** Python threads support is disabled. You can enable it with --enable-threads *** 20 | Python main interpreter initialized at 0x7ff28df01200 21 | your server socket listen backlog is limited to 100 connections 22 | your mercy for graceful operations on workers is 60 seconds 23 | mapped 72888 bytes (71 KB) for 1 cores 24 | *** Operational MODE: single process *** 25 | unable to load app 0 (mountpoint='') (callable not found or import error) 26 | *** no app loaded. going in full dynamic mode *** 27 | *** uWSGI is running in multiple interpreter mode *** 28 | spawned uWSGI worker 1 (and the only) (pid: 50108, cores: 1) 29 | --- no python application found, check your startup logs for errors --- 30 | [pid: 50108|app: -1|req: -1/1] 127.0.0.1 () {38 vars in 1073 bytes} [Wed Apr 24 17:26:48 2019] GET /api/docs => generated 21 bytes in 4 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 31 | --- no python application found, check your startup logs for errors --- 32 | [pid: 50108|app: -1|req: -1/2] 127.0.0.1 () {40 vars in 1033 bytes} [Wed Apr 24 17:26:48 2019] GET /favicon.ico => generated 21 bytes in 509 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 33 | -------------------------------------------------------------------------------- /ch6-code/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/.DS_Store -------------------------------------------------------------------------------- /ch6-code/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python2.7" 3 | } -------------------------------------------------------------------------------- /ch6-code/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn run:application -------------------------------------------------------------------------------- /ch6-code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/.DS_Store -------------------------------------------------------------------------------- /ch6-code/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/config/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/config/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/config/__init__.pyc -------------------------------------------------------------------------------- /ch6-code/api/config/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | TESTING = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | 7 | class ProductionConfig(Config): 8 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 9 | SQLALCHEMY_ECHO = False 10 | JWT_SECRET_KEY = 'JWT-SECRET' 11 | SECRET_KEY= 'SECRET-KEY' 12 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 13 | MAIL_DEFAULT_SENDER= '' 14 | MAIL_SERVER= '' 15 | MAIL_PORT= '' 16 | MAIL_USERNAME= ' 17 | MAIL_PASSWORD= '' 18 | MAIL_USE_TLS= False 19 | MAIL_USE_SSL= True 20 | UPLOAD_FOLDER= '' 21 | 22 | 23 | class DevelopmentConfig(Config): 24 | DEBUG = True 25 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 26 | SQLALCHEMY_ECHO = False 27 | JWT_SECRET_KEY = 'JWT-SECRET' 28 | SECRET_KEY= 'SECRET-KEY' 29 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 30 | MAIL_DEFAULT_SENDER= '' 31 | MAIL_SERVER= '' 32 | MAIL_PORT= '' 33 | MAIL_USERNAME= ' 34 | MAIL_PASSWORD= '' 35 | MAIL_USE_TLS= False 36 | MAIL_USE_SSL= True 37 | UPLOAD_FOLDER= '' 38 | 39 | 40 | class TestingConfig(Config): 41 | TESTING = True 42 | SQLALCHEMY_ECHO = False 43 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 44 | SQLALCHEMY_ECHO = False 45 | JWT_SECRET_KEY = 'JWT-SECRET' 46 | SECRET_KEY= 'SECRET-KEY' 47 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 48 | MAIL_DEFAULT_SENDER= '' 49 | MAIL_SERVER= '' 50 | MAIL_PORT= '' 51 | MAIL_USERNAME= ' 52 | MAIL_PASSWORD= '' 53 | MAIL_USE_TLS= False 54 | MAIL_USE_SSL= True 55 | UPLOAD_FOLDER= '' -------------------------------------------------------------------------------- /ch6-code/api/config/config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/config/config.pyc -------------------------------------------------------------------------------- /ch6-code/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/models/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/models/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | from api.models.books import BookSchema 8 | 9 | 10 | class Author(db.Model): 11 | __tablename__ = 'authors' 12 | 13 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 14 | first_name = db.Column(db.String(20)) 15 | last_name = db.Column(db.String(20)) 16 | avatar = db.Column(db.String(20), nullable=True) 17 | created = db.Column(db.DateTime, server_default=db.func.now()) 18 | books = db.relationship('Book', backref='Author', cascade="all, delete-orphan") 19 | 20 | def __init__(self, first_name, last_name, books=[]): 21 | self.first_name = first_name 22 | self.last_name = last_name 23 | self.books = books 24 | 25 | def create(self): 26 | db.session.add(self) 27 | db.session.commit() 28 | return self 29 | 30 | 31 | class AuthorSchema(ModelSchema): 32 | class Meta(ModelSchema.Meta): 33 | model = Author 34 | sqla_session = db.session 35 | 36 | id = fields.Number(dump_only=True) 37 | first_name = fields.String(required=True) 38 | last_name = fields.String(required=True) 39 | avatar = fields.String(dump_only=True) 40 | created = fields.String(dump_only=True) 41 | books = fields.Nested(BookSchema, many=True, only=['title','year','id']) 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ch6-code/api/models/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | 8 | 9 | class Book(db.Model): 10 | __tablename__ = 'books' 11 | 12 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 13 | title = db.Column(db.String(50)) 14 | year = db.Column(db.Integer) 15 | author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False) 16 | 17 | def __init__(self, title, year, author_id=None): 18 | self.title = title 19 | self.year = year 20 | self.author_id = author_id 21 | 22 | def create(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | return self 26 | 27 | 28 | class BookSchema(ModelSchema): 29 | class Meta(ModelSchema.Meta): 30 | model = Book 31 | sqla_session = db.session 32 | 33 | id = fields.Number(dump_only=True) 34 | title = fields.String(required=True) 35 | year = fields.Integer(required=True) 36 | author_id = fields.Integer() 37 | -------------------------------------------------------------------------------- /ch6-code/api/models/users.py: -------------------------------------------------------------------------------- 1 | class User(db.Model): 2 | __tablename__ = 'users' 3 | 4 | id = db.Column(db.Integer, primary_key = True) 5 | username = db.Column(db.String(120), unique = True, nullable = False) 6 | password = db.Column(db.String(120), nullable = False) 7 | isVerified = db.Column(db.Boolean, nullable=False, default=False) 8 | email = db.Column(db.String(120), unique = True, nullable = False) 9 | def create(self): 10 | db.session.add(self) 11 | db.session.commit() 12 | return self 13 | 14 | @classmethod 15 | def find_by_email(cls, email): 16 | return cls.query.filter_by(email = email).first() 17 | 18 | @classmethod 19 | def find_by_username(cls, username): 20 | print(cls) 21 | print(username) 22 | return cls.query.filter_by(username = username).first() 23 | 24 | @staticmethod 25 | def generate_hash(password): 26 | return sha256.hash(password) 27 | 28 | @staticmethod 29 | def verify_hash(password, hash): 30 | return sha256.verify(password, hash) 31 | 32 | class UserSchema(ModelSchema): 33 | class Meta(ModelSchema.Meta): 34 | model = User 35 | sqla_session = db.session 36 | 37 | id = fields.Number(dump_only=True) 38 | username = fields.String(required=True) 39 | email = fields.String(required=True) -------------------------------------------------------------------------------- /ch6-code/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/routes/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/routes/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint, request, url_for, current_app 5 | from api.utils.responses import response_with 6 | from api.utils import responses as resp 7 | from api.models.authors import Author, AuthorSchema 8 | from api.utils.database import db 9 | from flask_jwt_extended import jwt_required 10 | import os 11 | 12 | 13 | @author_routes.route('/', methods=['POST']) 14 | @jwt_required 15 | def create_author(): 16 | try: 17 | data = request.get_json() 18 | author_schema = AuthorSchema() 19 | author, error = author_schema.load(data) 20 | result = author_schema.dump(author.create()).data 21 | return response_with(resp.SUCCESS_201, value={"author": result}) 22 | except Exception as e: 23 | return response_with(resp.INVALID_INPUT_422) 24 | 25 | @author_routes.route('/avatar/', methods=['POST']) 26 | @jwt_required 27 | def upsert_author_avatar(author_id): 28 | try: 29 | file = request.files['avatar'] 30 | if file and allowed_file(file.filename): 31 | filename = secure_filename(file.filename) 32 | file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename)) 33 | get_author = Author.query.get_or_404(author_id) 34 | get_author.avatar = url_for('uploaded_file', filename=filename, _external=True) 35 | db.session.add(get_author) 36 | db.session.commit() 37 | author_schema = AuthorSchema() 38 | author, error = author_schema.dump(get_author) 39 | return response_with(resp.SUCCESS_200, value={"author": author}) 40 | except Exception as e: 41 | print e 42 | return response_with(resp.INVALID_INPUT_422) 43 | 44 | @author_routes.route('/', methods=['GET']) 45 | def get_author_list(): 46 | fetched = Author.query.all() 47 | author_schema = AuthorSchema(many=True, only=['first_name', 'last_name', 'id']) 48 | authors, error = author_schema.dump(fetched) 49 | return response_with(resp.SUCCESS_200, value={"authors": authors}) 50 | 51 | @author_routes.route('/', methods=['GET']) 52 | def get_author_detail(author_id): 53 | fetched = Author.query.get_or_404(author_id) 54 | author_schema = AuthorSchema() 55 | author, error = author_schema.dump(fetched) 56 | return response_with(resp.SUCCESS_200, value={"author": author}) 57 | 58 | @author_routes.route('/', methods=['PUT']) 59 | @jwt_required 60 | def update_author_detail(id): 61 | data = request.get_json() 62 | get_author = Author.query.get_or_404(id) 63 | get_author.first_name = data['first_name'] 64 | get_author.last_name = data['last_name'] 65 | db.session.add(get_author) 66 | db.session.commit() 67 | author_schema = AuthorSchema() 68 | author, error = author_schema.dump(get_author) 69 | return response_with(resp.SUCCESS_200, value={"author": author}) 70 | 71 | @author_routes.route('/', methods=['PATCH']) 72 | def modify_author_detail(id): 73 | data = request.get_json() 74 | get_author = Author.query.get(id) 75 | if data.get('first_name'): 76 | get_author.first_name = data['first_name'] 77 | if data.get('last_name'): 78 | get_author.last_name = data['last_name'] 79 | db.session.add(get_author) 80 | db.session.commit() 81 | author_schema = AuthorSchema() 82 | author, error = author_schema.dump(get_author) 83 | return response_with(resp.SUCCESS_200, value={"author": author}) 84 | 85 | @author_routes.route('/', methods=['DELETE']) 86 | def delete_author(id): 87 | get_author = Author.query.get_or_404(id) 88 | db.session.delete(get_author) 89 | db.session.commit() 90 | return response_with(resp.SUCCESS_204) -------------------------------------------------------------------------------- /ch6-code/api/routes/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from api.utils.responses import response_with 7 | from api.utils import responses as resp 8 | from api.models.books import Book, BookSchema 9 | from api.utils.database import db 10 | from flask_jwt_extended import (jwt_required, jwt_refresh_token_required, get_jwt_identity, get_raw_jwt) 11 | 12 | book_routes = Blueprint("book_routes", __name__) 13 | 14 | 15 | @book_routes.route('/', methods=['POST']) 16 | @jwt_required 17 | def create_book(): 18 | try: 19 | data = request.get_json() 20 | book_schema = BookSchema() 21 | book, error = book_schema.load(data) 22 | result = book_schema.dump(book.create()).data 23 | return response_with(resp.SUCCESS_201, value={"book": result}) 24 | except Exception as e: 25 | print e 26 | return response_with(resp.INVALID_INPUT_422) 27 | 28 | 29 | @book_routes.route('/', methods=['GET']) 30 | def get_book_list(): 31 | fetched = Book.query.all() 32 | book_schema = BookSchema(many=True, only=['author_id','title', 'year']) 33 | books, error = book_schema.dump(fetched) 34 | return response_with(resp.SUCCESS_200, value={"books": books}) 35 | 36 | 37 | @book_routes.route('/', methods=['GET']) 38 | def get_book_detail(id): 39 | fetched = Book.query.get_or_404(id) 40 | book_schema = BookSchema() 41 | books, error = book_schema.dump(fetched) 42 | return response_with(resp.SUCCESS_200, value={"books": books}) 43 | 44 | @book_routes.route('/', methods=['PUT']) 45 | @jwt_required 46 | def update_book_detail(id): 47 | data = request.get_json() 48 | get_book = Book.query.get_or_404(id) 49 | get_book.title = data['title'] 50 | get_book.year = data['year'] 51 | db.session.add(get_book) 52 | db.session.commit() 53 | book_schema = BookSchema() 54 | book, error = book_schema.dump(get_book) 55 | return response_with(resp.SUCCESS_200, value={"book": book}) 56 | 57 | @book_routes.route('/', methods=['PATCH']) 58 | @jwt_required 59 | def modify_book_detail(id): 60 | data = request.get_json() 61 | get_book = Book.query.get_or_404(id) 62 | if data.get('title'): 63 | get_book.title = data['title'] 64 | if data.get('year'): 65 | get_book.year = data['year'] 66 | db.session.add(get_book) 67 | db.session.commit() 68 | book_schema = BookSchema() 69 | book, error = book_schema.dump(get_book) 70 | return response_with(resp.SUCCESS_200, value={"book": book}) 71 | 72 | @book_routes.route('/', methods=['DELETE']) 73 | @jwt_required 74 | def delete_book(id): 75 | get_book = Book.query.get_or_404(id) 76 | db.session.delete(get_book) 77 | db.session.commit() 78 | return response_with(resp.SUCCESS_204) 79 | -------------------------------------------------------------------------------- /ch6-code/api/routes/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from flask import url_for, render_template_string 7 | from api.utils.responses import response_with 8 | from api.utils import responses as resp 9 | from api.models.users import User, UserSchema 10 | from api.utils.database import db 11 | from flask_jwt_extended import create_access_token 12 | from api.utils.token import generate_verification_token, confirm_verification_token 13 | from api.utils.email import send_email 14 | import datetime 15 | 16 | user_routes = Blueprint("user_routes", __name__) 17 | 18 | @user_routes.route('/', methods=['POST']) 19 | def create_user(): 20 | try: 21 | data = request.get_json() 22 | if(User.find_by_email(data['email']) is not None or User.find_by_username(data['username']) is not None): 23 | return response_with(resp.INVALID_INPUT_422) 24 | data['password'] = User.generate_hash(data['password']) 25 | user_schmea = UserSchema() 26 | user, error = user_schmea.load(data) 27 | token = generate_verification_token(data['email']) 28 | verification_email = url_for('user_routes.verify_email', token=token, _external=True) 29 | html = render_template_string("

Welcome! Thanks for signing up. Please follow this link to activate your account:

{{ verification_email }}


Thanks!

", verification_email=verification_email) 30 | subject = "Please Verify your email" 31 | send_email(user.email, subject, html) 32 | result = user_schmea.dump(user.create()).data 33 | return response_with(resp.SUCCESS_201) 34 | except Exception as e: 35 | print e 36 | return response_with(resp.INVALID_INPUT_422) 37 | 38 | @user_routes.route('/confirm/', methods=['GET']) 39 | def verify_email(token): 40 | try: 41 | email = confirm_verification_token(token) 42 | except Exception as e: 43 | return response_with(resp.SERVER_ERROR_401) 44 | user = User.query.filter_by(email=email).first_or_404() 45 | if user.isVerified: 46 | return response_with(resp.INVALID_INPUT_422) 47 | else: 48 | user.isVerified = True 49 | db.session.add(user) 50 | db.session.commit() 51 | return response_with(resp.SUCCESS_200, value={'message': 'E-mail verified, you can proceed to login now.'}) 52 | 53 | @user_routes.route('/login', methods=['POST']) 54 | def authenticate_user(): 55 | try: 56 | data = request.get_json() 57 | if data.get('email') : 58 | current_user = User.find_by_email(data['email']) 59 | elif data.get('username') : 60 | current_user = User.find_by_username(data['username']) 61 | if not current_user: 62 | return response_with(resp.SERVER_ERROR_404) 63 | if current_user and not current_user.isVerified: 64 | return response_with(resp.BAD_REQUEST_400) 65 | if User.verify_hash(data['password'], current_user.password): 66 | access_token = create_access_token(identity = current_user.username) 67 | return response_with(resp.SUCCESS_200, value={'message': 'Logged in as admin', "access_token": access_token}) 68 | else: 69 | return response_with(resp.UNAUTHORIZED_401) 70 | except Exception as e: 71 | print e 72 | return response_with(resp.INVALID_INPUT_422) -------------------------------------------------------------------------------- /ch6-code/api/tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/tests/.DS_Store -------------------------------------------------------------------------------- /ch6-code/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/tests/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/tests/test_authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from api.utils.test_base import BaseTestCase 6 | from api.models.authors import Author 7 | from api.models.books import Book 8 | from datetime import datetime 9 | from flask_jwt_extended import create_access_token 10 | import unittest2 as unittest 11 | import io 12 | 13 | def create_authors(): 14 | author1 = Author(first_name="John", last_name="Doe").create() 15 | Book(title="Test Book 1", year=datetime(1976, 1, 1), author_id=author1.id).create() 16 | Book(title="Test Book 2", year=datetime(1992, 12, 1), author_id=author1.id).create() 17 | 18 | author2 = Author(first_name="Jane", last_name="Doe").create() 19 | Book(title="Test Book 3", year=datetime(1986, 1, 3), author_id=author2.id).create() 20 | Book(title="Test Book 4", year=datetime(1992, 12, 1), author_id=author2.id).create() 21 | 22 | 23 | def login(): 24 | access_token = create_access_token(identity = 'kunal.relan@hotmail.com') 25 | return access_token 26 | 27 | 28 | class TestAuthors(BaseTestCase): 29 | def setUp(self): 30 | super(TestAuthors, self).setUp() 31 | create_authors() 32 | 33 | def test_create_author(self): 34 | token = login() 35 | author = { 36 | 'first_name': 'Johny', 37 | 'last_name': 'Doee' 38 | } 39 | 40 | response = self.app.post( 41 | '/api/authors/', 42 | data=json.dumps(author), 43 | content_type='application/json', 44 | headers= { 'Authorization': 'Bearer '+token } 45 | ) 46 | data = json.loads(response.data) 47 | self.assertEqual(201, response.status_code) 48 | self.assertTrue('author' in data) 49 | 50 | def test_create_author_no_authorization(self): 51 | author = { 52 | 'first_name': 'Johny', 53 | 'last_name': 'Doee' 54 | } 55 | 56 | response = self.app.post( 57 | '/api/authors/', 58 | data=json.dumps(author), 59 | content_type='application/json', 60 | ) 61 | data = json.loads(response.data) 62 | self.assertEqual(401, response.status_code) 63 | 64 | def test_create_author_no_name(self): 65 | token = login() 66 | author = { 67 | 'first_name': 'Johny' 68 | } 69 | 70 | response = self.app.post( 71 | '/api/authors/', 72 | data=json.dumps(author), 73 | content_type='application/json', 74 | headers= { 'Authorization': 'Bearer '+token } 75 | ) 76 | data = json.loads(response.data) 77 | self.assertEqual(422, response.status_code) 78 | 79 | def test_upload_avatar(self): 80 | token = login() 81 | response = self.app.post( 82 | '/api/authors/avatar/2', 83 | data=dict(avatar=(io.BytesIO(b'test'), 'test_file.jpg')), 84 | content_type='multipart/form-data', 85 | headers= { 'Authorization': 'Bearer '+ token } 86 | ) 87 | self.assertEqual(200, response.status_code) 88 | 89 | def test_upload_avatar_without_file(self): 90 | token = login() 91 | response = self.app.post( 92 | '/api/authors/avatar/2', 93 | data=dict(file=(io.BytesIO(b'test'), 'test_file.csv')), 94 | content_type='multipart/form-data', 95 | headers= { 'Authorization': 'Bearer '+ token } 96 | ) 97 | self.assertEqual(422, response.status_code) 98 | 99 | def test_get_authors(self): 100 | response = self.app.get( 101 | '/api/authors/', 102 | content_type='application/json' 103 | ) 104 | data = json.loads(response.data) 105 | self.assertEqual(200, response.status_code) 106 | self.assertTrue('authors' in data) 107 | 108 | def test_get_author_detail(self): 109 | response = self.app.get( 110 | '/api/authors/2', 111 | content_type='application/json' 112 | ) 113 | data = json.loads(response.data) 114 | self.assertEqual(200, response.status_code) 115 | self.assertTrue('author' in data) 116 | self.assertTrue('books' in data['author']) 117 | 118 | def test_update_author(self): 119 | token = login() 120 | author = { 121 | 'first_name': 'Joseph' 122 | } 123 | response = self.app.put( 124 | '/api/authors/2', 125 | data=json.dumps(author), 126 | content_type='application/json', 127 | headers= { 'Authorization': 'Bearer '+token } 128 | ) 129 | self.assertEqual(200, response.status_code) 130 | def test_delete_author(self): 131 | token = login() 132 | response = self.app.delete( 133 | '/api/authors/2', 134 | headers= { 'Authorization': 'Bearer '+token } 135 | ) 136 | self.assertEqual(204, response.status_code) 137 | 138 | def test_create_book(self): 139 | token = login() 140 | author = { 141 | 'title': 'Alice in wonderland', 142 | 'year': 1982, 143 | 'author_id': 2 144 | } 145 | 146 | response = self.app.post( 147 | '/api/books/', 148 | data=json.dumps(author), 149 | content_type='application/json', 150 | headers= { 'Authorization': 'Bearer '+token } 151 | ) 152 | data = json.loads(response.data) 153 | self.assertEqual(201, response.status_code) 154 | self.assertTrue('book' in data) 155 | 156 | def test_create_book_no_author(self): 157 | token = login() 158 | author = { 159 | 'title': 'Alice in wonderland', 160 | 'year': 1982 161 | } 162 | 163 | response = self.app.post( 164 | '/api/books/', 165 | data=json.dumps(author), 166 | content_type='application/json', 167 | headers= { 'Authorization': 'Bearer '+token } 168 | ) 169 | data = json.loads(response.data) 170 | self.assertEqual(422, response.status_code) 171 | 172 | def test_create_book_no_authorization(self): 173 | author = { 174 | 'title': 'Alice in wonderland', 175 | 'year': 1982, 176 | 'author_id': 2 177 | } 178 | 179 | response = self.app.post( 180 | '/api/books/', 181 | data=json.dumps(author), 182 | content_type='application/json' 183 | ) 184 | data = json.loads(response.data) 185 | self.assertEqual(401, response.status_code) 186 | 187 | def test_get_books(self): 188 | response = self.app.get( 189 | '/api/books/', 190 | content_type='application/json' 191 | ) 192 | data = json.loads(response.data) 193 | self.assertEqual(200, response.status_code) 194 | self.assertTrue('books' in data) 195 | 196 | def test_get_book_details(self): 197 | response = self.app.get( 198 | '/api/books/2', 199 | content_type='application/json' 200 | ) 201 | data = json.loads(response.data) 202 | self.assertEqual(200, response.status_code) 203 | self.assertTrue('books' in data) 204 | 205 | def test_update_book(self): 206 | token = login() 207 | author = { 208 | 'year': 1992, 209 | 'title': 'Alice' 210 | } 211 | response = self.app.put( 212 | '/api/books/2', 213 | data=json.dumps(author), 214 | content_type='application/json', 215 | headers= { 'Authorization': 'Bearer '+token } 216 | ) 217 | self.assertEqual(200, response.status_code) 218 | 219 | def test_delete_book(self): 220 | token = login() 221 | response = self.app.delete( 222 | '/api/books/2', 223 | headers= { 'Authorization': 'Bearer '+token } 224 | ) 225 | self.assertEqual(204, response.status_code) 226 | 227 | 228 | if __name__ == '__main__': 229 | unittest.main() -------------------------------------------------------------------------------- /ch6-code/api/tests/test_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from datetime import datetime 6 | import unittest2 as unittest 7 | from api.utils.token import generate_verification_token, confirm_verification_token 8 | from api.utils.test_base import BaseTestCase 9 | from api.models.users import User 10 | 11 | def create_users(): 12 | user1 = User(email="kunal.relan12@gmail.com", username='kunalrelan12', password=User.generate_hash('helloworld'), isVerified=True).create() 13 | user2 = User(email="kunal.relan123@gmail.com", username='kunalrelan125', password=User.generate_hash('helloworld')).create() 14 | 15 | 16 | class TestUsers(BaseTestCase): 17 | def setUp(self): 18 | super(TestUsers, self).setUp() 19 | create_users() 20 | 21 | def test_login_user(self): 22 | newuser = { 23 | "email" : "kunal.relan12@gmail.com", 24 | "password" : "helloworld" 25 | } 26 | response = self.app.post( 27 | '/api/users/login', 28 | data=json.dumps(newuser), 29 | content_type='application/json' 30 | ) 31 | data = json.loads(response.data) 32 | self.assertEqual(200, response.status_code) 33 | self.assertTrue('access_token' in data) 34 | 35 | def test_login_user_wrong_credentials(self): 36 | user = { 37 | "email" : "kunal.relan12@gmail.com", 38 | "password" : "helloworld12" 39 | } 40 | response = self.app.post( 41 | '/api/users/login', 42 | data=json.dumps(user), 43 | content_type='application/json' 44 | ) 45 | data = json.loads(response.data) 46 | self.assertEqual(401, response.status_code) 47 | 48 | def test_login_unverified_user(self): 49 | user = { 50 | "email" : "kunal.relan123@gmail.com", 51 | "password" : "helloworld" 52 | } 53 | response = self.app.post( 54 | '/api/users/login', 55 | data=json.dumps(user), 56 | content_type='application/json' 57 | ) 58 | data = json.loads(response.data) 59 | self.assertEqual(400, response.status_code) 60 | 61 | 62 | def test_create_user(self): 63 | user = { 64 | "username" : "kunalrelan2", 65 | "password" : "helloworld", 66 | "email" : "kunal.relan12@hotmail.com" 67 | } 68 | 69 | response = self.app.post( 70 | '/api/users/', 71 | data=json.dumps(user), 72 | content_type='application/json' 73 | ) 74 | data = json.loads(response.data) 75 | self.assertEqual(201, response.status_code) 76 | self.assertTrue('success' in data['code']) 77 | 78 | def test_create_user_without_username(self): 79 | user = { 80 | "password" : "helloworld", 81 | "email" : "kunal.relan12@hotmail.com" 82 | } 83 | 84 | response = self.app.post( 85 | '/api/users/', 86 | data=json.dumps(user), 87 | content_type='application/json' 88 | ) 89 | data = json.loads(response.data) 90 | self.assertEqual(422, response.status_code) 91 | 92 | def test_confirm_email(self): 93 | token = generate_verification_token('kunal.relan123@gmail.com') 94 | 95 | response = self.app.get( 96 | '/api/users/confirm/'+token 97 | ) 98 | data = json.loads(response.data) 99 | print(data) 100 | self.assertEqual(200, response.status_code) 101 | self.assertTrue('success' in data['code']) 102 | 103 | def test_confirm_email_for_verified_user(self): 104 | token = generate_verification_token('kunal.relan12@gmail.com') 105 | 106 | response = self.app.get( 107 | '/api/users/confirm/'+token 108 | ) 109 | data = json.loads(response.data) 110 | print(data) 111 | self.assertEqual(422, response.status_code) 112 | 113 | 114 | def test_confirm_email_with_incorrect_email(self): 115 | token = generate_verification_token('kunal.relan43@gmail.com') 116 | 117 | response = self.app.get( 118 | '/api/users/confirm/'+token 119 | ) 120 | data = json.loads(response.data) 121 | self.assertEqual(404, response.status_code) 122 | 123 | 124 | if __name__ == '__main__': 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /ch6-code/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch6-code/api/utils/__init__.py -------------------------------------------------------------------------------- /ch6-code/api/utils/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask_sqlalchemy import SQLAlchemy 5 | db = SQLAlchemy() -------------------------------------------------------------------------------- /ch6-code/api/utils/responses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import make_response, jsonify 5 | 6 | INVALID_FIELD_NAME_SENT_422 = { 7 | "http_code": 422, 8 | "code": "invalidField", 9 | "message": "Invalid fields found" 10 | } 11 | 12 | INVALID_INPUT_422 = { 13 | "http_code": 422, 14 | "code": "invalidInput", 15 | "message": "Invalid input" 16 | } 17 | 18 | MISSING_PARAMETERS_422 = { 19 | "http_code": 422, 20 | "code": "missingParameter", 21 | "message": "Missing parameters." 22 | } 23 | 24 | BAD_REQUEST_400 = { 25 | "http_code": 400, 26 | "code": "badRequest", 27 | "message": "Bad request" 28 | } 29 | 30 | SERVER_ERROR_500 = { 31 | "http_code": 500, 32 | "code": "serverError", 33 | "message": "Server error" 34 | } 35 | 36 | SERVER_ERROR_404 = { 37 | "http_code": 404, 38 | "code": "notFound", 39 | "message": "Resource not found" 40 | } 41 | 42 | FORBIDDEN_403 = { 43 | "http_code": 403, 44 | "code": "notAuthorized", 45 | "message": "You are not authorised to execute this." 46 | } 47 | UNAUTHORIZED_401 = { 48 | "http_code": 401, 49 | "code": "notAuthorized", 50 | "message": "Invalid authentication." 51 | } 52 | 53 | NOT_FOUND_HANDLER_404 = { 54 | "http_code": 404, 55 | "code": "notFound", 56 | "message": "route not found" 57 | } 58 | 59 | SUCCESS_200 = { 60 | 'http_code': 200, 61 | 'code': 'success' 62 | } 63 | 64 | SUCCESS_201 = { 65 | 'http_code': 201, 66 | 'code': 'success' 67 | } 68 | 69 | SUCCESS_204 = { 70 | 'http_code': 204, 71 | 'code': 'success' 72 | } 73 | 74 | 75 | def response_with(response, value=None, message=None, error=None, headers={}, pagination=None): 76 | result = {} 77 | if value is not None: 78 | result.update(value) 79 | 80 | if response.get('message', None) is not None: 81 | result.update({'message': response['message']}) 82 | 83 | result.update({'code': response['code']}) 84 | 85 | if error is not None: 86 | result.update({'errors': error}) 87 | 88 | if pagination is not None: 89 | result.update({'pagination': pagination}) 90 | 91 | headers.update({'Access-Control-Allow-Origin': '*'}) 92 | headers.update({'server': 'Flask REST API'}) 93 | 94 | return make_response(jsonify(result), response['http_code'], headers) 95 | -------------------------------------------------------------------------------- /ch6-code/api/utils/test_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest2 as unittest 5 | from main import create_app 6 | from api.utils.database import db 7 | from api.config.config import TestingConfig 8 | import tempfile 9 | 10 | class BaseTestCase(unittest.TestCase): 11 | def setUp(self): 12 | app = create_app(TestingConfig) 13 | self.test_db_file = tempfile.mkstemp()[1] 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + self.test_db_file 15 | with app.app_context(): 16 | db.create_all() 17 | app.app_context().push() 18 | self.app = app.test_client() 19 | 20 | def tearDown(self): 21 | db.session.close_all() 22 | db.drop_all() 23 | 24 | -------------------------------------------------------------------------------- /ch6-code/api/utils/token.py: -------------------------------------------------------------------------------- 1 | from itsdangerous import URLSafeTimedSerializer 2 | from flask import current_app 3 | 4 | 5 | def generate_verification_token(email): 6 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 7 | return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) 8 | 9 | 10 | def confirm_verification_token(token, expiration=3600): 11 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 12 | try: 13 | email = serializer.loads( 14 | token, 15 | salt=current_app.config['SECURITY_PASSWORD_SALT'], 16 | max_age=expiration 17 | ) 18 | except Exception as e: 19 | return e 20 | return email -------------------------------------------------------------------------------- /ch6-code/code-apache.conf: -------------------------------------------------------------------------------- 1 | 2 | # The ServerName directive sets the request scheme, hostname and port that 3 | # the server uses to identify itself. This is used when creating 4 | # redirection URLs. In the context of virtual hosts, the ServerName 5 | # specifies what hostname must appear in the request's Host: header to 6 | # match this virtual host. For the default virtual host (this file) this 7 | # value is not decisive as it is used as a last resort host regardless. 8 | # However, you must set it for any further virtual host explicitly. 9 | #ServerName www.example.com 10 | 11 | ServerAdmin webmaster@localhost 12 | DocumentRoot /var/www/html 13 | 14 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, 15 | # error, crit, alert, emerg. 16 | # It is also possible to configure the loglevel for particular 17 | # modules, e.g. 18 | #LogLevel info ssl:warn 19 | 20 | ErrorLog ${APACHE_LOG_DIR}/error.log 21 | CustomLog ${APACHE_LOG_DIR}/access.log combined 22 | 23 | Order deny,allow 24 | Allow from all 25 | 26 | ProxyPreserveHost On 27 | 28 | ProxyPass "http://127.0.0.1:5000/" 29 | ProxyPassReverse "http://127.0.0.1:5000/" 30 | 31 | # For most configuration files from conf-available/, which are 32 | # enabled or disabled at a global level, it is possible to 33 | # include a line for only one particular virtual host. For example the 34 | # following line enables the CGI configuration for this host only 35 | # after it has been globally disabled with "a2disconf". 36 | #Include conf-available/serve-cgi-bin.conf 37 | 38 | -------------------------------------------------------------------------------- /ch6-code/code-app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | handlers: 5 | - url: /avatar 6 | static_dir: images 7 | - url: /.* 8 | script: wsgi.application 9 | 10 | libraries: 11 | - name: ssl 12 | version: latest 13 | env_variables: 14 | WORK_ENV: PROD -------------------------------------------------------------------------------- /ch6-code/code-flask-app-gunicorn.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description= Flask App service 3 | After=network.target 4 | 5 | [Service] 6 | User=flask 7 | Group=www-data 8 | Restart=on-failure 9 | Environment="WORK_ENV=PROD" 10 | WorkingDirectory=/home/flask/flask-api-app/src 11 | ExecStart=/home/flask/flask-api-app/src/venv/bin/gunicorn -c /home/flask/flask-api-app/src/gunicorn.conf -b 0.0.0.0:5000 wsgi:application 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ch6-code/code-flask-app.service: -------------------------------------------------------------------------------- 1 | #Metadata and dependencies section 2 | [Unit] 3 | Description=Flask App service 4 | After=network.target 5 | #Define users and app working directory 6 | [Service] 7 | User=flask 8 | Group=www-data 9 | WorkingDirectory=/home/flask/flask-api-app/src 10 | Environment="WORK_ENV=PROD" 11 | ExecStart=/home/flask/flask-api-app/src/venv/bin/uwsgi --ini flask-app.ini 12 | #Link the service to start on multi-user system up 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ch6-code/code-nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | server_name flaskapp; 5 | 6 | location / { 7 | include uwsgi_params; 8 | uwsgi_pass unix:/home/flask/flask-api-app/src/flask-app.sock; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ch6-code/flask-app.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = run:application 3 | 4 | master = true 5 | processes = 5 6 | 7 | socket = flask-app.sock 8 | chmod-socket = 660 9 | vacuum = true 10 | 11 | die-on-term = true 12 | -------------------------------------------------------------------------------- /ch6-code/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask 4 | from flask import jsonify 5 | from apispec.ext.marshmallow import MarshmallowPlugin 6 | from apispec_webframeworks.flask import FlaskPlugin 7 | from api.utils.database import db 8 | from api.utils.responses import response_with 9 | import api.utils.responses as resp 10 | from api.routes.authors import author_routes 11 | from api.routes.books import book_routes 12 | from api.routes.users import user_routes 13 | from flask_jwt_extended import JWTManager 14 | from api.config.config import DevelopmentConfig, ProductionConfig, TestingConfig 15 | from flask import send_from_directory 16 | from flask_jwt_extended import JWTManager 17 | from flask_swagger import swagger 18 | from flask_swagger_ui import get_swaggerui_blueprint 19 | from apispec import APISpec 20 | 21 | SWAGGER_URL = '/api/docs' 22 | app = Flask(__name__) 23 | 24 | 25 | if os.environ.get('WORK_ENV') == 'PROD': 26 | app_config = ProductionConfig 27 | elif os.environ.get('WORK_ENV') == 'TEST': 28 | app_config = TestingConfig 29 | else: 30 | app_config = DevelopmentConfig 31 | 32 | app.config.from_object(app_config) 33 | 34 | db.init_app(app) 35 | with app.app_context(): 36 | db.create_all() 37 | app.register_blueprint(author_routes, url_prefix='/api/authors') 38 | app.register_blueprint(book_routes, url_prefix='/api/books') 39 | app.register_blueprint(user_routes, url_prefix='/api/users') 40 | 41 | 42 | @app.route('/avatar/') 43 | def uploaded_file(filename): 44 | return send_from_directory(app.config['UPLOAD_FOLDER'],filename) 45 | 46 | @app.after_request 47 | def add_header(response): 48 | return response 49 | 50 | @app.errorhandler(400) 51 | def bad_request(e): 52 | logging.error(e) 53 | return response_with(resp.BAD_REQUEST_400) 54 | 55 | @app.errorhandler(500) 56 | def server_error(e): 57 | logging.error(e) 58 | return response_with(resp.SERVER_ERROR_500) 59 | 60 | @app.errorhandler(404) 61 | def not_found(e): 62 | logging.error(e) 63 | return response_with(resp.SERVER_ERROR_404) 64 | 65 | # END GLOBAL HTTP CONFIGURATIONS 66 | 67 | @app.route("/api/spec") 68 | def spec(): 69 | swag = swagger(app, prefix='/api') 70 | swag['info']['base'] = "http://localhost:5000" 71 | swag['info']['version'] = "1.0" 72 | swag['info']['title'] = "Flask Author DB" 73 | return jsonify(swag) 74 | 75 | swaggerui_blueprint = get_swaggerui_blueprint('/api/docs', '/api/spec', config={'app_name': "Flask Author DB"}) 76 | app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) 77 | jwt = JWTManager(app) 78 | db.init_app(app) 79 | mail.init_app(app) 80 | with app.app_context(): 81 | # from api.models import * 82 | db.create_all() 83 | 84 | if __name__ == "__main__": 85 | app.run(port=5000, host="0.0.0.0", use_reloader=False) -------------------------------------------------------------------------------- /ch6-code/run.py: -------------------------------------------------------------------------------- 1 | from main import app as application 2 | 3 | if __name__ == "__main__": 4 | application.run() -------------------------------------------------------------------------------- /ch6-code/text.log: -------------------------------------------------------------------------------- 1 | *** Starting uWSGI 2.0.18 (64bit) on [Wed Apr 24 17:26:41 2019] *** 2 | compiled with version: 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4) on 23 April 2019 17:13:02 3 | os: Darwin-18.2.0 Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 4 | nodename: Kunals-MacBook-Pro.local 5 | machine: x86_64 6 | clock source: unix 7 | pcre jit disabled 8 | detected number of CPU cores: 4 9 | current working directory: /Users/kunalrelan/Desktop/flask-api-starter/src 10 | detected binary path: /usr/local/bin/uwsgi 11 | *** WARNING: you are running uWSGI without its master process manager *** 12 | your processes number limit is 709 13 | your memory page size is 4096 bytes 14 | detected max file descriptor number: 10240 15 | lock engine: OSX spinlocks 16 | thunder lock: disabled (you can enable it with --thunder-lock) 17 | uwsgi socket 0 bound to TCP address 0.0.0.0:5000 fd 3 18 | Python version: 2.7.10 (default, Aug 17 2018, 19:45:58) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] 19 | *** Python threads support is disabled. You can enable it with --enable-threads *** 20 | Python main interpreter initialized at 0x7ff28df01200 21 | your server socket listen backlog is limited to 100 connections 22 | your mercy for graceful operations on workers is 60 seconds 23 | mapped 72888 bytes (71 KB) for 1 cores 24 | *** Operational MODE: single process *** 25 | unable to load app 0 (mountpoint='') (callable not found or import error) 26 | *** no app loaded. going in full dynamic mode *** 27 | *** uWSGI is running in multiple interpreter mode *** 28 | spawned uWSGI worker 1 (and the only) (pid: 50108, cores: 1) 29 | --- no python application found, check your startup logs for errors --- 30 | [pid: 50108|app: -1|req: -1/1] 127.0.0.1 () {38 vars in 1073 bytes} [Wed Apr 24 17:26:48 2019] GET /api/docs => generated 21 bytes in 4 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 31 | --- no python application found, check your startup logs for errors --- 32 | [pid: 50108|app: -1|req: -1/2] 127.0.0.1 () {40 vars in 1033 bytes} [Wed Apr 24 17:26:48 2019] GET /favicon.ico => generated 21 bytes in 509 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 33 | -------------------------------------------------------------------------------- /ch7-code/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/.DS_Store -------------------------------------------------------------------------------- /ch7-code/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python2.7" 3 | } -------------------------------------------------------------------------------- /ch7-code/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn run:application -------------------------------------------------------------------------------- /ch7-code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/.DS_Store -------------------------------------------------------------------------------- /ch7-code/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/config/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/config/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/config/__init__.pyc -------------------------------------------------------------------------------- /ch7-code/api/config/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | TESTING = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | 7 | class ProductionConfig(Config): 8 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 9 | SQLALCHEMY_ECHO = False 10 | JWT_SECRET_KEY = 'JWT-SECRET' 11 | SECRET_KEY= 'SECRET-KEY' 12 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 13 | MAIL_DEFAULT_SENDER= '' 14 | MAIL_SERVER= '' 15 | MAIL_PORT= '' 16 | MAIL_USERNAME= ' 17 | MAIL_PASSWORD= '' 18 | MAIL_USE_TLS= False 19 | MAIL_USE_SSL= True 20 | UPLOAD_FOLDER= '' 21 | 22 | 23 | class DevelopmentConfig(Config): 24 | DEBUG = True 25 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 26 | SQLALCHEMY_ECHO = False 27 | JWT_SECRET_KEY = 'JWT-SECRET' 28 | SECRET_KEY= 'SECRET-KEY' 29 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 30 | MAIL_DEFAULT_SENDER= '' 31 | MAIL_SERVER= '' 32 | MAIL_PORT= '' 33 | MAIL_USERNAME= ' 34 | MAIL_PASSWORD= '' 35 | MAIL_USE_TLS= False 36 | MAIL_USE_SSL= True 37 | UPLOAD_FOLDER= '' 38 | 39 | 40 | class TestingConfig(Config): 41 | TESTING = True 42 | SQLALCHEMY_ECHO = False 43 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://:/" 44 | SQLALCHEMY_ECHO = False 45 | JWT_SECRET_KEY = 'JWT-SECRET' 46 | SECRET_KEY= 'SECRET-KEY' 47 | SECURITY_PASSWORD_SALT= 'SECRET-KEY-PASSWORD' 48 | MAIL_DEFAULT_SENDER= '' 49 | MAIL_SERVER= '' 50 | MAIL_PORT= '' 51 | MAIL_USERNAME= ' 52 | MAIL_PASSWORD= '' 53 | MAIL_USE_TLS= False 54 | MAIL_USE_SSL= True 55 | UPLOAD_FOLDER= '' -------------------------------------------------------------------------------- /ch7-code/api/config/config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/config/config.pyc -------------------------------------------------------------------------------- /ch7-code/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/models/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/models/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | from api.models.books import BookSchema 8 | 9 | 10 | class Author(db.Model): 11 | __tablename__ = 'authors' 12 | 13 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 14 | first_name = db.Column(db.String(20)) 15 | last_name = db.Column(db.String(20)) 16 | avatar = db.Column(db.String(20), nullable=True) 17 | created = db.Column(db.DateTime, server_default=db.func.now()) 18 | books = db.relationship('Book', backref='Author', cascade="all, delete-orphan") 19 | 20 | def __init__(self, first_name, last_name, books=[]): 21 | self.first_name = first_name 22 | self.last_name = last_name 23 | self.books = books 24 | 25 | def create(self): 26 | db.session.add(self) 27 | db.session.commit() 28 | return self 29 | 30 | 31 | class AuthorSchema(ModelSchema): 32 | class Meta(ModelSchema.Meta): 33 | model = Author 34 | sqla_session = db.session 35 | 36 | id = fields.Number(dump_only=True) 37 | first_name = fields.String(required=True) 38 | last_name = fields.String(required=True) 39 | avatar = fields.String(dump_only=True) 40 | created = fields.String(dump_only=True) 41 | books = fields.Nested(BookSchema, many=True, only=['title','year','id']) 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ch7-code/api/models/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from api.utils.database import db 5 | from marshmallow_sqlalchemy import ModelSchema 6 | from marshmallow import fields 7 | 8 | 9 | class Book(db.Model): 10 | __tablename__ = 'books' 11 | 12 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 13 | title = db.Column(db.String(50)) 14 | year = db.Column(db.Integer) 15 | author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False) 16 | 17 | def __init__(self, title, year, author_id=None): 18 | self.title = title 19 | self.year = year 20 | self.author_id = author_id 21 | 22 | def create(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | return self 26 | 27 | 28 | class BookSchema(ModelSchema): 29 | class Meta(ModelSchema.Meta): 30 | model = Book 31 | sqla_session = db.session 32 | 33 | id = fields.Number(dump_only=True) 34 | title = fields.String(required=True) 35 | year = fields.Integer(required=True) 36 | author_id = fields.Integer() 37 | -------------------------------------------------------------------------------- /ch7-code/api/models/users.py: -------------------------------------------------------------------------------- 1 | class User(db.Model): 2 | __tablename__ = 'users' 3 | 4 | id = db.Column(db.Integer, primary_key = True) 5 | username = db.Column(db.String(120), unique = True, nullable = False) 6 | password = db.Column(db.String(120), nullable = False) 7 | isVerified = db.Column(db.Boolean, nullable=False, default=False) 8 | email = db.Column(db.String(120), unique = True, nullable = False) 9 | def create(self): 10 | db.session.add(self) 11 | db.session.commit() 12 | return self 13 | 14 | @classmethod 15 | def find_by_email(cls, email): 16 | return cls.query.filter_by(email = email).first() 17 | 18 | @classmethod 19 | def find_by_username(cls, username): 20 | print(cls) 21 | print(username) 22 | return cls.query.filter_by(username = username).first() 23 | 24 | @staticmethod 25 | def generate_hash(password): 26 | return sha256.hash(password) 27 | 28 | @staticmethod 29 | def verify_hash(password, hash): 30 | return sha256.verify(password, hash) 31 | 32 | class UserSchema(ModelSchema): 33 | class Meta(ModelSchema.Meta): 34 | model = User 35 | sqla_session = db.session 36 | 37 | id = fields.Number(dump_only=True) 38 | username = fields.String(required=True) 39 | email = fields.String(required=True) -------------------------------------------------------------------------------- /ch7-code/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/routes/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/routes/authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint, request, url_for, current_app 5 | from api.utils.responses import response_with 6 | from api.utils import responses as resp 7 | from api.models.authors import Author, AuthorSchema 8 | from api.utils.database import db 9 | from flask_jwt_extended import jwt_required 10 | import os 11 | 12 | 13 | @author_routes.route('/', methods=['POST']) 14 | @jwt_required 15 | def create_author(): 16 | try: 17 | data = request.get_json() 18 | author_schema = AuthorSchema() 19 | author, error = author_schema.load(data) 20 | result = author_schema.dump(author.create()).data 21 | return response_with(resp.SUCCESS_201, value={"author": result}) 22 | except Exception as e: 23 | return response_with(resp.INVALID_INPUT_422) 24 | 25 | @author_routes.route('/avatar/', methods=['POST']) 26 | @jwt_required 27 | def upsert_author_avatar(author_id): 28 | try: 29 | file = request.files['avatar'] 30 | if file and allowed_file(file.filename): 31 | filename = secure_filename(file.filename) 32 | file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename)) 33 | get_author = Author.query.get_or_404(author_id) 34 | get_author.avatar = url_for('uploaded_file', filename=filename, _external=True) 35 | db.session.add(get_author) 36 | db.session.commit() 37 | author_schema = AuthorSchema() 38 | author, error = author_schema.dump(get_author) 39 | return response_with(resp.SUCCESS_200, value={"author": author}) 40 | except Exception as e: 41 | print e 42 | return response_with(resp.INVALID_INPUT_422) 43 | 44 | @author_routes.route('/', methods=['GET']) 45 | def get_author_list(): 46 | fetched = Author.query.all() 47 | author_schema = AuthorSchema(many=True, only=['first_name', 'last_name', 'id']) 48 | authors, error = author_schema.dump(fetched) 49 | return response_with(resp.SUCCESS_200, value={"authors": authors}) 50 | 51 | @author_routes.route('/', methods=['GET']) 52 | def get_author_detail(author_id): 53 | fetched = Author.query.get_or_404(author_id) 54 | author_schema = AuthorSchema() 55 | author, error = author_schema.dump(fetched) 56 | return response_with(resp.SUCCESS_200, value={"author": author}) 57 | 58 | @author_routes.route('/', methods=['PUT']) 59 | @jwt_required 60 | def update_author_detail(id): 61 | data = request.get_json() 62 | get_author = Author.query.get_or_404(id) 63 | get_author.first_name = data['first_name'] 64 | get_author.last_name = data['last_name'] 65 | db.session.add(get_author) 66 | db.session.commit() 67 | author_schema = AuthorSchema() 68 | author, error = author_schema.dump(get_author) 69 | return response_with(resp.SUCCESS_200, value={"author": author}) 70 | 71 | @author_routes.route('/', methods=['PATCH']) 72 | def modify_author_detail(id): 73 | data = request.get_json() 74 | get_author = Author.query.get(id) 75 | if data.get('first_name'): 76 | get_author.first_name = data['first_name'] 77 | if data.get('last_name'): 78 | get_author.last_name = data['last_name'] 79 | db.session.add(get_author) 80 | db.session.commit() 81 | author_schema = AuthorSchema() 82 | author, error = author_schema.dump(get_author) 83 | return response_with(resp.SUCCESS_200, value={"author": author}) 84 | 85 | @author_routes.route('/', methods=['DELETE']) 86 | def delete_author(id): 87 | get_author = Author.query.get_or_404(id) 88 | db.session.delete(get_author) 89 | db.session.commit() 90 | return response_with(resp.SUCCESS_204) -------------------------------------------------------------------------------- /ch7-code/api/routes/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from api.utils.responses import response_with 7 | from api.utils import responses as resp 8 | from api.models.books import Book, BookSchema 9 | from api.utils.database import db 10 | from flask_jwt_extended import (jwt_required, jwt_refresh_token_required, get_jwt_identity, get_raw_jwt) 11 | 12 | book_routes = Blueprint("book_routes", __name__) 13 | 14 | 15 | @book_routes.route('/', methods=['POST']) 16 | @jwt_required 17 | def create_book(): 18 | try: 19 | data = request.get_json() 20 | book_schema = BookSchema() 21 | book, error = book_schema.load(data) 22 | result = book_schema.dump(book.create()).data 23 | return response_with(resp.SUCCESS_201, value={"book": result}) 24 | except Exception as e: 25 | print e 26 | return response_with(resp.INVALID_INPUT_422) 27 | 28 | 29 | @book_routes.route('/', methods=['GET']) 30 | def get_book_list(): 31 | fetched = Book.query.all() 32 | book_schema = BookSchema(many=True, only=['author_id','title', 'year']) 33 | books, error = book_schema.dump(fetched) 34 | return response_with(resp.SUCCESS_200, value={"books": books}) 35 | 36 | 37 | @book_routes.route('/', methods=['GET']) 38 | def get_book_detail(id): 39 | fetched = Book.query.get_or_404(id) 40 | book_schema = BookSchema() 41 | books, error = book_schema.dump(fetched) 42 | return response_with(resp.SUCCESS_200, value={"books": books}) 43 | 44 | @book_routes.route('/', methods=['PUT']) 45 | @jwt_required 46 | def update_book_detail(id): 47 | data = request.get_json() 48 | get_book = Book.query.get_or_404(id) 49 | get_book.title = data['title'] 50 | get_book.year = data['year'] 51 | db.session.add(get_book) 52 | db.session.commit() 53 | book_schema = BookSchema() 54 | book, error = book_schema.dump(get_book) 55 | return response_with(resp.SUCCESS_200, value={"book": book}) 56 | 57 | @book_routes.route('/', methods=['PATCH']) 58 | @jwt_required 59 | def modify_book_detail(id): 60 | data = request.get_json() 61 | get_book = Book.query.get_or_404(id) 62 | if data.get('title'): 63 | get_book.title = data['title'] 64 | if data.get('year'): 65 | get_book.year = data['year'] 66 | db.session.add(get_book) 67 | db.session.commit() 68 | book_schema = BookSchema() 69 | book, error = book_schema.dump(get_book) 70 | return response_with(resp.SUCCESS_200, value={"book": book}) 71 | 72 | @book_routes.route('/', methods=['DELETE']) 73 | @jwt_required 74 | def delete_book(id): 75 | get_book = Book.query.get_or_404(id) 76 | db.session.delete(get_book) 77 | db.session.commit() 78 | return response_with(resp.SUCCESS_204) 79 | -------------------------------------------------------------------------------- /ch7-code/api/routes/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | from flask import request 6 | from flask import url_for, render_template_string 7 | from api.utils.responses import response_with 8 | from api.utils import responses as resp 9 | from api.models.users import User, UserSchema 10 | from api.utils.database import db 11 | from flask_jwt_extended import create_access_token 12 | from api.utils.token import generate_verification_token, confirm_verification_token 13 | from api.utils.email import send_email 14 | import datetime 15 | 16 | user_routes = Blueprint("user_routes", __name__) 17 | 18 | @user_routes.route('/', methods=['POST']) 19 | def create_user(): 20 | try: 21 | data = request.get_json() 22 | if(User.find_by_email(data['email']) is not None or User.find_by_username(data['username']) is not None): 23 | return response_with(resp.INVALID_INPUT_422) 24 | data['password'] = User.generate_hash(data['password']) 25 | user_schmea = UserSchema() 26 | user, error = user_schmea.load(data) 27 | token = generate_verification_token(data['email']) 28 | verification_email = url_for('user_routes.verify_email', token=token, _external=True) 29 | html = render_template_string("

Welcome! Thanks for signing up. Please follow this link to activate your account:

{{ verification_email }}


Thanks!

", verification_email=verification_email) 30 | subject = "Please Verify your email" 31 | send_email(user.email, subject, html) 32 | result = user_schmea.dump(user.create()).data 33 | return response_with(resp.SUCCESS_201) 34 | except Exception as e: 35 | print e 36 | return response_with(resp.INVALID_INPUT_422) 37 | 38 | @user_routes.route('/confirm/', methods=['GET']) 39 | def verify_email(token): 40 | try: 41 | email = confirm_verification_token(token) 42 | except Exception as e: 43 | return response_with(resp.SERVER_ERROR_401) 44 | user = User.query.filter_by(email=email).first_or_404() 45 | if user.isVerified: 46 | return response_with(resp.INVALID_INPUT_422) 47 | else: 48 | user.isVerified = True 49 | db.session.add(user) 50 | db.session.commit() 51 | return response_with(resp.SUCCESS_200, value={'message': 'E-mail verified, you can proceed to login now.'}) 52 | 53 | @user_routes.route('/login', methods=['POST']) 54 | def authenticate_user(): 55 | try: 56 | data = request.get_json() 57 | if data.get('email') : 58 | current_user = User.find_by_email(data['email']) 59 | elif data.get('username') : 60 | current_user = User.find_by_username(data['username']) 61 | if not current_user: 62 | return response_with(resp.SERVER_ERROR_404) 63 | if current_user and not current_user.isVerified: 64 | return response_with(resp.BAD_REQUEST_400) 65 | if User.verify_hash(data['password'], current_user.password): 66 | access_token = create_access_token(identity = current_user.username) 67 | return response_with(resp.SUCCESS_200, value={'message': 'Logged in as admin', "access_token": access_token}) 68 | else: 69 | return response_with(resp.UNAUTHORIZED_401) 70 | except Exception as e: 71 | print e 72 | return response_with(resp.INVALID_INPUT_422) -------------------------------------------------------------------------------- /ch7-code/api/tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/tests/.DS_Store -------------------------------------------------------------------------------- /ch7-code/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/tests/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/tests/test_authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from api.utils.test_base import BaseTestCase 6 | from api.models.authors import Author 7 | from api.models.books import Book 8 | from datetime import datetime 9 | from flask_jwt_extended import create_access_token 10 | import unittest2 as unittest 11 | import io 12 | 13 | def create_authors(): 14 | author1 = Author(first_name="John", last_name="Doe").create() 15 | Book(title="Test Book 1", year=datetime(1976, 1, 1), author_id=author1.id).create() 16 | Book(title="Test Book 2", year=datetime(1992, 12, 1), author_id=author1.id).create() 17 | 18 | author2 = Author(first_name="Jane", last_name="Doe").create() 19 | Book(title="Test Book 3", year=datetime(1986, 1, 3), author_id=author2.id).create() 20 | Book(title="Test Book 4", year=datetime(1992, 12, 1), author_id=author2.id).create() 21 | 22 | 23 | def login(): 24 | access_token = create_access_token(identity = 'kunal.relan@hotmail.com') 25 | return access_token 26 | 27 | 28 | class TestAuthors(BaseTestCase): 29 | def setUp(self): 30 | super(TestAuthors, self).setUp() 31 | create_authors() 32 | 33 | def test_create_author(self): 34 | token = login() 35 | author = { 36 | 'first_name': 'Johny', 37 | 'last_name': 'Doee' 38 | } 39 | 40 | response = self.app.post( 41 | '/api/authors/', 42 | data=json.dumps(author), 43 | content_type='application/json', 44 | headers= { 'Authorization': 'Bearer '+token } 45 | ) 46 | data = json.loads(response.data) 47 | self.assertEqual(201, response.status_code) 48 | self.assertTrue('author' in data) 49 | 50 | def test_create_author_no_authorization(self): 51 | author = { 52 | 'first_name': 'Johny', 53 | 'last_name': 'Doee' 54 | } 55 | 56 | response = self.app.post( 57 | '/api/authors/', 58 | data=json.dumps(author), 59 | content_type='application/json', 60 | ) 61 | data = json.loads(response.data) 62 | self.assertEqual(401, response.status_code) 63 | 64 | def test_create_author_no_name(self): 65 | token = login() 66 | author = { 67 | 'first_name': 'Johny' 68 | } 69 | 70 | response = self.app.post( 71 | '/api/authors/', 72 | data=json.dumps(author), 73 | content_type='application/json', 74 | headers= { 'Authorization': 'Bearer '+token } 75 | ) 76 | data = json.loads(response.data) 77 | self.assertEqual(422, response.status_code) 78 | 79 | def test_upload_avatar(self): 80 | token = login() 81 | response = self.app.post( 82 | '/api/authors/avatar/2', 83 | data=dict(avatar=(io.BytesIO(b'test'), 'test_file.jpg')), 84 | content_type='multipart/form-data', 85 | headers= { 'Authorization': 'Bearer '+ token } 86 | ) 87 | self.assertEqual(200, response.status_code) 88 | 89 | def test_upload_avatar_without_file(self): 90 | token = login() 91 | response = self.app.post( 92 | '/api/authors/avatar/2', 93 | data=dict(file=(io.BytesIO(b'test'), 'test_file.csv')), 94 | content_type='multipart/form-data', 95 | headers= { 'Authorization': 'Bearer '+ token } 96 | ) 97 | self.assertEqual(422, response.status_code) 98 | 99 | def test_get_authors(self): 100 | response = self.app.get( 101 | '/api/authors/', 102 | content_type='application/json' 103 | ) 104 | data = json.loads(response.data) 105 | self.assertEqual(200, response.status_code) 106 | self.assertTrue('authors' in data) 107 | 108 | def test_get_author_detail(self): 109 | response = self.app.get( 110 | '/api/authors/2', 111 | content_type='application/json' 112 | ) 113 | data = json.loads(response.data) 114 | self.assertEqual(200, response.status_code) 115 | self.assertTrue('author' in data) 116 | self.assertTrue('books' in data['author']) 117 | 118 | def test_update_author(self): 119 | token = login() 120 | author = { 121 | 'first_name': 'Joseph' 122 | } 123 | response = self.app.put( 124 | '/api/authors/2', 125 | data=json.dumps(author), 126 | content_type='application/json', 127 | headers= { 'Authorization': 'Bearer '+token } 128 | ) 129 | self.assertEqual(200, response.status_code) 130 | def test_delete_author(self): 131 | token = login() 132 | response = self.app.delete( 133 | '/api/authors/2', 134 | headers= { 'Authorization': 'Bearer '+token } 135 | ) 136 | self.assertEqual(204, response.status_code) 137 | 138 | def test_create_book(self): 139 | token = login() 140 | author = { 141 | 'title': 'Alice in wonderland', 142 | 'year': 1982, 143 | 'author_id': 2 144 | } 145 | 146 | response = self.app.post( 147 | '/api/books/', 148 | data=json.dumps(author), 149 | content_type='application/json', 150 | headers= { 'Authorization': 'Bearer '+token } 151 | ) 152 | data = json.loads(response.data) 153 | self.assertEqual(201, response.status_code) 154 | self.assertTrue('book' in data) 155 | 156 | def test_create_book_no_author(self): 157 | token = login() 158 | author = { 159 | 'title': 'Alice in wonderland', 160 | 'year': 1982 161 | } 162 | 163 | response = self.app.post( 164 | '/api/books/', 165 | data=json.dumps(author), 166 | content_type='application/json', 167 | headers= { 'Authorization': 'Bearer '+token } 168 | ) 169 | data = json.loads(response.data) 170 | self.assertEqual(422, response.status_code) 171 | 172 | def test_create_book_no_authorization(self): 173 | author = { 174 | 'title': 'Alice in wonderland', 175 | 'year': 1982, 176 | 'author_id': 2 177 | } 178 | 179 | response = self.app.post( 180 | '/api/books/', 181 | data=json.dumps(author), 182 | content_type='application/json' 183 | ) 184 | data = json.loads(response.data) 185 | self.assertEqual(401, response.status_code) 186 | 187 | def test_get_books(self): 188 | response = self.app.get( 189 | '/api/books/', 190 | content_type='application/json' 191 | ) 192 | data = json.loads(response.data) 193 | self.assertEqual(200, response.status_code) 194 | self.assertTrue('books' in data) 195 | 196 | def test_get_book_details(self): 197 | response = self.app.get( 198 | '/api/books/2', 199 | content_type='application/json' 200 | ) 201 | data = json.loads(response.data) 202 | self.assertEqual(200, response.status_code) 203 | self.assertTrue('books' in data) 204 | 205 | def test_update_book(self): 206 | token = login() 207 | author = { 208 | 'year': 1992, 209 | 'title': 'Alice' 210 | } 211 | response = self.app.put( 212 | '/api/books/2', 213 | data=json.dumps(author), 214 | content_type='application/json', 215 | headers= { 'Authorization': 'Bearer '+token } 216 | ) 217 | self.assertEqual(200, response.status_code) 218 | 219 | def test_delete_book(self): 220 | token = login() 221 | response = self.app.delete( 222 | '/api/books/2', 223 | headers= { 'Authorization': 'Bearer '+token } 224 | ) 225 | self.assertEqual(204, response.status_code) 226 | 227 | 228 | if __name__ == '__main__': 229 | unittest.main() -------------------------------------------------------------------------------- /ch7-code/api/tests/test_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from datetime import datetime 6 | import unittest2 as unittest 7 | from api.utils.token import generate_verification_token, confirm_verification_token 8 | from api.utils.test_base import BaseTestCase 9 | from api.models.users import User 10 | 11 | def create_users(): 12 | user1 = User(email="kunal.relan12@gmail.com", username='kunalrelan12', password=User.generate_hash('helloworld'), isVerified=True).create() 13 | user2 = User(email="kunal.relan123@gmail.com", username='kunalrelan125', password=User.generate_hash('helloworld')).create() 14 | 15 | 16 | class TestUsers(BaseTestCase): 17 | def setUp(self): 18 | super(TestUsers, self).setUp() 19 | create_users() 20 | 21 | def test_login_user(self): 22 | newuser = { 23 | "email" : "kunal.relan12@gmail.com", 24 | "password" : "helloworld" 25 | } 26 | response = self.app.post( 27 | '/api/users/login', 28 | data=json.dumps(newuser), 29 | content_type='application/json' 30 | ) 31 | data = json.loads(response.data) 32 | self.assertEqual(200, response.status_code) 33 | self.assertTrue('access_token' in data) 34 | 35 | def test_login_user_wrong_credentials(self): 36 | user = { 37 | "email" : "kunal.relan12@gmail.com", 38 | "password" : "helloworld12" 39 | } 40 | response = self.app.post( 41 | '/api/users/login', 42 | data=json.dumps(user), 43 | content_type='application/json' 44 | ) 45 | data = json.loads(response.data) 46 | self.assertEqual(401, response.status_code) 47 | 48 | def test_login_unverified_user(self): 49 | user = { 50 | "email" : "kunal.relan123@gmail.com", 51 | "password" : "helloworld" 52 | } 53 | response = self.app.post( 54 | '/api/users/login', 55 | data=json.dumps(user), 56 | content_type='application/json' 57 | ) 58 | data = json.loads(response.data) 59 | self.assertEqual(400, response.status_code) 60 | 61 | 62 | def test_create_user(self): 63 | user = { 64 | "username" : "kunalrelan2", 65 | "password" : "helloworld", 66 | "email" : "kunal.relan12@hotmail.com" 67 | } 68 | 69 | response = self.app.post( 70 | '/api/users/', 71 | data=json.dumps(user), 72 | content_type='application/json' 73 | ) 74 | data = json.loads(response.data) 75 | self.assertEqual(201, response.status_code) 76 | self.assertTrue('success' in data['code']) 77 | 78 | def test_create_user_without_username(self): 79 | user = { 80 | "password" : "helloworld", 81 | "email" : "kunal.relan12@hotmail.com" 82 | } 83 | 84 | response = self.app.post( 85 | '/api/users/', 86 | data=json.dumps(user), 87 | content_type='application/json' 88 | ) 89 | data = json.loads(response.data) 90 | self.assertEqual(422, response.status_code) 91 | 92 | def test_confirm_email(self): 93 | token = generate_verification_token('kunal.relan123@gmail.com') 94 | 95 | response = self.app.get( 96 | '/api/users/confirm/'+token 97 | ) 98 | data = json.loads(response.data) 99 | print(data) 100 | self.assertEqual(200, response.status_code) 101 | self.assertTrue('success' in data['code']) 102 | 103 | def test_confirm_email_for_verified_user(self): 104 | token = generate_verification_token('kunal.relan12@gmail.com') 105 | 106 | response = self.app.get( 107 | '/api/users/confirm/'+token 108 | ) 109 | data = json.loads(response.data) 110 | print(data) 111 | self.assertEqual(422, response.status_code) 112 | 113 | 114 | def test_confirm_email_with_incorrect_email(self): 115 | token = generate_verification_token('kunal.relan43@gmail.com') 116 | 117 | response = self.app.get( 118 | '/api/users/confirm/'+token 119 | ) 120 | data = json.loads(response.data) 121 | self.assertEqual(404, response.status_code) 122 | 123 | 124 | if __name__ == '__main__': 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /ch7-code/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/building-rest-apis-with-flask/322c156e85945bf0fd51267caa396bf43f6ca6bd/ch7-code/api/utils/__init__.py -------------------------------------------------------------------------------- /ch7-code/api/utils/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask_sqlalchemy import SQLAlchemy 5 | db = SQLAlchemy() -------------------------------------------------------------------------------- /ch7-code/api/utils/responses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import make_response, jsonify 5 | 6 | INVALID_FIELD_NAME_SENT_422 = { 7 | "http_code": 422, 8 | "code": "invalidField", 9 | "message": "Invalid fields found" 10 | } 11 | 12 | INVALID_INPUT_422 = { 13 | "http_code": 422, 14 | "code": "invalidInput", 15 | "message": "Invalid input" 16 | } 17 | 18 | MISSING_PARAMETERS_422 = { 19 | "http_code": 422, 20 | "code": "missingParameter", 21 | "message": "Missing parameters." 22 | } 23 | 24 | BAD_REQUEST_400 = { 25 | "http_code": 400, 26 | "code": "badRequest", 27 | "message": "Bad request" 28 | } 29 | 30 | SERVER_ERROR_500 = { 31 | "http_code": 500, 32 | "code": "serverError", 33 | "message": "Server error" 34 | } 35 | 36 | SERVER_ERROR_404 = { 37 | "http_code": 404, 38 | "code": "notFound", 39 | "message": "Resource not found" 40 | } 41 | 42 | FORBIDDEN_403 = { 43 | "http_code": 403, 44 | "code": "notAuthorized", 45 | "message": "You are not authorised to execute this." 46 | } 47 | UNAUTHORIZED_401 = { 48 | "http_code": 401, 49 | "code": "notAuthorized", 50 | "message": "Invalid authentication." 51 | } 52 | 53 | NOT_FOUND_HANDLER_404 = { 54 | "http_code": 404, 55 | "code": "notFound", 56 | "message": "route not found" 57 | } 58 | 59 | SUCCESS_200 = { 60 | 'http_code': 200, 61 | 'code': 'success' 62 | } 63 | 64 | SUCCESS_201 = { 65 | 'http_code': 201, 66 | 'code': 'success' 67 | } 68 | 69 | SUCCESS_204 = { 70 | 'http_code': 204, 71 | 'code': 'success' 72 | } 73 | 74 | 75 | def response_with(response, value=None, message=None, error=None, headers={}, pagination=None): 76 | result = {} 77 | if value is not None: 78 | result.update(value) 79 | 80 | if response.get('message', None) is not None: 81 | result.update({'message': response['message']}) 82 | 83 | result.update({'code': response['code']}) 84 | 85 | if error is not None: 86 | result.update({'errors': error}) 87 | 88 | if pagination is not None: 89 | result.update({'pagination': pagination}) 90 | 91 | headers.update({'Access-Control-Allow-Origin': '*'}) 92 | headers.update({'server': 'Flask REST API'}) 93 | 94 | return make_response(jsonify(result), response['http_code'], headers) 95 | -------------------------------------------------------------------------------- /ch7-code/api/utils/test_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest2 as unittest 5 | from main import create_app 6 | from api.utils.database import db 7 | from api.config.config import TestingConfig 8 | import tempfile 9 | 10 | class BaseTestCase(unittest.TestCase): 11 | def setUp(self): 12 | app = create_app(TestingConfig) 13 | self.test_db_file = tempfile.mkstemp()[1] 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + self.test_db_file 15 | with app.app_context(): 16 | db.create_all() 17 | app.app_context().push() 18 | self.app = app.test_client() 19 | 20 | def tearDown(self): 21 | db.session.close_all() 22 | db.drop_all() 23 | 24 | -------------------------------------------------------------------------------- /ch7-code/api/utils/token.py: -------------------------------------------------------------------------------- 1 | from itsdangerous import URLSafeTimedSerializer 2 | from flask import current_app 3 | 4 | 5 | def generate_verification_token(email): 6 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 7 | return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) 8 | 9 | 10 | def confirm_verification_token(token, expiration=3600): 11 | serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) 12 | try: 13 | email = serializer.loads( 14 | token, 15 | salt=current_app.config['SECURITY_PASSWORD_SALT'], 16 | max_age=expiration 17 | ) 18 | except Exception as e: 19 | return e 20 | return email -------------------------------------------------------------------------------- /ch7-code/code-apache.conf: -------------------------------------------------------------------------------- 1 | 2 | # The ServerName directive sets the request scheme, hostname and port that 3 | # the server uses to identify itself. This is used when creating 4 | # redirection URLs. In the context of virtual hosts, the ServerName 5 | # specifies what hostname must appear in the request's Host: header to 6 | # match this virtual host. For the default virtual host (this file) this 7 | # value is not decisive as it is used as a last resort host regardless. 8 | # However, you must set it for any further virtual host explicitly. 9 | #ServerName www.example.com 10 | 11 | ServerAdmin webmaster@localhost 12 | DocumentRoot /var/www/html 13 | 14 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, 15 | # error, crit, alert, emerg. 16 | # It is also possible to configure the loglevel for particular 17 | # modules, e.g. 18 | #LogLevel info ssl:warn 19 | 20 | ErrorLog ${APACHE_LOG_DIR}/error.log 21 | CustomLog ${APACHE_LOG_DIR}/access.log combined 22 | 23 | Order deny,allow 24 | Allow from all 25 | 26 | ProxyPreserveHost On 27 | 28 | ProxyPass "http://127.0.0.1:5000/" 29 | ProxyPassReverse "http://127.0.0.1:5000/" 30 | 31 | # For most configuration files from conf-available/, which are 32 | # enabled or disabled at a global level, it is possible to 33 | # include a line for only one particular virtual host. For example the 34 | # following line enables the CGI configuration for this host only 35 | # after it has been globally disabled with "a2disconf". 36 | #Include conf-available/serve-cgi-bin.conf 37 | 38 | -------------------------------------------------------------------------------- /ch7-code/code-app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | handlers: 5 | - url: /avatar 6 | static_dir: images 7 | - url: /.* 8 | script: wsgi.application 9 | 10 | libraries: 11 | - name: ssl 12 | version: latest 13 | env_variables: 14 | WORK_ENV: PROD -------------------------------------------------------------------------------- /ch7-code/code-flask-app-gunicorn.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description= Flask App service 3 | After=network.target 4 | 5 | [Service] 6 | User=flask 7 | Group=www-data 8 | Restart=on-failure 9 | Environment="WORK_ENV=PROD" 10 | WorkingDirectory=/home/flask/flask-api-app/src 11 | ExecStart=/home/flask/flask-api-app/src/venv/bin/gunicorn -c /home/flask/flask-api-app/src/gunicorn.conf -b 0.0.0.0:5000 wsgi:application 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ch7-code/code-flask-app.service: -------------------------------------------------------------------------------- 1 | #Metadata and dependencies section 2 | [Unit] 3 | Description=Flask App service 4 | After=network.target 5 | #Define users and app working directory 6 | [Service] 7 | User=flask 8 | Group=www-data 9 | WorkingDirectory=/home/flask/flask-api-app/src 10 | Environment="WORK_ENV=PROD" 11 | ExecStart=/home/flask/flask-api-app/src/venv/bin/uwsgi --ini flask-app.ini 12 | #Link the service to start on multi-user system up 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ch7-code/code-newrelic.ini: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------------- 2 | 3 | # 4 | # This file configures the New Relic Python Agent. 5 | # 6 | # The path to the configuration file should be supplied to the function 7 | # newrelic.agent.initialize() when the agent is being initialized. 8 | # 9 | # The configuration file follows a structure similar to what you would 10 | # find for Microsoft Windows INI files. For further information on the 11 | # configuration file format see the Python ConfigParser documentation at: 12 | # 13 | # http://docs.python.org/library/configparser.html 14 | # 15 | # For further discussion on the behaviour of the Python agent that can 16 | # be configured via this configuration file see: 17 | # 18 | # http://newrelic.com/docs/python/python-agent-configuration 19 | # 20 | 21 | # --------------------------------------------------------------------------- 22 | 23 | # Here are the settings that are common to all environments. 24 | 25 | [newrelic] 26 | 27 | # You must specify the license key associated with your New 28 | # Relic account. This key binds the Python Agent's data to your 29 | # account in the New Relic service. 30 | license_key = 31 | 32 | # The application name. Set this to be the name of your 33 | # application as you would like it to show up in New Relic UI. 34 | # The UI will then auto-map instances of your application into a 35 | # entry on your home dashboard page. 36 | app_name = Python Application 37 | 38 | # When "true", the agent collects performance data about your 39 | # application and reports this data to the New Relic UI at 40 | # newrelic.com. This global switch is normally overridden for 41 | # each environment below. 42 | monitor_mode = true 43 | 44 | # Sets the name of a file to log agent messages to. Useful for 45 | # debugging any issues with the agent. This is not set by 46 | # default as it is not known in advance what user your web 47 | # application processes will run as and where they have 48 | # permission to write to. Whatever you set this to you must 49 | # ensure that the permissions for the containing directory and 50 | # the file itself are correct, and that the user that your web 51 | # application runs as can write to the file. If not able to 52 | # write out a log file, it is also possible to say "stderr" and 53 | # output to standard error output. This would normally result in 54 | # output appearing in your web server log. 55 | #log_file = /tmp/newrelic-python-agent.log 56 | 57 | # Sets the level of detail of messages sent to the log file, if 58 | # a log file location has been provided. Possible values, in 59 | # increasing order of detail, are: "critical", "error", "warning", 60 | # "info" and "debug". When reporting any agent issues to New 61 | # Relic technical support, the most useful setting for the 62 | # support engineers is "debug". However, this can generate a lot 63 | # of information very quickly, so it is best not to keep the 64 | # agent at this level for longer than it takes to reproduce the 65 | # problem you are experiencing. 66 | log_level = info 67 | 68 | # High Security Mode enforces certain security settings, and prevents 69 | # them from being overridden, so that no sensitive data is sent to New 70 | # Relic. Enabling High Security Mode means that request parameters are 71 | # not collected and SQL can not be sent to New Relic in its raw form. 72 | # To activate High Security Mode, it must be set to 'true' in this 73 | # local .ini configuration file AND be set to 'true' in the 74 | # server-side configuration in the New Relic user interface. For 75 | # details, see 76 | # https://docs.newrelic.com/docs/subscriptions/high-security 77 | high_security = false 78 | 79 | # The Python Agent will attempt to connect directly to the New 80 | # Relic service. If there is an intermediate firewall between 81 | # your host and the New Relic service that requires you to use a 82 | # HTTP proxy, then you should set both the "proxy_host" and 83 | # "proxy_port" settings to the required values for the HTTP 84 | # proxy. The "proxy_user" and "proxy_pass" settings should 85 | # additionally be set if proxy authentication is implemented by 86 | # the HTTP proxy. The "proxy_scheme" setting dictates what 87 | # protocol scheme is used in talking to the HTTP proxy. This 88 | # would normally always be set as "http" which will result in the 89 | # agent then using a SSL tunnel through the HTTP proxy for end to 90 | # end encryption. 91 | # proxy_scheme = http 92 | # proxy_host = hostname 93 | # proxy_port = 8080 94 | # proxy_user = 95 | # proxy_pass = 96 | 97 | # Capturing request parameters is off by default. To enable the 98 | # capturing of request parameters, first ensure that the setting 99 | # "attributes.enabled" is set to "true" (the default value), and 100 | # then add "request.parameters.*" to the "attributes.include" 101 | # setting. For details about attributes configuration, please 102 | # consult the documentation. 103 | # attributes.include = request.parameters.* 104 | 105 | # The transaction tracer captures deep information about slow 106 | # transactions and sends this to the UI on a periodic basis. The 107 | # transaction tracer is enabled by default. Set this to "false" 108 | # to turn it off. 109 | transaction_tracer.enabled = true 110 | 111 | # Threshold in seconds for when to collect a transaction trace. 112 | # When the response time of a controller action exceeds this 113 | # threshold, a transaction trace will be recorded and sent to 114 | # the UI. Valid values are any positive float value, or (default) 115 | # "apdex_f", which will use the threshold for a dissatisfying 116 | # Apdex controller action - four times the Apdex T value. 117 | transaction_tracer.transaction_threshold = apdex_f 118 | 119 | # When the transaction tracer is on, SQL statements can 120 | # optionally be recorded. The recorder has three modes, "off" 121 | # which sends no SQL, "raw" which sends the SQL statement in its 122 | # original form, and "obfuscated", which strips out numeric and 123 | # string literals. 124 | transaction_tracer.record_sql = obfuscated 125 | 126 | # Threshold in seconds for when to collect stack trace for a SQL 127 | # call. In other words, when SQL statements exceed this 128 | # threshold, then capture and send to the UI the current stack 129 | # trace. This is helpful for pinpointing where long SQL calls 130 | # originate from in an application. 131 | transaction_tracer.stack_trace_threshold = 0.5 132 | 133 | # Determines whether the agent will capture query plans for slow 134 | # SQL queries. Only supported in MySQL and PostgreSQL. Set this 135 | # to "false" to turn it off. 136 | transaction_tracer.explain_enabled = true 137 | 138 | # Threshold for query execution time below which query plans 139 | # will not not be captured. Relevant only when "explain_enabled" 140 | # is true. 141 | transaction_tracer.explain_threshold = 0.5 142 | 143 | # Space separated list of function or method names in form 144 | # 'module:function' or 'module:class.function' for which 145 | # additional function timing instrumentation will be added. 146 | transaction_tracer.function_trace = 147 | 148 | # The error collector captures information about uncaught 149 | # exceptions or logged exceptions and sends them to UI for 150 | # viewing. The error collector is enabled by default. Set this 151 | # to "false" to turn it off. 152 | error_collector.enabled = true 153 | 154 | # To stop specific errors from reporting to the UI, set this to 155 | # a space separated list of the Python exception type names to 156 | # ignore. The exception name should be of the form 'module:class'. 157 | error_collector.ignore_errors = 158 | 159 | # Browser monitoring is the Real User Monitoring feature of the UI. 160 | # For those Python web frameworks that are supported, this 161 | # setting enables the auto-insertion of the browser monitoring 162 | # JavaScript fragments. 163 | browser_monitoring.auto_instrument = true 164 | 165 | # A thread profiling session can be scheduled via the UI when 166 | # this option is enabled. The thread profiler will periodically 167 | # capture a snapshot of the call stack for each active thread in 168 | # the application to construct a statistically representative 169 | # call tree. 170 | thread_profiler.enabled = true 171 | 172 | # Your application deployments can be recorded through the 173 | # New Relic REST API. To use this feature provide your API key 174 | # below then use the `newrelic-admin record-deploy` command. 175 | # api_key = 176 | 177 | # Distributed tracing lets you see the path that a request takes 178 | # through your distributed system. Enabling distributed tracing 179 | # changes the behavior of some New Relic features, so carefully 180 | # consult the transition guide before you enable this feature: 181 | # https://docs.newrelic.com/docs/transition-guide-distributed-tracing 182 | distributed_tracing.enabled = false 183 | 184 | # --------------------------------------------------------------------------- 185 | 186 | # 187 | # The application environments. These are specific settings which 188 | # override the common environment settings. The settings related to a 189 | # specific environment will be used when the environment argument to the 190 | # newrelic.agent.initialize() function has been defined to be either 191 | # "development", "test", "staging" or "production". 192 | # 193 | 194 | [newrelic:development] 195 | monitor_mode = false 196 | 197 | [newrelic:test] 198 | monitor_mode = false 199 | 200 | [newrelic:staging] 201 | app_name = Python Application (Staging) 202 | monitor_mode = true 203 | 204 | [newrelic:production] 205 | monitor_mode = true 206 | 207 | # --------------------------------------------------------------------------- 208 | -------------------------------------------------------------------------------- /ch7-code/code-nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | server_name flaskapp; 5 | 6 | location / { 7 | include uwsgi_params; 8 | uwsgi_pass unix:/home/flask/flask-api-app/src/flask-app.sock; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ch7-code/flask-app.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = run:application 3 | 4 | master = true 5 | processes = 5 6 | 7 | socket = flask-app.sock 8 | chmod-socket = 660 9 | vacuum = true 10 | 11 | die-on-term = true 12 | -------------------------------------------------------------------------------- /ch7-code/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask 4 | from flask import jsonify 5 | from apispec.ext.marshmallow import MarshmallowPlugin 6 | from apispec_webframeworks.flask import FlaskPlugin 7 | from api.utils.database import db 8 | from api.utils.responses import response_with 9 | import api.utils.responses as resp 10 | from api.routes.authors import author_routes 11 | from api.routes.books import book_routes 12 | from api.routes.users import user_routes 13 | from flask_jwt_extended import JWTManager 14 | from api.config.config import DevelopmentConfig, ProductionConfig, TestingConfig 15 | from flask import send_from_directory 16 | from flask_jwt_extended import JWTManager 17 | from flask_swagger import swagger 18 | from flask_swagger_ui import get_swaggerui_blueprint 19 | from apispec import APISpec 20 | import os 21 | import flask_monitoringdashboard as dashboard 22 | import sentry_sdk 23 | from sentry_sdk.integrations.flask import \ 24 | FlaskIntegration 25 | 26 | SWAGGER_URL = '/api/docs' 27 | 28 | sentry_sdk.init( 29 | dsn="https://9a2e4316112f44c7944bbae6c1f2207c@sentry.io/1454543", 30 | integrations=[FlaskIntegration()] 31 | ) 32 | 33 | app = Flask(__name__) 34 | dashboard.bind(app) 35 | 36 | 37 | if os.environ.get('WORK_ENV') == 'PROD': 38 | app_config = ProductionConfig 39 | elif os.environ.get('WORK_ENV') == 'TEST': 40 | app_config = TestingConfig 41 | else: 42 | app_config = DevelopmentConfig 43 | 44 | app.config.from_object(app_config) 45 | 46 | db.init_app(app) 47 | with app.app_context(): 48 | db.create_all() 49 | app.register_blueprint(author_routes, url_prefix='/api/authors') 50 | app.register_blueprint(book_routes, url_prefix='/api/books') 51 | app.register_blueprint(user_routes, url_prefix='/api/users') 52 | 53 | 54 | @app.route('/avatar/') 55 | def uploaded_file(filename): 56 | return send_from_directory(app.config['UPLOAD_FOLDER'],filename) 57 | 58 | @app.after_request 59 | def add_header(response): 60 | return response 61 | 62 | @app.errorhandler(400) 63 | def bad_request(e): 64 | logging.error(e) 65 | return response_with(resp.BAD_REQUEST_400) 66 | 67 | @app.errorhandler(500) 68 | def server_error(e): 69 | logging.error(e) 70 | return response_with(resp.SERVER_ERROR_500) 71 | 72 | @app.errorhandler(404) 73 | def not_found(e): 74 | logging.error(e) 75 | return response_with(resp.SERVER_ERROR_404) 76 | 77 | # END GLOBAL HTTP CONFIGURATIONS 78 | 79 | @app.route("/api/spec") 80 | def spec(): 81 | swag = swagger(app, prefix='/api') 82 | swag['info']['base'] = "http://localhost:5000" 83 | swag['info']['version'] = "1.0" 84 | swag['info']['title'] = "Flask Author DB" 85 | return jsonify(swag) 86 | 87 | swaggerui_blueprint = get_swaggerui_blueprint('/api/docs', '/api/spec', config={'app_name': "Flask Author DB"}) 88 | app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) 89 | jwt = JWTManager(app) 90 | db.init_app(app) 91 | mail.init_app(app) 92 | with app.app_context(): 93 | # from api.models import * 94 | db.create_all() 95 | 96 | if __name__ == "__main__": 97 | app.run(port=5000, host="0.0.0.0", use_reloader=False) -------------------------------------------------------------------------------- /ch7-code/run.py: -------------------------------------------------------------------------------- 1 | from main import app as application 2 | 3 | if __name__ == "__main__": 4 | application.run() -------------------------------------------------------------------------------- /ch7-code/text.log: -------------------------------------------------------------------------------- 1 | *** Starting uWSGI 2.0.18 (64bit) on [Wed Apr 24 17:26:41 2019] *** 2 | compiled with version: 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4) on 23 April 2019 17:13:02 3 | os: Darwin-18.2.0 Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 4 | nodename: Kunals-MacBook-Pro.local 5 | machine: x86_64 6 | clock source: unix 7 | pcre jit disabled 8 | detected number of CPU cores: 4 9 | current working directory: /Users/kunalrelan/Desktop/flask-api-starter/src 10 | detected binary path: /usr/local/bin/uwsgi 11 | *** WARNING: you are running uWSGI without its master process manager *** 12 | your processes number limit is 709 13 | your memory page size is 4096 bytes 14 | detected max file descriptor number: 10240 15 | lock engine: OSX spinlocks 16 | thunder lock: disabled (you can enable it with --thunder-lock) 17 | uwsgi socket 0 bound to TCP address 0.0.0.0:5000 fd 3 18 | Python version: 2.7.10 (default, Aug 17 2018, 19:45:58) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] 19 | *** Python threads support is disabled. You can enable it with --enable-threads *** 20 | Python main interpreter initialized at 0x7ff28df01200 21 | your server socket listen backlog is limited to 100 connections 22 | your mercy for graceful operations on workers is 60 seconds 23 | mapped 72888 bytes (71 KB) for 1 cores 24 | *** Operational MODE: single process *** 25 | unable to load app 0 (mountpoint='') (callable not found or import error) 26 | *** no app loaded. going in full dynamic mode *** 27 | *** uWSGI is running in multiple interpreter mode *** 28 | spawned uWSGI worker 1 (and the only) (pid: 50108, cores: 1) 29 | --- no python application found, check your startup logs for errors --- 30 | [pid: 50108|app: -1|req: -1/1] 127.0.0.1 () {38 vars in 1073 bytes} [Wed Apr 24 17:26:48 2019] GET /api/docs => generated 21 bytes in 4 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 31 | --- no python application found, check your startup logs for errors --- 32 | [pid: 50108|app: -1|req: -1/2] 127.0.0.1 () {40 vars in 1033 bytes} [Wed Apr 24 17:26:48 2019] GET /favicon.ico => generated 21 bytes in 509 msecs (HTTP/1.1 500) 2 headers in 83 bytes (1 switches on core 0) 33 | -------------------------------------------------------------------------------- /errata.md: -------------------------------------------------------------------------------- 1 | # Errata for *Book Title* 2 | 3 | On **page xx** [Summary of error]: 4 | 5 | Details of error here. Highlight key pieces in **bold**. 6 | 7 | *** 8 | 9 | On **page xx** [Summary of error]: 10 | 11 | Details of error here. Highlight key pieces in **bold**. 12 | 13 | *** --------------------------------------------------------------------------------