澳门新萄京官方网站-www.8455.com-澳门新萄京赌场网址

数据与路由,Flask大型教程

2019-08-31 作者:www.8455.com   |   浏览(116)

数据

图书数据库的地址

# 基地址
http://t.yushu.im
# 关键字搜索
http://t.yushu.im/v2/book/search?q={}&start={}&count={}
# isbn搜索
http://t.yushu.im/v2/book/search/isbn/{isbn}
# 豆瓣api
https://api.douban.com/v2/book/1003078

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

  1. 结构,如图:

    图片 1

  2. Flask最上层是app核心对象 ,在这个核心对象上可以插入很多蓝图,这个蓝图是不能单独存在的,必须将app作为插板插入app ,在每一个蓝图上,可以注册很多静态文件,视图函数,模板 ,一个业务模块可以做为一个蓝图,比如book,之前的book.py 放到了app/web/路径下,就是考虑到了蓝图,app属于是整个Flask应用层,web属于是蓝图

  3. 一些初始化操作应该放入到__init__文件中,比如Flask的核心应用app初始化对象,应该放入到在应用层级app包的 __init__.py 中 ,而蓝图的初始化应该放入到蓝图层的web包__init__.py中,如图:

    图片 2

  4. Flask的核心应用app初始化对象文件app/__init__.py

# -*- coding: utf-8 -*-
from flask import Flask

def create_app():
    app = Flask(__name__)
    app.config.from_object('config')
    # 要返回回去
    return app
  1. 此时在主文件中
# -*- coding: utf-8 -*-
from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(debug=app.config['DEBUG'])

本文翻译自The Flask Mega-Tutorial Part XV: A Better Application Structure

flask是python的一个web应用框架,django很多人听过,flask比较少见,连创始人一开始写出来只是个笑话而已,对python3支持不太好,flask的好处是微应用,有需求的插件才使用,精简而灵活,由于灵活,要怎么创建一个项目要自己去思考怎么搭建,不像django帮你做完项目搭建的步骤。个人觉得怎么搭建一个项目也是一个学习后台架构的过程。这里不会详细介绍flask和flask各个模块的功能,留作思考与学习。

使用Python的Flask框架构建大型Web应用程序的结构示例,pythonflask

虽然小型web应用程序用单个脚本可以很方便,但这种方法却不能很好地扩展。随着应用变得复杂,在单个大的源文件中处理会变得问题重重。

与大多数其他web框架不同,Flask对大型项目没有特定的组织方式;应用程序的结构完全交给开发人员自己决定。在这一章,提出一个可能的方式来组织管理一个大型应用程序的包和模块。这种结构将用于书中其余的示例中。

1、项目结构

示例 基本多文件Flask应用结构

|-flasky
 |-app/
  |-templates/
  |-static/
  |-main/
   |-__init__.py
   |-errors.py
   |-forms.py
   |-views.py
  |-__init__.py
  |-email.py
  |-models.py
 |-migrations/
 |-tests/
  |-__init__.py
  |-test*.py
 |-venv/
 |-requirements.txt
 |-config.py
 |-manage.py

这个结构有四个顶层目录:

  • Flask应用一般放置在名为app的目录下。
  • migrations目录包含数据库迁移脚本,这和之前说的一样。
  • 单元测试放置在test目录下
  • venv目录包含Python虚拟环境,这和之前说的也是一样的。

还有一些新的文件:

  • requirements.txt列出一些依赖包,这样就可以很容易的在不同的计算机上部署一个相同的虚拟环境。
  • config.py存储了一些配置设置。
  • manage.py用于启动应用程序和其他应用程序任务。

为了帮助你完全理解这个结构,下面会描述将hello.py应用改为符合这一结构的整个流程。

2、配置选项 应用程序通常需要几个配置设置。最好的例子就是在开发过程中需要使用不同的数据库,测试,生产环境,这样他们可以做到互不干扰。

我们可以使用配置类的层次结构来代替hello.py中的简单类字典结构配置。下面展示了config.py文件。

config.py:应用程序配置

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
  SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' 
  SQLALCHEMY_COMMIT_ON_TEARDOWN = True
  FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
  FLASKY_MAIL_SENDER = 'Flasky Admin <[email protected]>' 
  FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')

  @staticmethod
  def init_app(app): 
    pass

class DevelopmentConfig(Config): 
  DEBUG = True

  MAIL_SERVER = 'smtp.googlemail.com'
  MAIL_PORT = 587
  MAIL_USE_TLS = True
  MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
  MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') 
  SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 
    'sqlite:///'   os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config): 
  TESTING = True
  SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 
    'sqlite:///'   os.path.join(basedir, 'data-test.sqlite')

class ProductionConfig(Config):
  SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 
    'sqlite:///'   os.path.join(basedir, 'data.sqlite')

config = {
  'development': DevelopmentConfig,
  'testing': TestingConfig,
  'production': ProductionConfig,
  'default': DevelopmentConfig
}

Config基类包含一些相同配置;不同的子类定义不同的配置。额外配置可以在需要的时候在加入。

为了让配置更灵活更安全,一些设置可以从环境变量中导入。例如,SECRET_KEY,由于它的敏感性,可以在环境中设置,但如果环境中没有定义就必须提供一个默认值。

在三个配置中SQLALCHEMY_DATABASE_URI变量可以分配不同的值。这样应用程序可以在不同的配置下运行,每个可以使用不同的数据库。

配置类可以定义一个将应用程序实例作为参数的init_app()静态方法。这里特定于配置的初始化是可以执行的。这里Config基类实现一个空init_app()方法。

在配置脚本的底部,这些不同的配置是注册在配置字典中。将其中一个配置(开发配置)注册为默认配置。

3、应用程序包 应用程序包放置了所有应用程序代码、模板和静态文件。它被简单的称为app,也可以给定一个特定于应用的名称(如果需要的话)。templates和static目录是应用的一部分,因此这两个目录应该放置在app中。数据库模型和电子邮件支持功能也要置入到这个包中,每个都以app/models.py和app/email.py形式存入自己的模块当中。

3.1、使用一个应用程序工厂

在单个文件中创建应用程序的方式非常方便,但是它有一个大缺点。因为应用程序创建在全局范围,没有办法动态的适应应用配置的更改:脚本运行时,应用程序实例已经创建,所以它已经来不及更改配置。对于单元测试这是特别重要的,因为有时需要在不同的配置下运行应用程序来获得更好的测试覆盖率。

解决这一问题的方法就是将应用程序放入一个工厂函数中来延迟创建,这样就可以从脚本中显式的调用。

这不仅给脚本充足的时间来设置配置,也能用于创建多个应用程序实例——一些在测试过程中非常有用的东西。被定义在app包的构造函数中的应用程序工厂函数会在示例7-3中展示。

这个构造函数导入大部分当前需要使用的扩展,但因为没有应用程序实例初始化它们,它可以被创建但不初始化通过不传递参数给它们的构造函数。create_app()即应用程序工厂函数,需要传入用于应用程序的配置名。配置中的设置被保存在config.py中的一个类中,可以使用Flask的app.config配置对象的from_object()方法来直接导入。配置对象可以通过对象名从config字典中选出。一旦应用程序被创建且配置好,扩展就可以被初始化。调用扩展里的init_app()之前先创建并完成初始化工作。

app/ _init__.py:应用程序包构造函数_

from flask import Flask, render_template 
from flask.ext.bootstrap import Bootstrap 
from flask.ext.mail import Mail
from flask.ext.moment import Moment
from flask.ext.sqlalchemy import SQLAlchemy 
from config import config

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()

def create_app(config_name):
  app = Flask(__name__) 
  app.config.from_object(config[config_name]) 
  config[config_name].init_app(app)

  bootstrap.init_app(app)
  mail.init_app(app)
  moment.init_app(app)
  db.init_app(app)

  # attach routes and custom error pages here

  return app

工厂函数返回创建的应用程序实例,但是请注意,在当前状态下使用工厂函数创建的应用程序是不完整的,因为它们没有路由和自定义错误页面处理程序。这是下一节的主题。

3.2、在蓝图中实现应用程序的功能

应用程序工厂的转化工作引出了路由的复杂化。在单脚本应用中,应用程序实例是全局的,所以可以很容易地使用app.route装饰器定义路由。但是现在应用程序在运行时创建,app.route装饰器只有在create_app()调用后才开始存在,这就太迟了。就像路由那样,这些通过app.errorhandler装饰器定义的自定义错误页面处理程序也存在同样的问题。

