3.蓝图和ORM

一. 应用、蓝图与视图函数

我们将初始化app, 视图函数等内容,通过蓝图的方法进行重构:

目录结构:

/app/init.py

from flask import Flask


def create_app():
    app = Flask(__name__)
    app.config.from_object('config')
    register_blueprint(app)   # 完成蓝图注册
    return app


def register_blueprint(app):   # 注册蓝图
    from app.web.book import web
    app.register_blueprint(web)

/app/web/book.py

from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, Blueprint  # 蓝图

web = Blueprint('web', __name__)    # 蓝图初始化


@web.route('/book/search/<q>/<page>')
def search(q, page):
    """

    :param q:
    :param page:
    :return:
    """
    isbn_or_key = is_isbn_or_key(q)
    if isbn_or_key == 'isbn':
        result = YuShuBook.search_by_isbn(q)
    else:
        result = YuShuBook.search_by_keyword(q)

    return jsonify(result)

/fisher.py

from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=app.config['DEBUG'], port=81)


二. 单蓝图多模块拆分视图函数

修改文件目录:

app/web/blueprint.py:

from flask import Blueprint  # 蓝图

web = Blueprint('web', __name__)  # 蓝图初始化

app/web/book.py:

from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify
from .blueprint import web


@web.route('/book/search/<q>/<page>')
def search(q, page):
    """

    :param q:
    :param page:
    :return:
    """
    isbn_or_key = is_isbn_or_key(q)
    if isbn_or_key == 'isbn':
        result = YuShuBook.search_by_isbn(q)
    else:
        result = YuShuBook.search_by_keyword(q)

    return jsonify(result)

app/web/user.py:

from .blueprint import web

@web.route('/user')
def user():
    pass

app/web/init.py:

from app.web import book, user

app/init.py:

from flask import Flask


def create_app():
    app = Flask(__name__)
    app.config.from_object('config')
    register_blueprint(app)   # 完成蓝图注册
    return app


def register_blueprint(app):   # 注册蓝图
    from app.web.blueprint import web
    app.register_blueprint(web)

三. request对象

我们search视图函数的url,不够规范,
我们需要从book/search/<q>/<page>改为/book/search?q=XXX&page=XXXX的形式。

我们借助flask的request,修改app/web/book.py实现url规范化:

from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web


@web.route('/book/search')
def search():
    q = request.args['q']
    page = request.args['page']
    isbn_or_key = is_isbn_or_key(q)
    if isbn_or_key == 'isbn':
        result = YuShuBook.search_by_isbn(q)
    else:
        result = YuShuBook.search_by_keyword(q)

    return jsonify(result)

补充说明: flask的request 必须在视图函数中才会有效, 即必须在触发http的情况下有效。


四. WTForms参数验证

在app/web/book.py中的视图函数search中, 
我们希望能够对参数p和page 进行校验, 最的办法就是借助wtforms.

新增文件app/forms/book.py:

from wtforms import Form, StringField, IntegerField
from wtforms.validators import Length, NumberRange


class SearchForm(Form):
    q = StringField(validators=[Length(min=1, max=30)])
    page = IntegerField(validators=[NumberRange(min=1, max=99)], default=1)

修改app/web/book.py:

from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
from app.forms.book import SearchForm


@web.route('/book/search')
def search():
    # q = request.args['q']   # 至少要有一个字符串
    # page = request.args['page']   # 至少是正整数, 且有最大值限制
    form = SearchForm(request.args)
    if form.validate():
        q = form.q.data.strip()
        page = form.page.data.strip()  
        # 参数最好从form中取, 如果从request.args来取的化, 没有默认值了
        isbn_or_key = is_isbn_or_key(q)
        if isbn_or_key == 'isbn':
            result = YuShuBook.search_by_isbn(q)
        else:
            result = YuShuBook.search_by_keyword(q)

        return jsonify(result)
    else:
        return jsonify({'msg': '参数校验失败'})

优化报错

通过form.errors我们可以优化参数校验的报错
修改app/web/book.py:

from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
from app.forms.book import SearchForm


@web.route('/book/search')
def search():
    form = SearchForm(request.args)
    if form.validate():
        q = form.q.data.strip()
        page = form.page.data.strip()  # 最好从form中取, 如果从request.args来取的化, 没有默认值了
        isbn_or_key = is_isbn_or_key(q)
        if isbn_or_key == 'isbn':
            result = YuShuBook.search_by_isbn(q)
        else:
            result = YuShuBook.search_by_keyword(q)

        return jsonify(result)
    else:
        return jsonify(form.errors) # 会报默认的form的错误

如果想自定义`form.errors`的报错,
我们需要在`app/forms/book.py`的`SearchForm`的每个元素的校验器validators中添加`message='报错信息'`

bug: 如果url为/book/search?q= &page=1,我们发现 if form.validate():为True。 而我们对于空格,想要的结果是不计算length或length=0.我们需要添加验证器Datarequired()

修改app/forms/book.py:

from wtforms import Form, StringField, IntegerField
from wtforms.validators import Length, NumberRange, DataRequired


class SearchForm(Form):
    q = StringField(validators=[DataRequired(), Length(min=1, max=30)])
    page = IntegerField(validators=[NumberRange(min=1, max=99)], default=1)

五. 拆分配置文件

在search视图函数中, 我们的page参数一直没有使用, 我们可以修改代码:

修改app/web/book.py:

from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
from app.forms.book import SearchForm


