通过前面两篇文章,我们已经初步实现了一些简单的接口。还有很多需要做的工作,比如项目结构优化,接口请求权限控制等等。接下来,首先来优化一下,我们的项目结构。前面我们的所有程序,都是写在一个文件中的,这显然是不合理的。这次内容中,我们将使用 Flask 的 Blueprint 功能,完成项目结构的改进和优化。Blueprint 对于大型应用程序非常有价值,可以简化大型应用程序的工作。这次内容有涉及三个方面。第一,完善项目结构;第二、重构 author 接口接口;第三、新增 books 相关接口。
项目结构
以下面是最终完成的项目结构。在结构调整过程中,我重构了 author 接口、新增了 books 接口、加了日志、配置文件等相关内容。
代码语言:javascript复制|-- app
| |-- __init__.py
| |-- author
| | |-- __init__.py
| | |-- models.py
| | |-- routes.py
| | `-- schema.py
| |-- books
| | |-- __init__.py
| | |-- models.py
| | |-- routes.py
| | `-- schema.py
| `-- utils
| |-- __init__.py
| |-- log.py
| `-- responses.py
|-- config.py
|-- logs
|-- main.py
首先,我们需要在 api 目录下,新建一个名字为 app 的 python 包。然后在 app 包目录下,新建 author、books、utils 三个包。增加完三个目录后,返回上一级,继续新建 main.py 和 config.py 文件。
程序入口文件
每个项目都有入口文件或者 main 文件。使用你喜欢的文本编辑器打开 main.py 编写如下代码。
代码语言:javascript复制from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(port=5000,host='0.0.0.0')
配置文件
接下来,打开 config.py 编写配置程序。
代码语言:javascript复制import os
import logging
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
# load env
load_dotenv(os.path.join(basedir, '.flaskenv'))
# log dir
log_dir = os.path.join(basedir, os.getenv('LOG_DIR', 'logs'))
class Config(object):
SQLALCHEMY_DATABASE_URI = 'sqlite:///books.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
LOG_LEVEL = logging.INFO
在配置文件中,我配置了数据库连接 URI,日志的级别和目录。项目日后功能涉及的配置代码,都可以写在这里。例如发送邮件相关配置,缓存相关配置等。配置文件编写完成后,接着编写日志相关的功能。
日志功能
日志功能,是一个必要的功能。每一个部署到生产环境的项目,都会有日志功能。这对于程序 bug 的定位和程序的调试是非常必要的,必不可少的一个功能。首先,进入 utils 目录下,新建一个 log.py 文件,在文件中,编写如下程序。
代码语言:javascript复制import os
import datetime
from logging.handlers import BaseRotatingHandler
class DayRotatingHandler(BaseRotatingHandler):
def __init__(self, filename, mode, encoding=None, delay=False):
self.date = datetime.date.today()
self.suffix = "%Y-%m-%d.log"
super(BaseRotatingHandler, self).__init__(filename, mode, encoding, delay)
def shouldRollover(self, record):
return self.date != datetime.date.today()
def doRollover(self):
if self.stream:
self.stream.close()
self.stream = None
new_log_file = os.path.join(os.path.split(self.baseFilename)[0], datetime.date.today().strftime(self.suffix))
self.baseFilename = "{}".format(new_log_file)
self._open()
上面程序主要功能是实现日志按天分割的功能,这个功能可以让我们更加方便的定位应用在使用过程中出现的问题。
统一的接口响应
我们这个项目是前后端分离的应用,通过接口进行前后端通信的。接口的返回信息(响应)需要统一格式,这包含接口出错时的信息。为了实现这一目标,我们需要自己封装一个统一的接口响应方法。具体实现程序如下:
代码语言:javascript复制from flask import make_response, jsonify
INVALID_FIELD_NAME_SENT_422 = {
"http_code": 422,
"code": "invalidField",
"message": "Invalid fields found"
}
INVALID_INPUT_422 = {
"http_code": 422,
"code": "invalidInput",
"message": "Invalid input"
}
MISSING_PARAMETERS_422 = {
"http_code": 422,
"code": "missingParameter",
"message": "Missing parameters."
}
BAD_REQUEST_400 = {
"http_code": 400,
"code": "badRequest",
"message": "Bad request"
}
SERVER_ERROR_500 = {
"http_code": 500,
"code": "serverError",
"message": "Server error"
}
SERVER_ERROR_404 = {
"http_code": 404,
"code": "notFound",
"message": "Resource not found"
}
FORBIDDEN_403 = {
"http_code": 403,
"code": "notAuthorized",
"message": "You are not authorised to execute this."
}
UNAUTHORIZED_401 = {
"http_code": 401,
"code": "notAuthorized",
"message": "Invalid authentication."
}
NOT_FOUND_HANDLER_404 = {
"http_code": 404,
"code": "notFound",
"message": "route not found"
}
SUCCESS_200 = {
'http_code': 200,
'code': 'success'
}
SUCCESS_201 = {
'http_code': 201,
'code': 'success'
}
SUCCESS_204 = {
'http_code': 204,
'code': 'success'
}
def response_with(response, value=None, message=None, error=None, headers={}, pagination=None):
result = {}
if value is not None:
result.update(value)
if response.get('message', None) is not None:
result.update({'message': response['message']})
result.update({'code': response['code']})
if error is not None:
result.update({'errors': error})
if pagination is not None:
result.update({'pagination': pagination})
headers.update({'Access-Control-Allow-Origin': '*'})
headers.update({'server': 'Flask REST API'})
return make_response(jsonify(result), response['http_code'], headers)
上述程序,我们自定义了程序出现错误是的错误信息,统一的接口响应处理程序。完成上述程序的编写之后,紧接着来完成我们在入口程序中定义的 create_app 函数。
Flask App 核心程序
打开 app 目录下的init.py 文件,编写如下程序。
代码语言:javascript复制import os
import logging
from logging.handlers import RotatingFileHandler
import datetime
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_cors import CORS
from flask_migrate import Migrate
from config import Config, log_dir
from app.utils.log import DayRotatingHandler
from app.utils import responses as resp
from app.utils.responses import response_with
db = SQLAlchemy()
cors = CORS()
migrate = Migrate()
marshmallow = Marshmallow()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# 注册插件
register_plugins(app)
# 注册蓝图
register_blueprints(app)
# 注册日志处理器
register_logging(app)
# 注册错误处理函数
register_errors(app)
app.logger.info('Flask Rest Api startup')
return app
def register_logging(app):
app.logger.name = 'flask_api'
log_level = app.config.get("LOG_LEVEL", logging.INFO)
cls_handler = logging.StreamHandler()
log_file = os.path.join(log_dir, datetime.date.today().strftime("%Y-%m-%d.log"))
file_handler = DayRotatingHandler(log_file, mode="a", encoding="utf-8")
logging.basicConfig(level=log_level,
format="%(asctime)s %(name)s "
"%(filename)s[%(lineno)d] %(funcName)s() %(levelname)s: %(message)s",
datefmt="%Y/%m/%d %H:%M:%S",
handlers=[cls_handler, file_handler])
if not app.debug and not app.testing:
if app.config['LOG_TO_STDOUT']:
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
app.logger.addHandler(stream_handler)
else:
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler(os.path.join(log_dir, 'flask_api.log'),maxBytes=1024 * 1024 * 50, backupCount=5, encoding='utf-8')
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(name)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
def register_plugins(app):
cors.init_app(app, supports_credentials=True)
db.init_app(app)
marshmallow.init_app(app)
migrate.init_app(app, db)
def register_blueprints(app):
from app.author import author_bp
app.register_blueprint(author_bp,url_prefix='/api/author')
from app.books import books_bp
app.register_blueprint(books_bp,url_prefix='/api/books')
def register_errors(app):
@app.after_request
def add_header(response):
return response
@app.errorhandler(404)
def not_found(e):
logging.error(e)
return response_with(resp.SERVER_ERROR_404)
@app.errorhandler(500)
def server_error(e):
logging.error(e)
return response_with(resp.SERVER_ERROR_500)
@app.errorhandler(400)
def bad_request(e):
logging.error(e)
return response_with(resp.BAD_REQUEST_400)
上述程序是这个 Flask 项目最重要的部分,这部分程序集成了日志、数据库迁移、跨域、blueprint,还有我们编写的 author、books 接口都注册在这里。后续如果有新用到的 Flask 扩展,新开发的接口,都需要来这里进行注册操作。完成这部分代码的编写,项目结构调整的事,就告一段落了。接下来的重点,是编写项目业务相关的接口。
重构作者信息接口
由于我们项目的结构的调整,之前写好的 author 接口,需要重构一下。首先,打开 app 目录,在该目录下,创建 author 包。继续在 author 包下面添加 models.py、routes.py、schema.py 文件。
代码语言:javascript复制$ (venv) cd app
$ (venv) mkdir author && cd author && touch __init__.py
$ (venv) touch models.py routes.py schema.py
models.py 文件中编写 author 模型程序,具体程序如下:
代码语言:javascript复制from app import db
class Author(db.Model):
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
first_name = db.Column(db.String(20))
last_name = db.Column(db.String(20))
created = db.Column(db.DateTime, server_default=db.func.now())
books = db.relationship('Book', backref='Author',cascade="all, delete-orphan")
def __init__(self,first_name,last_name,books=[]):
self.first_name = first_name
self.last_name = last_name
self.books = books
def create(self):
db.session.add(self)
db.session.commit()
return self
schema.py 文件中编写对象序列化程序,具体程序如下:
代码语言:javascript复制from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields
from app.author.models import Author
from app.books.schema import BookSchema
from app import db
class AuthorSchema(ModelSchema):
class Meta(ModelSchema.Meta):
model = Author
sqla_session = db.session
id = fields.Number(dump_only=True)
first_name = fields.String(required=True)
last_name = fields.String(required=True)
created = fields.String(dump_only=True)
books = fields.Nested(BookSchema, many=True, only=['title','year','id'])
routes.py 文件中编写接口处理接口请求相关程序,具体程序如下:
代码语言:javascript复制from flask import request
from app import db
from app.author import author_bp
from app.author.models import Author
from app.author.schema import AuthorSchema
from app.utils.responses import response_with
from app.utils import responses as resp
@author_bp.route('/',methods=['POST'])
def create_author():
try:
data = request.get_json()
print(data)
author_schema = AuthorSchema()
author = author_schema.load(data)
result = author_schema.dump(author.create())
return response_with(resp.SUCCESS_201,value={"author":result})
except Exception as e:
#print(e)
return response_with(resp.INVALID_INPUT_422)
@author_bp.route('/',methods=['GET'])
def get_author_list():
fetched = Author.query.all()
author_schema = AuthorSchema(many=True,only=['first_name','last_name','id'])
authors = author_schema.dump(fetched)
return response_with(resp.SUCCESS_200,value={"authors":authors})
@author_bp.route('/<int:author_id>',methods=['GET'])
def get_author_detail(author_id):
fetched = Author.query.get_or_404(author_id)
author_schema = AuthorSchema()
author = author_schema.dump(fetched)
return response_with(resp.SUCCESS_200,value={"author":author})
@author_bp.route('/<int:id>',methods=['PUT'])
def update_author_detail(id):
data = request.get_json()
get_author = Author.query.get_or_404(id)
get_author.first_name = data['first_name']
get_author_last_name = data['last_name']
db.session.add(get_author)
db.session.commit()
author_schema = AuthorSchema()
author = author_schema.dump(get_author)
return response_with(resp.SUCCESS_200,value={"author":author})
@author_bp.route('/<int:id>',methods=['PATCH'])
def modify_author_detail(id):
data = request.get_json()
get_author = Author.query.get(id)
if data.get('first_name'):
get_author.first_name = data['first_name']
if data.get('last_name'):
get_author.last_name = data['last_name']
db.session.add(get_author)
db.session.commit()
author_schema = AuthorSchema()
author = author_schema.dump(get_author)
return response_with(resp.SUCCESS_200,value={"author":author})
@author_bp.route('/<int:id>',methods=['DELETE'])
def delete_author(id):
get_author = Author.query.get_or_404(id)
db.session.delete(get_author)
db.session.commit()
return response_with(resp.SUCCESS_204)
最后,在init.py 文件中,编写如下程序:
代码语言:javascript复制from flask import Blueprint
author_bp = Blueprint('author_bp',__name__)
from app.author import routes
上述程序,使用到了 Flask 的 Blueprint 功能,这对于构建大型的 Flask 应用非常有用。
实现图书信息接口
books 接口程序与 author 接口实现过程是一样的,这里我直接给出已经完成的所有程序。
books 的 models 程序,完整程序如下:
代码语言:javascript复制from app import db
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer,primary_key=True,autoincrement=True)
title = db.Column(db.String(50))
year = db.Column(db.Integer)
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'))
def __init__(self,title,year,author_id=None):
self.title = title
self.year = year
self.author_id = author_id
def create(self):
db.session.add(self)
db.session.commit()
return self
books 的 schema 程序,完整程序如下:
代码语言:javascript复制from app.books.models import Book
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields
from app import db
class BookSchema(ModelSchema):
class Meta(ModelSchema.Meta):
model = Book
sqla_session = db.session
id = fields.Number(dump_only=True)
title = fields.String(required=True)
year = fields.Integer(required=True)
author_id = fields.Integer()
books 的 routes 程序,完整程序如下:
代码语言:javascript复制from flask import request
from app import db
from app.books import books_bp
from app.books.models import Book
from app.books.schema import BookSchema
from app.utils.responses import response_with
from app.utils import responses as resp
@books_bp.route('/',methods=['POST'])
def create_book():
try:
data = request.get_json()
book_schema = BookSchema()
book = book_schema.load(data)
result = book_schema.dump(book.create())
return response_with(resp.SUCCESS_201,value={"book":result})
except Exception as e:
print(e)
return response_with(resp.INVALID_INPUT_422)
@books_bp.route('/',methods=['GET'])
def get_book_list():
fetched = Book.query.all()
book_schema = BookSchema(many=True,only=['author_id','title','year'])
books = book_schema.dump(fetched)
return response_with(resp.SUCCESS_200,value={"books":books})
@books_bp.route('/<int:id>',methods=['GET'])
def get_book_detail(id):
fetched = Book.query.get_or_404(id)
book_schema = BookSchema()
books = book_schema.dump(fetched)
return response_with(resp.SUCCESS_200,value={"books":books})
@books_bp.route('/<int:id>', methods=['PUT'])
def update_book_detail(id):
data = request.get_json()
get_book = Book.query.get_or_404()
get_book.title = data['title']
get_book.year = data['year']
db.session.add(get_book)
db.session.commit()
book_schema = BookSchema()
book = book_schema.dump(get_book)
return response_with(resp.SUCCESS_200,value={"book":book})
@books_bp.route('/<int:id>', methods=['PATCH'])
def modify_book_detail(id):
data = request.get_or_404()
get_book = Book.query.get_or_404(id)
if data.get('title'):
get_book.title = data['title']
if data.get('year'):
get_book_year = data['year']
db.session.add(get_book)
db.session.commit()
book = book_schema.dump(get_book)
return response_with(resp.SUCCESS_200,value={"book":book})
@books_bp.route('/<int:id>',methods=['DELETE'])
def delete_books(id):
get_book = Book.query.get_or_404(id)
db.session.delete(get_book)
db.session.commit()
return response_with(resp.SUCCESS_204)
books 的 Blueprint 实现,该程序编写在init.py 文件中,完整程序如下:
代码语言:javascript复制from flask import Blueprint
books_bp = Blueprint('books_bp',__name__)
from app.books import routes
完成 books 相关代码编写后,我们需要进行数据迁移和运行程序,来测试我们的程序。
运行程序
回到 api 目录下,运行以下命令,完成数据的迁移操作。
代码语言:javascript复制$(venv) flask db init
$(venv) flask db migrate
最后,我们执行如下命令运行我们的 Flask 应用,在执行之前,需要修改.flaskenv 文件 FLASK_APP=main.py,之后再运行下面的命令。
代码语言:javascript复制$(venv) flask run
如果一切正常,你将会看到类似于下面的输出结果。
代码语言:javascript复制 * Serving Flask app "main.py" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL C to quit)
* Restarting with stat
[2020-06-10 21:04:32,742] INFO in __init__: Flask Rest Api startup
2020/06/10 21:04:32 flask_api __init__.py[37] create_app() INFO: Flask Rest Api startup
* Debugger is active!
* Debugger PIN: 267-778-190
[2020-06-10 21:04:33,433] INFO in __init__: Flask Rest Api startup
2020/06/10 21:04:33 flask_api __init__.py[37] create_app() INFO: Flask Rest Api startup
这时我们就可以开始测试我们的接口程序了。这次分享全部内容至此完。