幸运的是Flask使用蓝图来提供一个更好的解决方案。一个蓝图就类似于一个可以定义路由的应用程序。不同的是,和路由相关联的蓝图都在休眠状态,只有当蓝图在应用中被注册后,此时的路由才会成为它的一部分。使用定义在全局作用域下的蓝图,定义应用程序的路由就几乎可以和单脚本应用程序一样简单了。

和应用程序一样,蓝图可以定义在一个文件或一个包中与多个模块一起创建更结构化的方式。为了追求最大的灵活性,可以在应用程序包中创建子包来持有蓝图。下面展示了创建蓝图的构造函数。

app/main/ _init__.py:创建蓝图_

from flask import Blueprint

main = Blueprint('main', __name__) 

from . import views, errors

蓝图是通过实例化Blueprint类对象来创建的。这个类的构造函数接收两个参数:蓝图名和蓝图所在的模块或包的位置。与应用程序一样,在大多数情况下,对于第二个参数值使用Python的__name__变量是正确的。

应用程序的路由都保存在app/main/views.py模块内部,而错误处理程序则保存在app/main/errors.py中。导入这些模块可以使路由、错误处理与蓝图相关联。重要的是要注意,在app/init.py脚本的底部导入模块要避免循环依赖,因为view.py和errors.py都需要导入main蓝图。

蓝图和应用程序一样注册在create_app()工厂函数中,如下所示。

示例 app/ _init__.py:蓝图注册_

def create_app(config_name): 
  # ...
  from .main import main as main_blueprint 
  app.register_blueprint(main_blueprint)

  return app

下面则展示了错误处理。

app/main/errors.py:蓝图的错误处理

from flask import render_template 
from . import main

@main.app_errorhandler(404) 
def page_not_found(e):
  return render_template('404.html'), 404

@main.app_errorhandler(500) 
def internal_server_error(e):
  return render_template('500.html'), 500

在蓝图中写错误处理的不同之处是,如果使用了errorhandler装饰器,则只会调用在蓝图中引起的错误处理。而应用程序范围内的错误处理则必须使用app_errorhandler。

这里展示了被更新在蓝图中的应用程序路由。

app/main/views.py:带有蓝图的应用程序路由

from datetime import datetime
from flask import render_template, session, redirect, url_for

from . import main
from .forms import NameForm 
from .. import db
from ..models import User

@main.route('/', methods=['GET', 'POST']) 
def index():
  form = NameForm()
  if form.validate_on_submit():
    # ...
    return redirect(url_for('.index')) 
  return render_template('index.html',
              form=form, name=session.get('name'),
              known=session.get('known', False),
              current_time=datetime.utcnow())

在蓝图中写视图函数有两大不同点。第一,正如之前的错误处理一样,路由装饰器来自于蓝图。第二个不同是url_for()函数的使用。你可能会回想,该函数的第一个参数为路由节点名,它给基于应用程序的路由指定默认视图函数。例如,单脚本应用程序中的index()视图函数的URL可以通过url_for('index')来获得。

不同的是Flask名称空间适用于来自蓝图的所有节点,这样多个蓝图可以使用相同节点定义视图函数而不会产生冲突。名称空间就是蓝图名(Blueprint构造函数中的第一个参数),所以index()视图函数注册为main.index且它的URL可以通过url_for('main.index')获得。

在蓝图中,url_for()函数同样支持更短格式的节点,省略蓝图名,例如url_for('.index')。有了这个,就可以这样使用当前请求的蓝图了。这实际意味着相同蓝图内的重定向可以使用更短的形式,如果重定向跨蓝图则必须使用带名称空间的节点名。

完成了应用程序页面更改,表单对象也保存在app/main/forms.py模块中的蓝图里面。

4、启动脚本 顶层目录中的manage.py文件用于启动应用。

manage.py:启动脚本

#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Role
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default') 
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
  return dict(app=app, db=db, User=User, Role=Role)

manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__': 
  manager.run()

这个脚本开始于创建应用程序。使用环境变量FLASK_CONFIG,若它已经定义了则从中获取配置;如果没有,则是用默认配置。然后用于Python shell的Flask-Script、Flask-Migrate以及自定义上下文会被初始化。

为了方便,会增加一行执行环境,这样在基于Unix的操作系统上可以通过./manage.py来执行脚本来替代冗长的python manage.py。

5、需求文件 应用程序必须包含requirements.txt文件来记录所有依赖包,包括精确的版本号。这很重要,因为可以在不同的机器上重新生成虚拟环境,例如在生产环境的机器上部署应用程序。这个文件可以通过下面的pip命令自动生成:

(venv) $ pip freeze >requirements.txt

当安装或更新一个包之后最好再更新一下这个文件。以下展示了一个需求文件示例:

Flask==0.10.1
Flask-Bootstrap==3.0.3.1
Flask-Mail==0.9.0
Flask-Migrate==1.1.0
Flask-Moment==0.2.0
Flask-SQLAlchemy==1.0
Flask-Script==0.6.6
Flask-WTF==0.9.4
Jinja2==2.7.1
Mako==0.9.1
MarkupSafe==0.18
SQLAlchemy==0.8.4
WTForms==1.0.5
Werkzeug==0.9.4
alembic==0.6.2
blinker==1.3
itsdangerous==0.23

当你需要完美复制一个虚拟环境的时候,你可以运行以下命令创建一个新的虚拟环境:

(venv) $ pip install -r requirements.txt

当你读到这时,示例requirements.txt文件中的版本号可能已经过时了。如果喜欢你可以尝试用最近发布的包。如果遇到任何问题,你可以随时回退到需求文件中与应用兼容的指定版本。

6、单元测试 这个应用非常小以至于不需要太多的测试,但是作为示例会在示例中展示两个简单的测试定义。

示例:tests/test_basics.py:单元测试

import unittest
from flask import current_app 
from app import create_app, db

class BasicsTestCase(unittest.TestCase): 
  def setUp(self):
    self.app = create_app('testing')
    self.app_context = self.app.app_context()
    self.app_context.push()
    db.create_all()

  def tearDown(self): 
    db.session.remove() 
    db.drop_all() 
    self.app_context.pop()

  def test_app_exists(self): 
    self.assertFalse(current_app is None)

  def test_app_is_testing(self): 
    self.assertTrue(current_app.config['TESTING'])

编写好的测试使用的是来自于Python标准库中标准的unittest包。setUp()和tearDown()方法在每个测试之前和之后运行,且任何一个方法必须以test_开头作为测试来执行。

建议:如果你想要学习更多使用Python的unittest包来写单元测试的内容,请参阅官方文档。
setUp()方法尝试创建一个测试环境,类似于运行应用程序。首先它创建应用程序配置用于测试并激活上下文。这一步确保测试可以和常规请求一样访问current_app。然后,当需要的时候,可以创建一个供测试使用的全新数据库。数据库和应用程序上下文会在tearDown()方法中被移除。

第一个测试确保应用程序实例存在。第二个测试确保应用程序在测试配置下运行。为了确保tests目录有效,需要在tests目录下增加__init__.py文件,不过该文件可以为空,这样unittest包可以扫描所有模块并定位测试。

建议:如果你有克隆在GitHub上的应用程序,你现在可以运行git checkout 7a来切换到这个版本的应用程序。为了确保你已经安装了所有依赖集,需要运行pip install -r requirements.txt。
为了运行单元测试,可以在manage.py脚本中增加一个自定义的命令。

下面展示如何添加测试命令。

示例:manage.pyt:单元测试启动脚本

@manager.command
def test():
  """Run the unit tests."""
  import unittest
  tests = unittest.TestLoader().discover('tests') 
  unittest.TextTestRunner(verbosity=2).run(tests)

manager.command装饰器使得它可以很容易的实现自定义命令。被装饰的函数名可以被当做命令名使用,且函数的文档字符串会显示帮助信息。test()函数的执行会调用unittest包中的测试运行器。

单元测试可以像下面这样执行:

(venv) $ python manage.py test

test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok

.----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

7、数据库启动 与单脚本的应用相比,重构后的应用使用不同数据库。