@web.route('/book/search')
def search():
    form = SearchForm(request.args)
    if form.validate():
        q = form.q.data.strip()
        page = form.page.data.strip()  # 最好从form中取, 如果从request.args来取的化, 没有默认值了
        isbn_or_key = is_isbn_or_key(q)
        if isbn_or_key == 'isbn':
            result = YuShuBook.search_by_isbn(q)
        else:
            result = YuShuBook.search_by_keyword(q, page) 
            # 修改了传入参数    countstart改为page

        return jsonify(result)
    else:
        return jsonify(form.errors)

修改/yushu_book.py:

from httper import HTTP


class YuShuBook:
    per_page = 15   # 每页个数
    isbn_url = 'http://t.yushu.im/v2/book/isbn/{}'
    keyword_url = 'http://t.yushu.im/v2/book/search?q={}&count={}&start={}'

    @classmethod
    def search_by_isbn(cls, isbn):
        url = cls.isbn_url.format(isbn)
        result = HTTP.get(url)
        return result

    @classmethod
    def search_by_keyword(cls, keyword, page=1):  # countstart改为page

        url = cls.keyword_url.format(keyword, cls.per_page, cls.per_page*(page-1))
        result = HTTP.get(url)
        return result

代码优化方向:per_page放入配置文件中,方便修改。url = cls.keyword_url.format(keyword, cls.per_page, cls.per_page*(page-1))中cls.per_page*(page-1)用函数单独封装, 使代码更好阅读。

拆分配置文件

我们将/config.py删除, 在app下新建secure.py和setting两个配置文件

secure.py   存放开发环境和生产环境不一样的,较为私密的参数。 如DEBUG=True
setting.py  存放开发环境和生产环境通用的参数。             如PER_PAGE=15

secure.py:

DEBUG = False

setting.py:

per_page = 15

修改/yushu_book.py:

from httper import HTTP
from flask import current_app  # 导入核心对象app


class YuShuBook:
    isbn_url = 'http://t.yushu.im/v2/book/isbn/{}'
    keyword_url = 'http://t.yushu.im/v2/book/search?q={}&count={}&start={}'

    @classmethod
    def search_by_isbn(cls, isbn):
        url = cls.isbn_url.format(isbn)
        result = HTTP.get(url)
        return result

    @classmethod
    def search_by_keyword(cls, keyword, page=1):
        url = cls.keyword_url.format(keyword, current_app.config['PER_PAGE'], cls.calculate_start(page))
        result = HTTP.get(url)
        return result

    @staticmethod
    def calculate_start(page):
        return (page - 1) * current_app.config['PER_PAGE']

修改app/init.py:

from flask import Flask


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)   # 完成蓝图注册
    return app


def register_blueprint(app):   # 注册蓝图
    from app.web.blueprint import web
    app.register_blueprint(web)

六. 数据库模型

app下新建models/book.py:

from sqlalchemy import Column, Integer, String


class Book():
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(30), default='未名')
    bingding = Column(String(20))    # 精装还是平装
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdata = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(String(1000))
    image = Column(String(50))    

    def sample(self):
        pass


七. 将模型映射到数据库中

在app/secure.py配置文件中添加mysql的uri:

DEBUG = True
SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://root:@localhost:3306/fisher'

更新models/book.py:

from sqlalchemy import Column, Integer, String
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


class Book(db.Model):
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(30), default='未名')
    bingding = Column(String(20))    # 精装还是平装
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdata = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(String(1000))
    image = Column(String(50))

    def sample(self):
        pass

更新app/init.py

from flask import Flask
from app.models.book import db


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)   

    db.init_app(app)    # 注册数据库
    db.create_all()     # 让模型进行映射
    return app


def register_blueprint(app):   
    from app.web.blueprint import web
    app.register_blueprint(web)

运行后报错:

'No application found. Either work inside a view function or push'
RuntimeError: No application found. Either work inside a view function or push an application context. See http://flask-sqlalchemy.pocoo.org/contexts/.

解决上面报错的方法1:

修改app/init.py:

from flask import Flask
from app.models.book import db


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)   

    db.init_app(app)    
    db.create_all(app=app)     # create_all传入app实例
    return app


def register_blueprint(app):   
    from app.web.blueprint import web
    app.register_blueprint(web)

解决上面报错的方法2:

修改app/init.py:

from flask import Flask
from app.models.book import db


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)   

    db.init_app(app)   
    with app.app_context():
        db.create_all()  
    return app


def register_blueprint(app):   
    from app.web.blueprint import web
    app.register_blueprint(web)

解决上面报错的方法3:

修改models/book.py一个地方: db = SQLAlchemy() 改为

from fisher import app 
db = SQLAlchemy(app)

这样容易导致循环引用,不推荐。

我们为什么能够用这三种方法解决异常呢?

查看db.create_all的源码:

  def create_all(self, bind='__all__', app=None):
        """Creates all tables.

        .. versionchanged:: 0.12
           Parameters were added
        """
        self._execute_for_all_tables(app, bind, 'create_all')

查看_execute_for_all_tables:

    def _execute_for_all_tables(self, app, bind, operation, skip_tables=False):
        app = self.get_app(app)

       ···

查看get_app:

   def get_app(self, reference_app=None):
        """Helper method that implements the logic to look up an
        application."""

        if reference_app is not None:   # 对应方法1
            return reference_app

        if current_app:         # 对应方法2 如果current_appunbound,我们手动push
            return current_app._get_current_object()

        if self.app is not None:     # 对应方法3 dbself.app赋值
            return self.app

        raise RuntimeError(
            'No application found. Either work inside a view function or push'
            ' an application context. See'
            ' http://flask-sqlalchemy.pocoo.org/contexts/.'
        )


猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80714338
3.