-
Notifications
You must be signed in to change notification settings - Fork 1
Flask Web Development 07
p.95
在 flask, 雖然應用程式可以撰寫在一個 script 檔案中, 但是當應用變大時, 就不容易 scale, 加上 flask 本身不像其他框架, 強加要如何組織檔案架構, 反而將檔案組織留給開發者本身, 因此如何維護大型應用將在本章探討.
切到本章 code
$ git checkout 7a
flasky 檔案架構
$ tree ../flasky
../flasky
├── LICENSE
├── README.md
├── app
│ ├── __init__.py
│ ├── email.py
│ ├── main
│ │ ├── __init__.py
│ │ ├── errors.py
│ │ ├── forms.py
│ │ └── views.py
│ ├── models.py
│ ├── static
│ │ └── favicon.ico
│ └── templates
│ ├── 404.html
│ ├── 500.html
│ ├── base.html
│ ├── index.html
│ └── mail
│ ├── new_user.html
│ └── new_user.txt
├── config.py
├── data.sqlite
├── hello.pyc
├── manage.py
├── migrations
│ ├── README
│ ├── alembic.ini
│ ├── env.py
│ ├── env.pyc
│ ├── script.py.mako
│ └── versions
│ ├── 22e40d65b3cb_initial_migration.pyc
│ └── 38c4e85512a9_initial_migration.py
├── requirements.txt
└── tests
├── __init__.py
└── test_basics.py
8 directories, 30 files
最上層的四個資料夾:
- app/ 包含 flask 應用
- migrations/ 資料庫遷移的指令檔
- test/ 做 unit test 測試
- venv/ Python 的環境 (我的部份沒有, 因為不是用 virtualenv)
其他檔案
- requirements.txt 應用需要的套件
- config.py 應用的設定檔
- manage.py 啟動應用或其他工作的管理檔
本章將探討如何將 hello.py 應用拆成以上的結構
在 hello.py 中, 所有的設定都寫在單一檔案, 但是在開發工作中, 通常會依環境需求需要不同的設定檔
- development
- testing
- production
為了不讓彼此相互干擾, 不直接覆寫 dictionary 的設定值, 將設定檔抽出來成立一個結構式的 configuration class, 放在 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
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')
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
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 base class 中, 存放共用的設定, 其他類別則繼承 Config 的設定, 通常開發與上線的資料庫可能有所不同, 抽出來在不同類設定顯得更有彈性, config 指定預設採 Development 的設定.
如果將單一應用看成一個套件, 應用中所包含的 application code, templates 及 static files 全放在一起, 形成一個 app, 對於 app 的應用可以有特定的名稱, 在此先稱為 app. 由於在 flask 中, templates
與 static
本身就是應用的一部分, 因此這兩個資料夾會一併移到 app, database models 與 email support 同樣也一併移入 app 中, 形成 app/models.py
和 app/email.py
.
所有應用都放在單一檔案, 固然方便, 但是當應用啟動後, 就無法動態改變設定, 因為應用存在 global scope. 為了解決此問題, factory function 允許應用可以產生多個 instance 並伴隨不同的設定檔.
- app/init.py
在建構子當中, 每個應用 instance 都需要使用共同的 Flask extensions, 因此在建構子中, 先是不傳遞參數來呼叫這些擴充套件的建構子, 例如原本應該是 db = SQLAlchemy(app), 現在是 db = SQLAlchemy(), 等到 create_app() 時, 除了導入之前的設定檔, 在 app instance 生成後, 才初始化擴充套件 db.init_app(app).
from flask import Flask
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)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
到目前為止, 工廠模式可用來建立 app instance, 但是狀態並不完整, routes 以及 error page handlers 還沒定義.
但由於工廠模式, 使得 route 變得複雜, 在單一檔案中, application instance 屬於全域變數 (global scope), 所以路由可透過修飾子 app.route
decorator 來處理. 但現在應用的產生屬於 runtime, 在 create_app() 後, app 才會產生但已經太遲, app.errorhandler
decorator 同樣遭遇相同問題.
沒搞懂
flask 中提供一種使用 blueprint 的解法, blueprint 像模具般, 也可以用來設定 route, 無論是在單一檔案或是在多個模組下, route 在 blueprint 也是全域變數, 也更彈性.
- app/main/init.py
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
應用程式的路由會存於 app/main/views.py, error handlers 會存於 app/main/errors.py, 值得一提的是, 以上兩個模組會在 app/init.py 下引入, 以避免循環的相依.
app/__init__.py
Blueprint registration
def create_app(config_name):
# ...
from main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
app/main/errors.py
與單一檔案的不同點在於, 使用修飾子時, @main.app_errorhandler
, 端看 blueprint 註冊在哪
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
app/main/views.py
與單一檔案的兩個相異處, 第一, @main.route
; 第二, url_for('main.index')
from flask import render_template, session, redirect, url_for, current_app
from .. import db
from ..models import User
from ..email import send_email
from . import main
from .forms import NameForm
@main.route('/', methods=['GET', 'POST'])
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)
else:
session['known'] = True
session['name'] = form.name.data
return redirect(url_for('.index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False))
- manage.py
- 如果 os 環境中有定義 FLASK_CONFIG, 則使用該對應的設定, 否則使用預設值
- 為方便起見, 宣告為可執行的 python 檔,
#!/usr/bin/env python
, 即可用 ./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)
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
if __name__ == '__main__':
manager.run()
pip 可將虛擬環境中所使用的套件名稱以及版本匯出, 以利當應用程式遷移到新的 host 上, 可以重新佈署使用的套件
$ pip freeze
alembic==0.8.5
blinker==1.4
dominate==2.1.17
Flask==0.10.1
Flask-Bootstrap==3.3.5.7
Flask-Mail==0.9.1
Flask-Migrate==1.8.0
Flask-Moment==0.5.1
Flask-Script==2.0.5
Flask-SQLAlchemy==2.1
Flask-WTF==0.12
itsdangerous==0.24
Jinja2==2.8
Mako==1.0.4
MarkupSafe==0.23
python-editor==0.5
SQLAlchemy==1.0.12
visitor==0.1.2
Werkzeug==0.11.4
WTForms==2.1
輸出到 requirements.txt, 慣例使用的檔案名稱
$ pip freeze > requirements.txt
未來在新的 host 上, 只要建立隔離的虛擬環境後, 執行下列指令, 即可安裝之前用過的套件
$ pip install -r requirements.txt
- 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 套件, 為了執行測試, 在 manage.py
中加入 test()
- manage.py
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
執行結果如下
$ python manage.py test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.073s
在設定檔中, 可依據不同開發環境, 指定資料庫名稱, 或是資料庫類型, 建立資料庫遷移的指令
$ python manage.py db upgrade