从环境变量中获取的数据库URL作为首选,默认SQLite数据库作为可选。三个配置中的环境变量和SQLite数据库文件名是不一样的。例如,开发配置的URL是从DEV_DATABASE_URL环境变量中获取,如果没有定义则会使用名为data-dev.sqlite的SQLite数据库。

无论数据库URL源的是哪一个,都必须为新的数据库创建数据库表。如果使用了Flask-Migrate来保持迁移跟踪,数据库表可以被创建或更新到最近的版本通过下面的命令:

(venv) $ python manage.py db upgrade

相信与否,已经到了第一部分结束的地方。你现在已经学到了Flask必要的基本要素,但是你不确定如何将这些零散的知识组合在一起形成一个真正的应用程序。第二部分的目的是通过开发一个完整的应用程序来带领你继续前行。

虽然小型web应用程序用单个脚本可以很方便,但这种方法却不能很好地扩展...

搜索关键字

  1. 根据上面的地址可以知道搜索的时候有两种方式,而对于isbn搜索,又分为两种isbn13 由13个0-9在数字组成,isbn10 由10表0-9表数字组组成,中间可能包含' - ' ,所以要分开来判断
  2. 在函数中要注意:isdigit()可以判断是否为数字 ,replace()用来替换,
@app.route("/search/<q>/<page>")
def search(q,page):
    """
    搜索书籍路由
    :param q: 关键字 OR isbn
    :param page: 页码
    """
    isbn_or_key = 'key'
    # 1. 判断长度是否为13且是否为数字
    if len(q) == 13 and q.isdigit():
        isbn_or_key = 'isbn'
    # 2. 把-替换掉,判断是否为纯数字
    short_q = q.replace('-', '')
    if '-' in q and len(short_q) == 10 and short_q.isdigit():
        isbn_or_key = 'isbn'
    pass
  1. 多逻辑判断的时候,应该把结果看着为假的放到前面,对数据库操作的放到后面,这样有利于节约资源

二、用蓝图注册视图函数

  1. 在蓝图中注册试图函数,在app/web/book.py中,记得导入Blueprint
# -*- coding: utf-8 -*-
from flask import jsonify, Blueprint
from helper import is_isbn_key
from ShanqiuBook import ShanqiuBook

# 蓝图 blueprint,进行初始化,蓝图的名字和参数为蓝图所在的模块名一般用__name__
web = Blueprint ('web',__name__)

# 此时这里用的就是web了
@web.route('/book/search/<q>/<page>')
def hello(q,page):
    is_or_key = is_isbn_key(q)
    if is_or_key == 'isbn':
        result = ShanqiuBook.search_by_isbn(q)
    else:
        result = ShanqiuBook.search_by_keyword(q)

    return jsonify(result)
  1. 在蓝图中注册了试图函数,还需要把蓝图插入到app中,app/__init__.py
# -*- coding: utf-8 -*-
from flask import Flask

def create_app():
    app = Flask(__name__)
    app.config.from_object('config')
    # 调用一下就可以
    register_blueprint(app)
    return app

# 通过这个方法插入到app中
def register_blueprint(app):
    from app.web.book import web
    # 注册这个蓝图对象
    app.register_blueprint(web)

这是Flask Mega-Tutorial系列的第十五部分,我将使用适用于大型应用的风格重构本应用。

|-flasky
  |-apps/
    |-templates/
    |-static/
    |-main/
      |-__init__.py
      |-errors.py
      |-forms.py
      |-views.py
    |-__init__.py
    |-email.py
    |-models.py
  |-migrations/
  |-tests/
    |-__init__.py
    |-test*.py
  |-venv/
  |-requirements.txt
  |-config.py
  |-manage.py
  |-command.py

简单的重构

  1. 上面的代码都写到视图中这样不妥,体现不了封装性,看起来不好,应该把一个实现的功能封装起来,建立一个函数,方便日后的管理
  2. 在目录下建立一个helper.py文件,这个文件主要就是提供一些方法,把上面的内容放到这里,只需要返回一个值就可以了
# -*- coding: utf-8 -*-

def is_isbn_or_key(word):
    isbn_or_key = 'key'
    if len(word) == 13 and word.isdigit():
        isbn_or_key = 'isbn'

    short_word = word.replace('-', '')
    if '-' in word and len(short_word) == 10 and short_word.isdigit():
        isbn_or_key = 'isbn'

    return isbn_or_key
  1. 在主文件中调用这个方法就可以了,记得传值,和接收返回的值
# -*- coding: utf-8 -*-

from flask import Flask,make_response
# 1. 这里要导入
from helper import is_isbn_or_key

app = Flask(__name__)
app.config.from_object('config')

@app.route('/book/search/<q>/<page>')
def search(q,page):
    # 2. 调用方法即可
    is_or_key = is_isbn_or_key(q)
    pass

if __name__ == '__main__':
    app.rundebug=app.config['DEBUG'])

三、单蓝图多模块拆分视图函数

  1. 蓝图,就是为了分模块的,比如一个web系统就是属于一个web模块,一个移动端使用的api就是一个api模块,而我们这里的book,user等不同类别的py文件,要是每一个都注册一个蓝图的话就有点小题大作了,所以要进行单蓝图
  2. 在一个模块(web)的初始文件中定义蓝图对象,然后这个模块中的其他的py文件引用的就是这一个蓝图对象来注册路由函数,
  3. 在app/web/book.py文件中
# -*- coding: utf-8 -*-
from flask import jsonify, Blueprint
from helper import is_isbn_key
from ShanqiuBook import ShanqiuBook
# 导入web模块
from . import web

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

    # 调用方法判断用户是根据什么查的
    is_or_key = is_isbn_key(q)
    if is_or_key == 'isbn':
        result = ShanqiuBook.search_by_isbn(q)
    else:
        result = ShanqiuBook.search_by_keyword(q)

    return jsonify(result)
  1. 这里先建立一个伪代码user.py,为了多一个模块进行演示
# -*- coding: utf-8 -*-
# 导入web模块
from . import web
@web.route("/user/login")
def login():
    return "success"
  1. 此时在app/web/__init__.py文件中,定义这个蓝图对象
# -*- coding: utf-8 -*-

# 蓝图 blueprint,进行初始化
from flask import Blueprint
web = Blueprint ('web',__name__)

# 这两个导入之后就可以成功的运行对应模块中相关的代码,注意这个位置,这蓝图实例化之后
from app.web import book
from app.web import user

Microblog已经是一个初具规模的应用了,所以我认为这是讨论Flask应用如何在持续增长中不会变得混乱和难以管理的好时机。 Flask是一个框架,旨在让你选择以任何方式来组织项目,基于该理念,在应用日益庞大或者技能水平变化的时候,才有可能更改和调整其结构。

项目有八个顶层目录:

requests请求

  1. 因为这个项目要访问不同的网址,所以在目录下新建一个http.py文件,专门用来提供访问网址
  2. 这里使用的requests,要先进行安装,注意:代码写的时候一定要简洁,千万不要使用python的关键字,以免与Python的模块冲突并导致此错误,把这个类名http改为别的名称
# -*- coding: utf-8 -*-

import requests
class aaa:

    # 传入url和是否返回的是json数据,这里是静态方法
    @staticmethod
    def get(url,return_json=True):
        # 发送get请求
        r = requests.get(url)
        # 因为有的url返回的json数据,但是有的并不是,所以加一个判断,不是的话返回文本
        # 还要判断状态码,200的话就是访问成功有数据
        if r.status_code != 200:
            return {} if return_json else ''
        return r.json() if return_json else r.text

        # 下面的写法太low
        # if r.status_code == 200:
        #     if return_json:
        #         return r.json()
        #     else:
        #         return r.text
        # else:
        #     if return_json:
        #         return {}
        #     else:
        #         return ''

四、Request对象

  1. 在app/web/book.py文件中,定义的url请求是/book/search/<q>/<page>这种格式的,Flask会将<>里的值自动映射成视图函数方法的参数,但是这种格式用着不爽,要把用户输入的参数作为请求参数传入,这个时候就要使用这种格式了http://127.0.0.1:5000/book/search/?q=金庸&page=1
  2. 这个该怎么获取值呢,这个时候就用到Flask内置的Request了,通过request对象就可以获取HTTP请求中包含的详细信息了,具体的用法看下面的代码
# -*- coding: utf-8 -*-

# 导入这个request模块,
from flask import jsonify, Blueprint,request
from helper import is_isbn_key
from ShanqiuBook import ShanqiuBook
from . import web

# http://127.0.0.1:5000/book/search/?q=金庸&page=1
@web.route('/book/search/')
def hello():
    # 通过Request对象拿到对应值的信息,但是这个并不是py中原始的字典,而是dict的子类immutableDict
    q = request.args['q']
    page = request.args['page']
    # ip = request.remote_addr

    # 通过这个方法把它转换为普通的dict
    # a = request.args.to_dict()
    # print(a)

    is_or_key = is_isbn_key(q)
    if is_or_key == 'isbn':
        result = ShanqiuBook.search_by_isbn(q)
    else:
        result = ShanqiuBook.search_by_keyword(q)

    return jsonify(result)
  1. Flask的request是基于代理模式实现的,想让request正常使用,必须确保是http请求触发的函数或视图函数中使用

在本章中,我将讨论适用于大型应用的一些模式,并且为了演示他们,我将对Microblog项目的结构进行一些更改,目标是使代码更易于维护和组织。 当然,在真正的Flask精神中,我鼓励你在尝试决定组织自己的项目的方式时仅仅将这些更改作为参考。

  • app的目录下是放Flask应用
  • migrations目录包含数据库迁移脚本
  • test目录下是放单元测试
  • venv是Python虚拟环境
  • requirements.txt是Python运行的依赖包列表
  • config.py是配置设置脚本
  • manage.py 用于启动应用程序和其他应用程序任务
  • command.py 控制

从API中获取数据

  1. 首先在目录下定义一个类,用于用于获取数据,ShanqiuBook,
# -*- coding: utf-8 -*-

from http import aaa
class ShanqiuBook:

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


    # 根据isbn进行搜索,这里使用这个静态装饰器,调用类变量更加的方便
    @classmethod
    def search_by_isbn(cls,isbn):
        # 调用类变量,
        url = cls.isbn_url.format(isbn)
        # 调用上面的方法用于请求网址
        result = aaa.get(url)
        # 这里返回的是json数据,但是在py中就是字典了
        return result

    # 根据关键字进行搜索
    @classmethod
    def search_by_keyword(cls,keyword,count=15,start=0):
        url = cls.keyword_url.format(keyword,count,start)
        result = aaa.get(url)
        return result
  1. 然后在视图中获取返回的数据
# -*- coding: utf-8 -*-

from flask import Flask
from helper import is_isbn_or_key

from flask import jsonify
# 实例化
from shanqiu_book import ShanQiuBook

app = Flask(__name__)
# 载入这个配置文件
app.config.from_object('config')

@app.route('/book/search/<q>/<page>')
def search(q,page):
     is_or_key = is_isbn_or_key(q)
     if is_or_key == 'isbn':
         # 这里直接使用使用类名调用就可以
         result = ShanQiuBook.search_by_isbn(q)
    else:
         result = ShanQiuBook.search_by_keyword(q)

    # 因为返回的是json数据,要手动的进行解析,这样写的话非常麻烦
    # return json.dumps(result), 200, {'content-type': 'application/json'}
    # 这里使用flask自带的jsonify替换麻烦的json.dumps和元组
     return jsonify(result)


if __name__ == '__main__':
    app.run(debug=app.config['DEBUG'])

五、WTForms参数验证

  1. 上面我们把url改了,但是如果用户输入了一些特殊的符号该怎么办?这个时候就要使用到参数验证,而WTForms框架就是一个优秀的参数验证框架,首先在对应的环境中进行安装(flask--yQglGu4) E:pyqiyueflask>pipenv install wtforms
  2. 这个参数验证写在哪里好呢,直接写在book.py中,这样是最不妥的,为了方便调用,应该写成一个类,所以写在app/forms/book.py文件中
# -*- coding: utf-8 -*-

# 导入需要使用的模块
from wtforms import Form,StringField,IntegerField
from wtforms.validators import Length,NumberRange

class SearchForm(Form):
    # 直接调用内置对象
    # 参数校验规则:
    # 1.定义的属性名q,page要与要校验的参数同名
    # 2.根据要传入的参数类型选择不同的Field类进行实例化
    # 3.传入一个数组,作为校验规则validators
    # 4.可以设置默认值
    q = StringField(validators=[DataRequired(),Length(min=1,max=30)])

    page = IntegerField(validators=[NumberRange(min=1,max=10)],default=1)
  1. 此时在app/web/book.py文件中就可以直接调用就行了
# -*- coding: utf-8 -*-
from flask import jsonify, Blueprint,request
from helper import is_isbn_key
from ShanqiuBook import ShanqiuBook
from . import web
# 导入参数校验
from app.forms.book import SearchForm

# http://127.0.0.1:5000/book/search/?q=金庸&page=1
@web.route('/book/search/')
def hello():

    # 验证层
    # 实例化我们自定义的SearchForm,需要传入一个字典作为要校验的参数
    form  = SearchForm(request.args)
    # validate()方法返回True/False来标示是否校验通过
    if form.validate():

        # 从form中取出校验后的q与page,并且清除空格
        q = form.q.data.strip()
        page = form.page.data

        is_or_key = is_isbn_key(q)
        if is_or_key == 'isbn':
            result = ShanqiuBook.search_by_isbn(q)
        else:
            result = ShanqiuBook.search_by_keyword(q)
        return jsonify(result)
    else:
        return jsonify({'msg':'参数校验失败'})

本章的GitHub链接为:Browse, Zip, Diff.

app层

app下面有main、static、templates三个文件夹以及init.py、email.py、models.py

  • main文件夹用来保存蓝本,此文件夹下 init.py文件里面创建蓝本,(蓝本和程序类似,也可以定义路由。不同的是,在蓝本中定义的路由处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的一部分。)main文件夹下views.py用来保存程序的路由,errors.py用来处理错误,forms.py是存放表单定义。

    • init.py

      和路由相关联的蓝图都在休眠状态,只有当蓝图在应用中被注册后,此时的路由才会成为它的一部分。使用定义在全局作用域下的蓝图,定义应用程序的路由就几乎可以和单脚本应用程序一样简单了。

      蓝图可以定义在一个文件或一个包中与多个模块一起创建更结构化的方式。为了追求最大的灵活性,可以在应用程序包中创建子包来持有蓝图。

      # app/main/ _init__.py:创建蓝图_
      from flask import Blueprint
      main = Blueprint('main', __name__) 
      from . import views, errors
      

      蓝图是通过实例化Blueprint类对象来创建的。这个类的构造函数接收两个参数:蓝图名和蓝图所在的模块或包的位置。与应用程序一样,在大多数情况下,对于第二个参数值使用Python的__name__变量。

      应用程序的路由都保存在app/main/views.py模块内部,而错误处理程序则保存在app/main/errors.py中。导入这些模块可以使路由、错误处理与蓝图相关联。重要的是要注意,在app/init.py脚本的底部导入模块要避免循环依赖,因为view.pyerrors.py都需要导入main蓝图。

      # app/ _init__.py:蓝图注册_
      def create_app(config_name): 
      
          from .main import main as main_blueprint 
          app.register_blueprint(main_blueprint)
      
          return app
      
    • errors.py

      在蓝图中写错误处理的不同之处是,如果使用了errorhandler装饰器,则只会调用在蓝图中引起的错误处理。而应用程序范围内的错误处理则必须使用app_errorhandler

      # app/main/errors.py:蓝图的错误处理
      from flask import render_template 
      from . import main
      
      @main.app_errorhandler(404) 
      def page_not_found(e):
          return render_template('404.html'), 404
      
      @main.app_errorhandler(500) 
      def internal_server_error(e):
          return render_template('500.html'), 500
      
    • views.py

      # app/main/views.py:带有蓝图的应用程序路由
      from datetime import datetime
      from flask import render_template, session, redirect, url_for
      
      from . import main
      from .forms import NameForm 
      from ..models import User
      
      @main.route('/',methods = ['POST','GET'])   #请求方式不管是post还是get都执行这个视图函数
      def index():
          form = NameForm()  #表单实例
          if form.validate_on_submit():   #提交按钮是否成功点击
               # 从数据库中查找和表单数据一样的数据,如果有,取第一个数据
              user = User.query.filter_by(username = form.name.data).first()
              if user is None:   #如果数据库中没有对应的数据
                  user = User(username = form.name.data)  #在数据库中对应的表中创建数据
                  db.session.add(user)  #加入到用户会话,以便数据库进行提交
                  session['known'] = False  #这是一个新用户
                  if current_app.config['FLASKY_ADMIN']:  #如果收件人已经定义,则调用发送邮件函数
                      send_email(current_app.config['FLASKY_ADMIN'],'New User','mail/new_user',user = user)
                      flash('The mail has been sent out')
              else:
                  session['known'] = True  #这是一个老用户
              session['name'] = form.name.data   #从表单获取数据
              return redirect(url_for('.index'))
          return render_template('index.html',current_time = datetime.utcnow(),
                                 form = form,name=session.get('name'),known
      

在蓝图中写视图函数有两大不同点。第一,正如之前的错误处理一样,路由装饰器来自于蓝图。第二个不同是url_for()函数的使用。该函数的第一个参数为路由节点名,它给基于应用程序的路由指定默认视图函数。例如,单脚本应用程序中的index()视图函数的URL可以通过url_for('index')来获得。

Flask名称空间适用于来自蓝图的所有节点,这样多个蓝图可以使用相同节点定义视图函数而不会产生冲突。名称空间就是蓝图名(Blueprint构造函数中的第一个参数),所以index()视图函数注册为main.index且它的URL可以通过url_for(main.index)获得。

rl_for()函数同样支持更短格式的节点,省略蓝图名,例如url_for('.index')。有了这个,就可以这样使用当前请求的蓝图了。这实际意味着相同蓝图内的重定向可以使用更短的形式,如果重定向跨蓝图则必须使用带名称空间的节点名。

完成了应用程序页面更改,表单对象也保存在app/main/forms.py模块中的蓝图里面。

  • static**存放静态文件

  • templates用来存放响应的html文件,mail子文件里面的用来保存发送邮件所需的.html和.txt文件

  • init.py文件里面包含create_app()函数,已经app的各种初始化。

    在单个文件中创建应用程序的方式非常方便,但是它有一个大缺点。因为应用程序创建在全局范围,没有办法动态的适应应用配置的更改:脚本运行时,应用程序实例已经创建,所以它已经来不及更改配置。解决这一问题的方法就是将应用程序放入一个工厂函数中来延迟创建,这样就可以从脚本中显式的调用。这不仅给脚本充足的时间来设置配置,也能用于创建多个应用程序实例。

    这个构造函数导入大部分当前需要使用的扩展,但因为没有应用程序实例初始化它们,它可以被创建但不初始化通过不传递参数给它们的构造函数。create_app()即应用程序工厂函数,需要传入用于应用程序的配置名。配置中的设置被保存在config.py中的一个类中,可以使用Flask的app.config配置对象的from_object()方法来直接导入。配置对象可以通过对象名从config字典中选出。一旦应用程序被创建且配置好,扩展就可以被初始化。调用扩展里的init_app()之前先创建并完成初始化工作。

    # app/ _init__.py:应用程序包构造函数
    from flask import Flask, render_template 
    from flask.ext.bootstrap import Bootstrap 
    from flask.ext.mail import Mail
    from flask.ext.moment import Moment
    from flask.ext.sqlalchemy import SQLAlchemy 
    from config import config
    
    bootstrap = Bootstrap()
    mail = Mail()
    moment = Moment()
    db = SQLAlchemy()
    
    def create_app(config_name):
        app = Flask(__name__) 
        app.config.from_object(config[config_name]) 
        config[config_name].init_app(app)
    
        bootstrap.init_app(app)
        mail.init_app(app)
        moment.init_app(app)
        db.init_app(app)
    
        return app
    

  • email.py包含send_email()发送文件函数(异步)

  • models.py包含User和Role两个表定义

将视图函数拆分到单独的文件中

  1. 如果视图函数都写在主文件中,不利于维护,而是应该把他们放入到一个文件中,每一个模块就是一个试图,用的时候直接引用,这样有利于维护
  2. 在根目录下建立一个app/web文件夹,在这个文件夹下面建立一个book.py文件,专门用来存放book模块,然后在主文件中引用这个模块就可以了,book.py
# -*- coding: utf-8 -*-
from flask import jsonify
from helper import is_isbn_key
from ShanqiuBook import ShanqiuBook

# 为了让book.py模块可以使用app对象
from demo import app

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

    # 调用方法判断用户是根据什么查的
    is_or_key = is_isbn_key(q)
    if is_or_key == 'isbn':
        result = ShanqiuBook.search_by_isbn(q)
    else:
        result = ShanqiuBook.search_by_keyword(q)

    return jsonify(result)
  1. 此时的主文件中
# -*- coding: utf-8 -*-
from flask import Flask
# 为了可以注册book.py中的路由
from app.web import book

app = Flask(__name__)
app.config.from_object('config')

if __name__ == '__main__':
    app.run(debug=app.config['DEBUG'])
  1. 但是这样写的话,会出现404,因为出现了循环引用

六、拆分配置文件

  1. 之前访问数据的时候,count和start都是写死的,现在来进行重构,之前的代码
   @classmethod
    def search_by_key(cls, q, count=15, start=0):
        # count:每页显示的数量
        # start:每页的第一条数据的下标
        url = cls.search_by_key_url.format(q, count, start)
        return HTTP.get(url)
  1. 这样写非常的不妥

    • 在视图函数中接收到的参数是page,代码的封装性,我们应该把count和start的计算过程放到ShanqiuBook.py的 search_by_key方法中来写

    • count的值为了方便日后的管理,这个应该放入到配置文件中,之前的配置文件是config.py,在根目录下,而这个应该放入到app目录下,而关于一些比较隐私的配置信息要妥善处理,所以在app目录下建立两个文件,secure.py用来存放私密的配置信息,setting.py用于存放一些不重要的配置信息,如下

    • app/secure.py

      # -*- coding: utf-8 -*-
      
      # 存放比较机密的配置文件,在上传git的时候不应该上传此文件
      DEBUG = True
      
    • app/setting.py

      # -*- coding: utf-8 -*-
      
      # 生产环境和开发环境几乎一样的,不怎么机密的配置文件
      
      # 每页显示的数据量
      PER_PAGE = 15
      
    • start的计算是一个单独的逻辑,应该用封装成一个方法,使用的时候直接调用

      # 获取每一页的起始下标
          @staticmethod
          def calculate_start(page):
              # 获取配置信息中的每页显示的数量
              return (page -1 ) * current_app.config['PER_PAGE']
      
  2. 重构后的ShanqiuBook.py

   -*- coding: utf-8 -*-
   from httper import httper
# 通过这种方式来导入当前的app对象,方便调用配置而文件

   from flask import current_app

   class ShanqiuBook:

   isbn_url = 'http://t.yushu.im/v2/book/search/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 = httper.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 = httper.get(url)
       return result

# 获取每一页的起始下标
   @staticmethod
   def calculate_start(page):
       return (page -1 ) * current_app.config['PER_PAGE']​
  1. 这个时候在app/__init__.py文件中把配置文件添加到app中
#-- coding: utf-8 --

from flask import Flask

def create_app():

    app = Flask(name)

    # app.config.from_object('config')

    # 把配置文件装载进来

    app.config.from_object('app.secure')

    app.config.from_object('app.setting')

    register_blueprint(app)

    return app

def register_blueprint(app):

    from app.web.book import web

    app.register_blueprint(web)

目前的局限性

目前状态下的应用有两个基本问题。 如果你观察应用的组织方式,你会注意到有几个不同的子系统可以被识别,但支持它们的代码都混合在了一起,没有任何明确的界限。 我们来回顾一下这些子系统是什么:

  • 用户认证子系统,包括app/routes.py中的一些视图函数,app/forms.py中的一些表单,app/templates中的一些模板以及*app/email.py中的电子邮件支持。
  • 错误子系统,它在app/errors.py中定义了错误处理程序并在app/templates中定义了模板。
  • 核心应用功能,包括显示和撰写用户动态,用户个人主页和关注以及用户动态的实时翻译,这些功能遍布大多数应用模块和模板。

思考这三个子系统以及它们组织的方式,你可能会注意到这样一个模式。 到目前为止,我一直遵循的组织逻辑是不同的应用功能归属到其专属的模块。 这些模块之中,一个用于视图函数,一个用于Web表单,一个用于错误,一个用于电子邮件,一个目录用于存放HTML模板等等。 虽然这是一个对小项目有意义的组织结构,但是一旦项目开始增长,它往往会使其中的一些模块变得非常大而且杂乱无章。

要想清晰地看到问题的一种方法,是思考如何通过尽可能多地重复使用这一项目来开始第二个项目。 例如,用户身份验证部分应该在其他应用中也能运行良好,但如果你想按原样使用该代码,则必须进入多个模块并将相关部分复制/粘贴到新项目的新文件中。 看到这是多么不方便了吗? 如果这个项目将所有与认证相关的文件从应用的其余部分中分离出来,会不会更好? Flask的blueprints功能有助于实现更实用的组织结构,从而更轻松地重用代码。

还有第二个问题,虽然它不太明显。 Flask应用实例在app/__init__.py中被创建为一个全局变量,然后又被很多应用模块导入。 虽然这本身并不是问题,但将应用实例作为全局变量可能会使某些情况复杂化,特别是与测试相关的情景。 想象一下你想要在不同的配置下测试这个应用。 由于应用被定义为全局变量,实际上没有办法使用不同配置变量来实例化的两个应用实例。 另一种糟心的情况是,所有测试都使用相同的应用,因此测试可能会对应用进行更改,就会影响稍后运行的其他测试。 理想情况下,你希望所有测试都在原始应用实例上运行的。

你可以在tests.py模块中看到我正在使用的应用实例化之后修改配置的技巧,以指示测试时使用内存数据库而不是默认的SQLite数据库。我真的没有其他办法来更改已配置的数据库,因为在测试开始时已经创建和配置了应用。 对于这种特殊情况,对已配置的应用实例修改配置似乎可以运行,但在其他情况下可能不会,并且在任何情况下,这是一种不推荐的做法,因为这么做可能会导致提示晦涩并且难以找到BUG。

更好的解决方案是不将应用设置为全局变量,而是使用应用工厂函数在运行时创建它。 这将是一个接受配置对象作为参数的函数,并返回一个配置完毕的Flask应用实例。 如果我能够通过应用工厂函数来修改应用,那么编写需要特殊配置的测试会变得很容易,因为每个测试都可以创建它各自的应用。

在本章中,我将通过为上面提到的三个子系统重构应用来介绍blueprints。 展示更改的详细列表有些不切实际,因为几乎应用中每个文件都有少许变化,所以我将讨论重构的步骤,然后你可以下载更改后的应用。

config.py 应用配置示例

config.py中含有一个基类Config定义,三个继承类定义DevlopmentConfig、TestingConfig、ProductionConfig和一个字典config

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' 
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
    FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>' 
    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')

    @staticmethod
    def init_app(app): 
        pass

class DevelopmentConfig(Config): 
    DEBUG = True

    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') 
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 
        'sqlite:///'   os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config): 
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 
        'sqlite:///'   os.path.join(basedir, 'data-test.sqlite')

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 
        'sqlite:///'   os.path.join(basedir, 'data.sqlite')

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

Config基类包含一些相同配置;不同的子类定义不同的配置。额外配置可以在需要的时候在加入。

为了让配置更灵活更安全,一些设置可以从环境变量中导入。例如,SECRET_KEY,由于它的敏感性,可以在环境中设置,但如果环境中没有定义就必须提供一个默认值。

在三个配置中SQLALCHEMY_DATABASE_URI变量可以分配不同的值。这样应用程序可以在不同的配置下运行,每个可以使用不同的数据库。

配置类可以定义一个将应用程序实例作为参数的init_app()静态方法。这里特定于配置的初始化是可以执行的。这里Config基类实现一个空init_app()方法。

在配置脚本的底部,这些不同的配置是注册在配置字典中(config)。将其中一个配置(开发配置)注册为默认配置。

循环引入流程分析

  1. 因为在整个的流程中,app两次初始化,如图
  2. 图片 3
  3. 整个流程中,出现了两次核心app对象的初始化,注册路由是在蓝色流程中初始化的app注册的。但是启动服务是红色流程中的app启动的
  4. book中注册路由所使用的app对象,是他自己所导入fisher模块的app对象(蓝色流程中),而不是红色主流程中所实例化的app对象
  5. 问题1:因为都是由fisher引入book,一个模块只会引入另一个模块一次,所以只执行了一次book
  6. 问题2:由于一次是主流程执行fisher文件;一次是由book模块导入 fisher
  7. 为了验证我们的结论,我们在app实例化,启动,注册路由是哪个地方加入日志信息,
print("id为" str(id(app)) "的app注册路由")
@app.route("/book/search/<q>/<page>")
def search(q, 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_key(q)
    return jsonify(result)
  1. 主文件
app = Flask(__name__)
print("id为" str(id(app)) "的app实例化")
app.config.from_object("config")
# 为了可以注册book.py中的路由
from app.web import book
if __name__ == '__main__':
    print("id为"   str(id(app))   "的app启动")
    app.run(debug=app.config['DEBUG'])
  1. 结果如下
id为92323280的app实例化
id为107142192的app实例化
id为107142192的app注册路由
id为92323280的app启动

可以看到注册路由的app,和启动服务的app不是同一个app。并且最后启动的app是最先实例化的app,也就是红色主流程的app;而注册路由的app是后实例化的app,也就是由book导入fisher模块的蓝色流程的app

七、定义第一个模型类

  1. 我们现在把文件进行整理,如下:

图片 4

  1. 首先在本地创建一个数据库,如下:

图片 5

  1. 在app/models/book.py文件中建立模型,这里使用到sqlalchemy来实现自动化映射,在Flask框架中对这个进行了改良Flask_SQLAlchemy,这个更加人性化,安装(flask--yQglGu4) E:pyqiyueflask>pipenv install flask-sqlalchemy

  2. 建立模型,这样就建立好了模型

# -*- coding: utf-8 -*-

# 首先导入
from sqlalchemy import Column,Integer,String

# sqlalchemy,自动化映射
# Flask_SQLAlchemy,这个是Flask封装后的api,更加人性化

class Book():
    # 需要把这些属性的默认值写成sqlalchemy提供的固定的类型
    # Column()传入参数:数据类型,主键,自增
    id = Column(Integer,primary_key=True,autoincrement=True)
    # 数据类型,不为空
    title = Column(String(50),nullable=False)
    author = Column(String(30),default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdate = Column(String(20))
    # 唯一:unique=True
    isbn = Column(String(15),nullable=False,unique=True)
    summary = Column(String(1000))
    image = Column(String(50))

    # 定义一些方法
    def sample(self):
        pass

Blueprints

在Flask中,blueprint是代表应用子集的逻辑结构。 blueprint可以包括路由,视图函数,表单,模板和静态文件等元素。 如果在单独的Python包中编写blueprint,那么你将拥有一个封装了应用特定功能的组件。

Blueprint的内容最初处于休眠状态。 为了关联这些元素,blueprint需要在应用中注册。 在注册过程中,需要将添加到blueprint中的所有元素传递给应用。 因此,你可以将blueprint视为应用功能的临时存储,以帮助组织代码。

manage.py

包含app创建,manage、migrate初始化,以及make_shell_context()函数在命令行获取上下文,避免频繁导入还有test()函数,用来测试。

# manage.py
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

from wsgi import app
from models.base import db

migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command("db", MigrateCommand)

if __name__ == "__main__":
    manager.run()

八、将模型映射到数据库中

  1. 在模型类app/models/book.py中引入导入核心对象,并实例化,继承
# -*- coding: utf-8 -*-
from sqlalchemy import Column,Integer,String

# 将模型映射到数据库中
# 首先导入核心的对象
from flask_sqlalchemy import SQLAlchemy

# 初始化
db = SQLAlchemy()

# 继承db.Model
class Book(db.Model):
    id = Column(Integer,primary_key=True,autoincrement=True)
    title = Column(String(50),nullable=False)
    author = Column(String(30),default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdate = Column(String(20))
    isbn = Column(String(15),nullable=False,unique=True)
    summary = Column(String(1000))
    image = Column(String(50))
  1. app/__init__.py中进行模型与flask关联
# -*- coding: utf-8 -*-
from flask import Flask
# 导入这个db
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和核心对象关联起来了
    db.init_app(app)
    # 注意这里,这样写的话会报错
    db.create_all() # 把所有的数据模型映射到数据库中
    return app

def register_blueprint(app):
    from app.web.book import web
    app.register_blueprint(web)
  1. 配置数据库连接的配置文件在app/secure.py文件中
# -*- coding: utf-8 -*-

# 存放比较机密的配置文件
DEBUG = True

# 数据库连接url,固定格式
# 要连接的数据库类型,数据库驱动(这里还要进行安装:pipenv install cymysql)
SQLALCHEMY_DATABASE_URI = 'mysql cymysql://root:123456@localhost:3306/book'
  1. 之后运行项目,就会创建在指定的数据库中创建一个数据表了,但是运行项目会出现下面的这种错误
    'No application found. Either work inside a view function or push'

这个是因为在Flask中,不是实例化了app核心对象,其他代码就可以直接使用,要在上面的第二步的注意事项中` db.create_all()`方法中,把app核心对象传入即可

db.create_all(app=app),这样就可以了,在数据库中就可以看到表了

图片 6

错误处理Blueprint

我创建的第一个blueprint用于封装对错误处理程序的支持。 该blueprint的结构如下:

app/
    errors/                             <-- blueprint package
        __init__.py                     <-- blueprint creation
        handlers.py                     <-- error handlers
    templates/
        errors/                         <-- error templates
            404.html
            500.html
    __init__.py                         <-- blueprint registration

实质上,我所做的是将app/errors.py模块移动到app/errors/handlers.py中,并将两个错误模板移动到app/templates/errors中,以便将它们与其他模板分开。 我还必须在两个错误处理程序中更改render_template()调用以使用新的errors模板子目录。 之后,我将blueprint创建添加到app/errors/init.py模块,并在创建应用实例之后,将blueprint注册到app/init.py

我必须提一下,Flask blueprints可以为自己的模板和静态文件配置单独的目录。 我已决定将模板移动到应用模板目录的子目录中,以便所有模板都位于一个层次结构中,但是如果你希望在blueprint中包含属于自己的模板,这也是支持的。 例如,如果向Blueprint()构造函数添加template_folder='templates'参数,则可以将错误blueprint的模板存储在app/errors/templates目录中。

创建blueprint与创建应用非常相似。 这是在blueprint的___init__.py模块中完成的:

app/errors/__init__.py:错误blueprint。

from flask import Blueprint

bp = Blueprint('errors', __name__)

from app.errors import handlers

Blueprint类获取blueprint的名称,基础模块的名称(通常在Flask应用实例中设置为__name__)以及一些可选参数(在这种情况下我不需要这些参数)。 Blueprint对象创建后,我导入了handlers.py模块,以便其中的错误处理程序在blueprint中注册。 该导入位于底部以避免循环依赖。

handlers.py模块中,我放弃使用@app.errorhandler装饰器将错误处理程序附加到应用程序,而是使用blueprint的@bp.app_errorhandler装饰器。 尽管两个装饰器最终都达到了相同的结果,但这样做的目的是试图使blueprint独立于应用,使其更具可移植性。我还需要修改两个错误模板的路径,因为它们被移动到了新errors子目录。

完成错误处理程序重构的最后一步是向应用注册blueprint:

app/init.py:向应用注册错误blueprint。

app = Flask(__name__)

# ...

from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)

# ...

from app import routes, models  # <-- remove errors from this import!

为了注册blueprint,将使用Flask应用实例的register_blueprint()方法。 在注册blueprint时,任何视图函数,模板,静态文件,错误处理程序等均连接到应用。 我将blueprint的导入放在app.register_blueprint()的上方,以避免循环依赖。

用户认证Blueprint

将应用的认证功能重构为blueprint的过程与错误处理程序的过程非常相似。 以下是重构为blueprint的目录层次结构:

app/
    auth/                               <-- blueprint package
        __init__.py                     <-- blueprint creation
        email.py                        <-- authentication emails
        forms.py                        <-- authentication forms
        routes.py                       <-- authentication routes
    templates/
        auth/                           <-- blueprint templates
            login.html
            register.html
            reset_password_request.html
            reset_password.html
    __init__.py                         <-- blueprint registration

为了创建这个blueprint,我必须将所有认证相关的功能移到为blueprint创建的新模块中。 这包括一些视图函数,Web表单和支持功能,例如通过电子邮件发送密码重设token的功能。 我还将模板移动到一个子目录中,以将它们与应用的其余部分分开,就像我对错误页面所做的那样。

在blueprint中定义路由时,使用@bp.route数据与路由,Flask大型教程。装饰器来代替@app.route装饰器。 在url_for()中用于构建URL的语法也需要进行更改。 对于直接附加到应用的常规视图函数,url_for()的第一个参数是视图函数名称。 但当在blueprint中定义路由时,该参数必须包含blueprint名称和视图函数名称,并以句点分隔。 因此,我不得不用诸如url_for('auth.login')的代码替换所有出现的url_for('login')代码,对于其余的视图函数也是如此。

注册auth blueprint到应用时,我使用了些许不同的格式:

app/__init__.py:注册用户认证blueprint到应用。

# ...
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
# ...

在这种情况下,register_blueprint()调用接收了一个额外的参数,url_prefix。 这完全是可选的,Flask提供了给blueprint的路由添加URL前缀的选项,因此blueprint中定义的任何路由都会在其完整URL中获取此前缀。 在许多情况下,这可以用来当成“命名空间”,它可以将blueprint中的所有路由与应用或其他blueprint中的其他路由分开。 对于用户认证,我认为让所有路由以/auth开头很不错,所以我添加了该前缀。 所以现在登录URL将会是http://localhost:5000/auth/login。 因为我使用url_for()来生成URL,所有URL都会自动合并前缀。

主应用Blueprint

第三个blueprint包含核心应用逻辑。 重构这个blueprint和前两个blueprint的过程一样。 我给这个blueprint命名为main,因此所有引用视图函数的url_for()调用都必须添加一个main.数据与路由,Flask大型教程。前缀。 鉴于这是应用的核心功能,我决定将模板留在原来的位置。 这不会有什么问题,因为我已将其他两个blueprint中的模板移动到子目录中了。

应用工厂模式

正如我在本章的介绍中所提到的,将应用设置为全局变量会引入一些复杂性,主要是以某些测试场景的局限性为形式。 在我介绍blueprint之前,应用必须是一个全局变量,因为所有的视图函数和错误处理程序都需要使用来自app的装饰器来修饰,比如@app.route。 但是现在所有的路由和错误处理程序都被转移到了blueprint中,因此保持应用全局性的理由就不够充分了。

所以我要做的是添加一个名为create_app()的函数来构造一个Flask应用实例,并消除全局变量。 转换并非容易,我不得不理清一些复杂的东西,但我们先来看看应用工厂函数:

app/init.py:应用工厂函数。

# ...
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = _l('Please log in to access this page.')
mail = Mail()
bootstrap = Bootstrap()
moment = Moment()
babel = Babel()

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    mail.init_app(app)
    bootstrap.init_app(app)
    moment.init_app(app)
    babel.init_app(app)

    # ... no changes to blueprint registration

    if not app.debug and not app.testing:
        # ... no changes to logging setup

    return app

你已经看到,大多数Flask插件都是通过创建插件实例并将应用作为参数传递来初始化的。 当应用不再作为全局变量时,有一种替代模式,插件分成两个阶段进行初始化。 插件实例首先像前面一样在全局范围内创建,但没有参数传递给它。 这会创建一个未附加到应用的插件实例。 当应用实例在工厂函数中创建时,必须在插件实例上调用init_app()方法,以将其绑定到现在已知的应用。

在初始化期间执行的其他任务保持不变,但会被移到工厂函数而不是在全局范围内。 这包括blueprint和日志配置的注册。 请注意,我在条件中添加了一个not app.testing子句,用于决定是否启用电子邮件和文件日志,以便在单元测试期间跳过所有这些日志记录。 由于在配置中TESTING变量在单元测试时会被设置为True,因此app.testing标志在运行单元测试时将变为True

那么谁来调用应用程工厂函数呢? 最明显使用此函数的地方是处于顶级目录的microblog.py脚本,它是唯一会将应用设置为全局变量的模块。 另一个调用该工厂函数的地方是tests.py,我将在下一节中更详细地讨论单元测试。

正如我上面提到的,大多数对app的引用都是随着blueprint的引入而消失的,但是我仍然需要解决代码中的一些问题。 例如,app/models.pyapp/translate.pyapp/main/routes.py模块都引用了app.config。 幸运的是,Flask开发人员试图使视图函数很容易地访问应用实例,而不必像我一直在做的那样导入它。 Flask提供的current_app变量是一个特殊的“上下文”变量,Flask在分派请求之前使用应用初始化该变量。 你之前已经看到另一个上下文变量,即存储当前语言环境的g变量。 这两个变量,以及Flask-Login的current_user和其他一些你还没有看到的东西,是“魔法”变量,因为它们像全局变量一样工作,但只能在处理请求期间且在处理它的线程中访问。

用Flask的current_app变量替换app就不需要将应用实例作为全局变量导入。 通过简单的搜索和替换,我可以毫无困难地用current_app.config替换对app.config的所有引用。

app/email.py模块提出了一个更大的挑战,所以我必须使用一个小技巧:

app/email.py:将应用实例传递给另一个线程。

from app import current_app

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email,
           args=(current_app._get_current_object(), msg)).start()

send_email()函数中,应用实例作为参数传递给后台线程,后台线程将发送电子邮件而不阻塞主应用程序。在作为后台线程运行的send_async_email()函数中直接使用current_app将不会奏效,因为current_app是一个与处理客户端请求的线程绑定的上下文感知变量。在另一个线程中,current_app没有赋值。直接将current_app作为参数传递给线程对象也不会有效,因为current_app实际上是一个代理对象,它被动态地映射到应用实例。因此,传递代理对象与直接在线程中使用current_app相同。我需要做的是访问存储在代理对象中的实际应用程序实例,并将其作为app参数传递。 current_app._get_current_object()表达式从代理对象中提取实际的应用实例,所以它就是我作为参数传递给线程的。

另一个棘手的模块是app/cli.py,它实现了一些用于管理语言翻译的快捷命令。 在这种情况下,current_app变量不起作用,因为这些命令是在启动时注册的,而不是在处理请求期间(这是唯一可以使用current_app的时间段)注册的。 为了在这个模块中删除对app的引用,我使用了另一个技巧,将这些自定义命令移动到一个将app实例作为参数的register()函数中:

app/cli.py:注册自定义应用命令。

import os
import click

def register(app):
    @app.cli.group()
    def translate():
        """Translation and localization commands."""
        pass

    @translate.command()
    @click.argument('lang')
    def init(lang):
        """Initialize a new language."""
        # ...

    @translate.command()
    def update():
        """Update all languages."""
        # ...

    @translate.command()
    def compile():
        """Compile all languages."""
        # ...

然后我从microblog.py中调用这个register()函数。 以下是完成重构后的microblog.py

microblog.py:重构后的主应用模块。

from app import create_app, db, cli
from app.models import User, Post

app = create_app()
cli.register(app)

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post' :Post}

单元测试的改进

正如我在本章开头所暗示的,到目前为止,我所做的很多工作都是为了改进单元测试工作流程。 在运行单元测试时,要确保应用的配置方式不会污染开发资源(如数据库)。

tests.py的当前版本采用了应用实例化之后修改配置的技巧,这是一种危险的做法,因为并不是所有类型的更改都会在修改之后才生效。 我想要的是有机会在添加到应用之前指定我想要的测试配置项。

create_app()函数现在接受一个配置类作为参数。 默认情况下,使用在config.py中定义的Config类,但现在我可以通过将新类传递给工厂函数来创建使用不同配置的应用实例。 下面是一个适用于我的单元测试的示例配置类:

tests.py:测试配置。

from config import Config

class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite://'

我在这里做的是创建应用的Config类的子类,并覆盖SQLAlchemy配置以使用内存SQLite数据库。 我还添加了一个TESTING属性,并设置为True,我目前不需要该属性,但如果应用需要确定它是否在单元测试下运行,它就派上用场了。

你一定还记得,我的单元测试依赖于setUp()tearDown()方法,它们由单元测试框架自动调用,以创建和销毁每次测试运行的环境。 我现在可以使用这两种方法为每个测试创建和销毁一个测试专用的应用:

tests.py:为每次测试创建一个应用。

class UserModelCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app(TestConfig)
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

新的应用将存储在self.app中,但光是创建一个应用不足以使所有的工作都成功。 思考创建数据库表的db.create_all()语句。 db实例需要注册到应用实例,因为它需要从app.config获取数据库URI,但是当你使用应用工厂时,应用就不止一个了。 那么db如何关联到我刚刚创建的self.app实例呢?

答案在application context中。 还记得current_app变量吗?当不存在全局应用实例导入时,该变量以代理的形式来引用应用实例。 这个变量在当前线程中查找活跃的应用上下文,如果找到了,它会从中获取应用实例。 如果没有上下文,那么就没有办法知道哪个应用实例处于活跃状态,所以current_app就会引发一个异常。 下面你可以看到它是如何在Python控制台中工作的。 这需要通过运行python启动,因为flask shell命令会自动激活应用程序上下文以方便使用。

>>> from flask import current_app
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
Traceback (most recent call last):
    ...
RuntimeError: Working outside of application context.

>>> from app import create_app
>>> app = create_app()
>>> app.app_context().push()
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
'sqlite:////home/miguel/microblog/app.db'

这就是秘密所在! 在调用你的视图函数之前,Flask推送一个应用上下文,它会使current_appg生效。 当请求完成时,上下文将与这些变量一起被删除。 为了使db.create_all()调用在单元测试setUp()方法中工作,我为刚刚创建的应用程序实例推送了一个应用上下文,这样db.create_all()可以使用 current_app.config知道数据库在哪里。 然后在tearDown()方法中,我弹出上下文以将所有内容重置为干净状态。

你还应该知道,应用上下文是Flask使用的两种上下文之一,还有一个请求上下文,它更具体,因为它适用于请求。 在处理请求之前激活请求上下文时,Flask的requestsession以及Flask-Login的current_user变量才会变成可用状态。

环境变量

正如构建此应用时你所看到的,在启动服务器之前,有许多配置选项取决于在环境中设置的变量。 这包括密钥、电子邮件服务器信息、数据库URL和Microsoft Translator API key。 你可能会和我一样觉得,这很不方便,因为每次打开新的终端会话时,都需要重新设置这些变量。

译者注:可以通过将环境变量设置到开机启动中,来保持它们在该计算机中的所有终端中都生效。

应用依赖大量环境变量的常见处理模式是将这些变量存储在应用根目录中的.env文件中。 应用在启动时会从此文件中导入变量,这样就不需要你手动设置这些变量了。

有一个支持.env文件的Python包,名为python-dotenv。 所以让我们安装这个包:

(venv) $ pip install python-dotenv

由于config.py模块是我读取所有环境变量的地方,因此我将在创建Config类之前导入.env文件,以便在构造类时设置变量:

config.py:导入.env文件中的环境变量。

import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))

class Config(object):
    # ...

现在你可以创建一个.env文件并在其中写入应用所需的所有环境变量了。不要将.env文件加入到源代码版本控制中,这非常重要。否则,一旦你的密码和其他重要信息上传到远程代码库中后,你就会后悔莫及。

.env文件可以用于所有配置变量,但是不能用于Flask命令行的FLASK_APPFLASK_DEBUG环境变量,因为它们在应用启动的早期(应用实例和配置对象存在之前)就被使用了。

以下示例显示了.env文件,该文件定义了一个安全密钥,将电子邮件配置为在本地运行的邮件服务器的25端口上,并且不进行身份验证,设置Microsoft Translator API key,使用数据库配置的默认值:

SECRET_KEY=a-really-long-and-unique-key-that-nobody-knows
MAIL_SERVER=localhost
MAIL_PORT=25
MS_TRANSLATOR_KEY=<your-translator-key-here>

依赖文件

此时我已经在Python虚拟环境中安装了一定数量的软件包。 如果你需要在另一台机器上重新生成你的环境,将无法记住你必须安装哪些软件包,所以一般公认的做法是在项目的根目录中写一个requirements.txt文件,列出所有依赖的包及其版本。 生成这个列表实际上很简单:

(venv) $ pip freeze > requirements.txt

pip freeze命令将安装在虚拟环境中的所有软件包以正确的格式输入到requirements.txt文件中。 现在,如果你需要在另一台计算机上创建相同的虚拟环境,无需逐个安装软件包,可以直接运行一条命令实现:

(venv) $ pip install -r requirements.txt

本文由澳门新萄京官方网站发布于www.8455.com,转载请注明出处:数据与路由,Flask大型教程

关键词: