在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Python/ Flask 方案
應用環(huán)境
配置管理
大型應用
可插撥視圖
Flask 方案
在 Shell 中使用 Flask
針對高級程序員的前言
使用藍圖的模塊化應用
部署方式
信號
排除應用錯誤
模板
請求環(huán)境
掌握應用錯誤
測試 Flask 應用
前言
教程
安裝
快速上手
Flask 擴展

Flask 方案

有一些東西是大多數(shù)網(wǎng)絡應用都會用到的。比如許多應用都會使用關系型數(shù)據(jù)庫和用戶 驗證,在請求之前連接數(shù)據(jù)庫并得到當前登錄用戶的信息,在請求之后關閉數(shù)據(jù)庫連接。

更多用戶貢獻的代碼片斷和方案參見 Flask 代碼片斷歸檔 。

本章包含內(nèi)容:

  • 大型應用
  • 應用工廠
  • 應用調(diào)度
  • 實現(xiàn) API 異常處理
  • URL 處理器
  • 使用 Distribute 部署
  • 使用 Fabric 部署
  • 在 Flask 中使用 SQLite 3
  • 在 Flask 中使用 SQLAlchemy
  • 上傳文件
  • 緩存
  • 視圖裝飾器
  • 使用 WTForms 進行表單驗證
  • 模板繼承
  • 消息閃現(xiàn)
  • 通過 jQuery 使用 AJAX
  • 自定義出錯頁面
  • 惰性載入視圖
  • 在 Flask 中使用 MongoKit
  • 添加一個頁面圖標
  • 流內(nèi)容
  • 延遲的請求回調(diào)
  • 添加 HTTP 方法重載
  • 請求內(nèi)容校驗
  • 基于 Celery 的后臺任務

大型應用

對于大型應用來說使用包代替模塊是一個好主意。使用包非常簡單。假設有一個小應用如下:

/yourapplication
    /yourapplication.py
    /static
        /style.css
    /templates
        layout.html
        index.html
        login.html
        ...

簡單的包

要把上例中的小應用裝換為大型應用只要在現(xiàn)有應用中創(chuàng)建一個名為 yourapplication 的新文件夾,并把所有東西都移動到這個文件夾內(nèi)。然后把 yourapplication.py 更名為 __init__.py 。(請首先刪除所有 .pyc 文件,否則基本上會出問題)

修改完后應該如下例:

/yourapplication
    /yourapplication
        /__init__.py
        /static
            /style.css
        /templates
            layout.html
            index.html
            login.html
            ...

但是現(xiàn)在如何運行應用呢?原本的 python yourapplication/__init__.py 無法運行了。因為 Python 不希望包內(nèi)的模塊成為啟動文件。但是這不是一個大問題,只要在 yourapplication 文件夾旁添加一個 runserver.py 文件就可以了,其內(nèi)容如下:

from yourapplication import app
app.run(debug=True)

我們從中學到了什么?現(xiàn)在我們來重構一下應用以適應多模塊。只要記住以下幾點:

  1. Flask 應用對象必須位于 __init__.py文件中。這樣每個模塊就可以安全地導入了,且 __name__變量會解析到正確的包。

  2. 所有視圖函數(shù)(在頂端有 route() 的)必須在 __init__.py 文件中被導入。不是導入對象本身,而是導入視圖模塊。請 在應用對象創(chuàng)建之后導入視圖對象。

__init__.py 示例:

from flask import Flask
app = Flask(__name__)

import yourapplication.views

views.py 內(nèi)容如下:

from yourapplication import app

@app.route('/')
def index():
    return 'Hello World!'

最終全部內(nèi)容如下:

/yourapplication
    /runserver.py
    /yourapplication
        /__init__.py
        /views.py
        /static
            /style.css
        /templates
            layout.html
            index.html
            login.html
            ...

回環(huán)導入

回環(huán)導入是指兩個模塊互相導入,本例中我們添加的 views.py 就與 __init__.py 相互依賴。每個 Python 程序員都討厭回環(huán)導入。一般情況下回環(huán)導入是個壞主意,但 在這里一點問題都沒有。原因是我們沒有真正使用 __init__.py 中的視圖,只是 保證模塊被導入,并且我們在文件底部才這樣做。

但是這種方式還是有些問題,因為沒有辦法使用裝飾器。要找到解決問題的靈感請參閱大型應用一節(jié)。

使用藍圖

對于大型應用推薦把應用分隔為小塊,每個小塊使用藍圖輔助執(zhí)行。關于這個主題的介紹 請參閱使用藍圖的模塊化應用一節(jié) 。

應用工廠

如果你已經(jīng)在應用中使用了包和藍圖( 使用藍圖的模塊化應用 ),那么還有許多方法可以更 進一步地改進你的應用。常用的方案是導入藍圖后創(chuàng)建應用對象,但是如果在一個函數(shù)中創(chuàng)建對象,那么就可以創(chuàng)建多個實例。

那么這樣做有什么用呢?

  1. 用于測試??梢葬槍Σ煌那闆r使用不同的配置來測試應用。

  2. 用于多實例,如果你需要運行同一個應用的不同版本的話。當然你可以在服務器上 使用不同配置運行多個相同應用,但是如果使用應用工廠,那么你可以只使用一個應用進程而得到多個應用實例,這樣更容易操控。

那么如何做呢?

基礎工廠

方法是在一個函數(shù)中設置應用,具體如下:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

    from yourapplication.views.admin import admin
    from yourapplication.views.frontend import frontend
    app.register_blueprint(admin)
    app.register_blueprint(frontend)

    return app

這個方法的缺點是在導入時無法在藍圖中使用應用對象。但是你可以在一個請求中使用它。 如何通過配置來訪問應用?使用 current_app:

from flask import current_app, Blueprint, render_template
admin = Blueprint('admin', __name__, url_prefix='/admin')

@admin.route('/')
def index():
    return render_template(current_app.config['INDEX_TEMPLATE'])

這里我們在配置中查找模板的名稱。

擴展對象初始化時不會綁定到一個應用,應用可以使用 db.init_app 來設置擴展。 擴展對象中不會儲存特定應用的狀態(tài),因此一個擴展可以被多個應用使用。關于擴展設計的更多信息請參閱 Flask 擴展開發(fā)

當使用 Flask-SQLAlchemy 時,你的 model.py 可能是這樣的:

from flask.ext.sqlalchemy import SQLAlchemy
# no app object passed! Instead we use use db.init_app in the factory.
db = SQLAlchemy()

# create some models

使用應用

因此,要使用這樣的應用就必須先創(chuàng)建它。下面是一個運行應用的示例 run.py 文件:

from yourapplication import create_app
app = create_app('/path/to/config.cfg')
app.run()

改進工廠

上面的工廠函數(shù)還不是足夠好,可以改進的地方主要有以下幾點:

  1. 為了單元測試,要想辦法傳入配置,這樣就不必在文件系統(tǒng)中創(chuàng)建配置文件。

  2. 當設置應用時從藍圖調(diào)用一個函數(shù),這樣就可以有機會修改屬性(如掛接請求前/后 處理器等)。

  3. 如果有必要的話,當創(chuàng)建一個應用時增加一個 WSGI 中間件。

應用調(diào)度

應用調(diào)度是在 WSGI 層面組合多個 WSGI 應用的過程??梢越M合多個 Flask 應用,也可以 組合 Flask 應用和其他 WSGI 應用。通過這種組合,如果有必要的話,甚至可以在同一個 解釋器中一邊運行 Django ,一邊運行 Flask 。這種組合的好處取決于應用內(nèi)部是如何 工作的。

應用調(diào)度與模塊化的最大不同在于應用調(diào)度中的每個 應用是完全獨立的,它們以各自的配置運行,并在 WSGI 層面被調(diào)度。

說明

下面所有的技術說明和舉例都歸結于一個可以運行于任何 WSGI 服務器的 application 對象。對于生產(chǎn)環(huán)境,參見 部署方式 。對于開發(fā)環(huán)境, Werkzeug 提供了一個內(nèi)建開發(fā)服務器,它使用 werkzeug.serving.run_simple() 來運行:

from werkzeug.serving import run_simple
run_simple('localhost', 5000, application, use_reloader=True)

注意 run_simple 不能用于生產(chǎn)環(huán)境,生產(chǎn)環(huán)境服務器參見成熟的 WSGI 服務器 。

為了使用交互調(diào)試器,應用和簡單服務器都應當處于調(diào)試模式。下面是一個簡單的 “ hello world ”示例,使用了調(diào)試模式和 run_simple:

from flask import Flask
from werkzeug.serving import run_simple

app = Flask(__name__)
app.debug = True

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    run_simple('localhost', 5000, app,
               use_reloader=True, use_debugger=True, use_evalex=True)

組合應用

如果你想在同一個 Python 解釋器中運行多個獨立的應用,那么你可以使用 werkzeug.wsgi.DispatcherMiddleware 。其原理是:每個獨立的 Flask 應用都 是一個合法的 WSGI 應用,它們通過調(diào)度中間件組合為一個基于前綴調(diào)度的大應用。

假設你的主應用運行于 / ,后臺接口位于 /backend:

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend

application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

根據(jù)子域調(diào)度

有時候你可能需要使用不同的配置來運行同一個應用的多個實例。可以把應用創(chuàng)建過程 放在一個函數(shù)中,這樣調(diào)用這個函數(shù)就可以創(chuàng)建一個應用的實例,具體實現(xiàn)參見應用工廠方案。

最常見的做法是每個子域創(chuàng)建一個應用,配置服務器來調(diào)度所有子域的應用請求,使用 子域來創(chuàng)建用戶自定義的實例。一旦你的服務器可以監(jiān)聽所有子域,那么就可以使用一個很簡單的 WSGI 應用來動態(tài)創(chuàng)建應用了。

WSGI 層是完美的抽象層,因此可以寫一個你自己的 WSGI 應用來監(jiān)視請求,并把請求分配給你的 Flask 應用。如果被分配的應用還沒有創(chuàng)建,那么就會動態(tài)創(chuàng)建應用并被登記下來:

from threading import Lock

class SubdomainDispatcher(object):

    def __init__(self, domain, create_app):
        self.domain = domain
        self.create_app = create_app
        self.lock = Lock()
        self.instances = {}

    def get_application(self, host):
        host = host.split(':')[0]
        assert host.endswith(self.domain), 'Configuration error'
        subdomain = host[:-len(self.domain)].rstrip('.')
        with self.lock:
            app = self.instances.get(subdomain)
            if app is None:
                app = self.create_app(subdomain)
                self.instances[subdomain] = app
            return app

    def __call__(self, environ, start_response):
        app = self.get_application(environ['HTTP_HOST'])
        return app(environ, start_response)

調(diào)度器示例:

from myapplication import create_app, get_user_for_subdomain
from werkzeug.exceptions import NotFound

def make_app(subdomain):
    user = get_user_for_subdomain(subdomain)
    if user is None:
        # 如果子域沒有對應的用戶,那么還是得返回一個 WSGI 應用
        # 用于處理請求。這里我們把 NotFound() 異常作為應用返回,
        # 它會被渲染為一個缺省的 404 頁面。然后,可能還需要把
        # 用戶重定向到主頁。
        return NotFound()

    # 否則為特定用戶創(chuàng)建應用
    return create_app(user)

application = SubdomainDispatcher('example.com', make_app)

根據(jù)路徑調(diào)度

根據(jù) URL 的路徑調(diào)度非常簡單。上面,我們通過查找 Host 頭來判斷子域,現(xiàn)在只要查找請求路徑的第一個斜杠之前的路徑就可以了:

from threading import Lock
from werkzeug.wsgi import pop_path_info, peek_path_info

class PathDispatcher(object):

    def __init__(self, default_app, create_app):
        self.default_app = default_app
        self.create_app = create_app
        self.lock = Lock()
        self.instances = {}

    def get_application(self, prefix):
        with self.lock:
            app = self.instances.get(prefix)
            if app is None:
                app = self.create_app(prefix)
                if app is not None:
                    self.instances[prefix] = app
            return app

    def __call__(self, environ, start_response):
        app = self.get_application(peek_path_info(environ))
        if app is not None:
            pop_path_info(environ)
        else:
            app = self.default_app
        return app(environ, start_response)

與根據(jù)子域調(diào)度相比最大的不同是:根據(jù)路徑調(diào)度時,如果創(chuàng)建函數(shù)返回 None ,那么就會回落到另一個應用:

from myapplication import create_app, default_app, get_user_for_prefix

def make_app(prefix):
    user = get_user_for_prefix(prefix)
    if user is not None:
        return create_app(user)

application = PathDispatcher(default_app, make_app)

實現(xiàn) API 異常處理

在 Flask 上經(jīng)常會執(zhí)行 RESTful API 。開發(fā)者首先會遇到的問題之一是用于 API 的內(nèi)建異常處理不給力,回饋的內(nèi)容不是很有用。

對于非法使用 API ,比使用 abort 更好的解決方式是實現(xiàn)你自己的異常處理類型, 并安裝相應句柄,輸出符合用戶格式要求的出錯信息。

簡單的異常類

基本的思路是引入一個新的異常,回饋一個合適的可讀性高的信息、一個狀態(tài)碼和一些可選的負載,給錯誤提供更多的環(huán)境內(nèi)容。

以下是一個簡單的示例:

from flask import jsonify

class InvalidUsage(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        Exception.__init__(self)
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

這樣一個視圖就可以拋出帶有出錯信息的異常了。另外,還可以通過 payload 參數(shù)以字典的形式提供一些額外的負載。

注冊一個錯誤處理句柄

現(xiàn)在,視圖可以拋出異常,但是會立即引發(fā)一個內(nèi)部服務錯誤。這是因為沒有為這個錯誤處理類注冊句柄。句柄增加很容易,例如:

@app.errorhandler(InvalidAPIUsage)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

在視圖中的用法

以下是如何在視圖中使用該功能:

@app.route('/foo')
def get_foo():
    raise InvalidUsage('This view is gone', status_code=410)

URL 處理器

New in version 0.7.

Flask 0.7 引入了 URL 處理器,其作用是為你處理大量包含相同部分的 URL 。假設你有 許多 URL 都包含語言代碼,但是又不想在每個函數(shù)中都重復處理這個語言代碼,那么就可可以使用 URL 處理器。

在與藍圖配合使用時, URL 處理器格外有用。下面我們分別演示在應用中和藍圖中使用 URL 處理器。

國際化應用的 URL

假設有應用如下:

from flask import Flask, g

app = Flask(__name__)

@app.route('/<lang_code>/')
def index(lang_code):
    g.lang_code = lang_code
    ...

@app.route('/<lang_code>/about')
def about(lang_code):
    g.lang_code = lang_code
    ...

上例中出現(xiàn)了大量的重復:必須在每一個函數(shù)中把語言代碼賦值給 g 對象。當然,如果使用一個裝飾器可以簡化這個工作。但是,當你需要生成由一個函數(shù)指向另一個函數(shù)的 URL 時,還是得顯式地提供語言代碼,相當麻煩。

我們使用 url_defaults() 函數(shù)來簡化這個問題。這個函數(shù)可以自動 把值注入到 url_for() 。以下代碼檢查在 URL 字典中是否存在語言代碼, 端點是否需要一個名為 'lang_code' 的值:

@app.url_defaults
def add_language_code(endpoint, values):
    if 'lang_code' in values or not g.lang_code:
        return
    if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
        values['lang_code'] = g.lang_code

URL 映射的 is_endpoint_expecting() 方法可用于檢查端點是否需要提供一個語言代碼。

上例的逆向函數(shù)是 url_value_preprocessor() 。這些函數(shù)在請求匹配后立即根據(jù) URL 的值執(zhí)行代碼。它們可以從 URL 字典中取出值,并把取出的值放在 其他地方:

@app.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code', None)

這樣就不必在每個函數(shù)中把 lang_code 賦值給 g 了。你還可以作 進一步改進:寫一個裝飾器把語言代碼作為 URL 的前綴。但是更好的解決方式是使用 藍圖。一旦 'lang_code' 從值的字典中彈出,它就不再傳送給視圖函數(shù)了。精簡后的代碼如下:

from flask import Flask, g

app = Flask(__name__)

@app.url_defaults
def add_language_code(endpoint, values):
    if 'lang_code' in values or not g.lang_code:
        return
    if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
        values['lang_code'] = g.lang_code

@app.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code', None)

@app.route('/<lang_code>/')
def index():
    ...

@app.route('/<lang_code>/about')
def about():
    ...

國際化的藍圖 URL

因為藍圖可以自動給所有 URL 加上一個統(tǒng)一的前綴,所以應用到每個函數(shù)就非常方便了。 更進一步,因為藍圖 URL 預處理器不需要檢查 URL 是否真的需要要一個 'lang_code' 參數(shù),所以可以去除 url_defaults())函數(shù)中的邏輯判斷:

from flask import Blueprint, g

bp = Blueprint('frontend', __name__, url_prefix='/<lang_code>')

@bp.url_defaults
def add_language_code(endpoint, values):
    values.setdefault('lang_code', g.lang_code)

@bp.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code')

@bp.route('/')
def index():
    ...

@bp.route('/about')
def about():
    ...

使用 Distribute 部署

distribute 的前身是 setuptools ,它是一個擴展庫,通常用于分發(fā) Python 庫和 擴展。它的英文名稱的就是“分發(fā)”的意思。它擴展了 Python 自帶的一個基礎模塊安裝 系統(tǒng) distutils ,支持多種更復雜的結構,方便了大型應用的分發(fā)部署。它的主要特色:

  • 支持依賴 :一個庫或者應用可以聲明其所依賴的其他庫的列表。依賴庫將被自動 安裝。
  • 包注冊 :可以在安裝過程中注冊包,這樣就可以通過一個包查詢其他包的信息。 這套系統(tǒng)最有名的功能是“切入點”,即一個包可以定義一個入口,以便于其他包掛接, 用以擴展包。
  • 安裝管理 : distribute 中的 easy_install 可以為你安裝其他庫。你也可以使用早晚會替代 easy_install 的 pip ,它除了安裝包還可以做更多的事。

Flask 本身,以及其他所有在 cheeseshop 中可以找到的庫要么是用 distribute 分發(fā)的, 要么是用老的 setuptools 或 distutils 分發(fā)的。

在這里我們假設你的應用名稱是 yourapplication.py ,且沒有使用模塊,而是一個 。 distribute 不支持分發(fā)標準模塊,因此我們不 討論模塊的問題。關于如何把模塊轉換為包的信息參見大型應用方案。

使用 distribute 將使發(fā)布更復雜,也更加自動化。如果你想要完全自動化處理,請同時閱讀使用 Fabric 部署一節(jié)。

基礎設置腳本

因為你已經(jīng)安裝了 Flask ,所以你應當已經(jīng)安裝了 setuptools 或 distribute 。如果 沒有安裝,不用怕,有一個 distribute_setup.py 腳本可以幫助你安裝。只要下載這個腳本,并在你的 Python 解釋器中運行就可以了。

標準聲明: 最好使用 virtualenv 。

你的設置代碼應用放在 setup.py 文件中,這個文件應當位于應用旁邊。這個文件名只是 一個約定,但是最好不要改變,因為大家都會去找這個文件。

是的,即使你使用 distribute ,你導入的包也是 setuptools 。 distribute 完全向后兼容于 setuptools ,因此它使用相同的導入名稱。

Flask 應用的基礎 setup.py 文件示例如下:

from setuptools import setup

setup(
    name='Your Application',
    version='1.0',
    long_description=__doc__,
    packages=['yourapplication'],
    include_package_data=True,
    zip_safe=False,
    install_requires=['Flask']
)

請記住,你必須顯式的列出子包。如果你要 distribute 自動為你搜索包,你可以使用 find_packages 函數(shù):

from setuptools import setup, find_packages

setup(
    ...
    packages=find_packages()
)

大多數(shù) setup 的參數(shù)可以望文生義,但是 include_package_data 和 zip_safe 可以不容易理解。 include_package_data 告訴 distribute 要搜索一個 MANIFEST.in 文件,把文件內(nèi)容所匹配的所有條目作為包數(shù)據(jù)安裝??梢酝ㄟ^使用這個參數(shù)分發(fā) Python 模塊的靜態(tài)文件和模板(參見分發(fā)資源 )。 zip_safe 標志可用于強制或防止創(chuàng)建 zip 壓縮包。通常你不會想要把包安裝為 zip 壓縮文件,因為一些工具不支持壓縮文件,而且壓縮文件比較難以調(diào)試。

分發(fā)資源

如果你嘗試安裝上文創(chuàng)建的包,你會發(fā)現(xiàn)諸如 static 或 templates 之類的文件夾沒有被安裝。原因是 distribute 不知道要為你添加哪些文件。你要做的是:在你的 setup.py 文件旁邊創(chuàng)建一個 MANIFEST.in 文件。這個文件列出了所有應當添加到 tar 壓縮包的文件:

recursive-include yourapplication/templates *
recursive-include yourapplication/static *

不要忘了把 setup 函數(shù)的 include_package_data 參數(shù)設置為 True !否則即使把內(nèi)容在 MANIFEST.in 文件中全部列出來也沒有用。

聲明依賴

依賴是在 install_requires 參數(shù)中聲明的,這個參數(shù)是一個列表。列表中的每一項都是一個需要在安裝時從 PyPI 獲得的包。缺省情況下,總是會獲得最新版本的包,但你可以指定最高版本和最低版本。示例:

install_requires=[
    'Flask>=0.2',
    'SQLAlchemy>=0.6',
    'BrokenPackage>=0.7,<=1.0'
]

我前面提到,依賴包都從 PyPI 獲得的。但是如果要從別的地方獲得包怎么辦呢?你只要 還是按照上述方法寫,然后提供一個可選地址列表就行了:

dependency_links=['http://example.com/yourfiles']

請確保頁面上有一個目錄列表,且頁面上的鏈接指向正確的 tar 壓縮包。這樣 distribute 就會找到文件了。如果你的包在公司內(nèi)部網(wǎng)絡上,請?zhí)峁┲赶蚍掌鞯?URL 。

安裝 / 開發(fā)

要安裝你的應用(理想情況下是安裝到一個 virtualenv ),只要運行帶 install 參數(shù)的 setup.py 腳本就可以了。它會將你的應用安裝到 virtualenv 的 site-packages 文件夾下,同時下載并安裝依賴:

$ python setup.py install

如果你正開發(fā)這個包,同時也希望相關依賴被安裝,那么可以使用 develop 來代替:

$ python setup.py develop

這樣做的好處是只安裝一個指向 site-packages 的連接,而不是把數(shù)據(jù)復制到那里。這樣在開發(fā)過程中就不必每次修改以后再運行 install 了。

使用 Fabric 部署

Fabric 是一個 Python 工具,與 Makefiles 類似,但是能夠在遠程服務器上執(zhí)行命令。如果與適當?shù)?Python 包( 大型應用 )與優(yōu)良的配置( 配置管理 )相結合那么 Fabric 將是在外部服務器上部署 Flask 的利器。

在下文開始之前,有幾點需要明確:

  • Fabric 1.0 需要要被安裝到本地。本教程假設使用的是最新版本的 Fabric 。

  • 應用已經(jīng)是一個包,且有一個可用的 setup.py 文件( 使用 Distribute 部署 )。

  • 在下面的例子中,我們假設遠程服務器使用 mod_wsgi 。當然,你可以使用你自己 喜歡的服務器,但是在示例中我們選擇 Apache + mod_wsgi ,因為它們設置方便, 且在沒有 root 權限情況下可以方便的重載應用。

創(chuàng)建第一個 Fabfile

fabfile 是控制 Fabric 的東西,其文件名為 fabfile.py ,由 fab 命令執(zhí)行。在這個文件中定義的所有函數(shù)都會被視作 fab 子命令。這些命令將會在一個或多個主機上運行。這些主機可以在 fabfile 中定義,也可以在命令行中定義。本例將在 fabfile 中 定義主機。

下面是第一個例子,比較級。它可以把當前的源代碼上傳至服務器,并安裝到一個預先存在的 virtual 環(huán)境:

from fabric.api import *

# 使用遠程命令的用戶名
env.user = 'appuser'
# 執(zhí)行命令的服務器
env.hosts = ['server1.example.com', 'server2.example.com']

def pack():
    # 創(chuàng)建一個新的分發(fā)源,格式為 tar 壓縮包
    local('python setup.py sdist --formats=gztar', capture=False)

def deploy():
    # 定義分發(fā)版本的名稱和版本號
    dist = local('python setup.py --fullname', capture=True).strip()
    # 把 tar 壓縮包格式的源代碼上傳到服務器的臨時文件夾
    put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz')
    # 創(chuàng)建一個用于解壓縮的文件夾,并進入該文件夾
    run('mkdir /tmp/yourapplication')
    with cd('/tmp/yourapplication'):
        run('tar xzf /tmp/yourapplication.tar.gz')
        # 現(xiàn)在使用 virtual 環(huán)境的 Python 解釋器來安裝包
        run('/var/www/yourapplication/env/bin/python setup.py install')
    # 安裝完成,刪除文件夾
    run('rm -rf /tmp/yourapplication /tmp/yourapplication.tar.gz')
    # 最后 touch .wsgi 文件,讓 mod_wsgi 觸發(fā)應用重載
    run('touch /var/www/yourapplication.wsgi')

上例中的注釋詳細,應當是容易理解的。以下是 fabric 提供的最常用命令的簡要說明:

  • run - 在遠程服務器上執(zhí)行一個命令
  • local - 在本地機器上執(zhí)行一個命令
  • put - 上傳文件到遠程服務器上
  • cd - 在服務器端改變目錄。必須與 with 語句聯(lián)合使用。

運行 Fabfile

那么如何運行 fabfile 呢?答案是使用 fab 命令。要在遠程服務器上部署當前版本的代碼可以使用這個命令:

$ fab pack deploy

但是這個命令需要遠程服務器上已經(jīng)創(chuàng)建了 /var/www/yourapplication 文件夾,且 /var/www/yourapplication/env 是一個 virtual 環(huán)境。更進一步,服務器上還沒有創(chuàng)建配置文件和 .wsgi 文件。那么,我們?nèi)绾卧谝粋€新的服務器上創(chuàng)建一個基礎環(huán)境呢?

這個問題取決于你要設置多少臺服務器。如果只有一臺應用服務器(多數(shù)情況下),那么在 fabfile 中創(chuàng)建命令有一點多余。當然,你可以這么做。這個命令可以稱之為 setup 或 bootstrap 。在使用命令時顯式傳遞服務器名稱:

$ fab -H newserver.example.com bootstrap

設置一個新服務器大致有以下幾個步驟:

  1. 在 /var/www 創(chuàng)建目錄結構:
$ mkdir /var/www/yourapplication
$ cd /var/www/yourapplication
$ virtualenv --distribute env
  1. 上傳一個新的 application.wsgi 文件和應用配置文件(如 application.cfg ) 到服務器上。

  2. 創(chuàng)建一個新的用于 yourapplication 的 Apache 配置并激活它。要確保激活 .wsgi 文件變動監(jiān)視,這樣在 touch 的時候可以自動重載應用。( 更多信息參見 mod_wsgi (Apache)

現(xiàn)在的問題是: application.wsgi 和 application.cfg 文件從哪里來?

WSGI 文件

WSGI 文件必須導入應用,并且還必須設置一個環(huán)境變量用于告訴應用到哪里去搜索配置。 示例:

import os
os.environ['YOURAPPLICATION_CONFIG'] = '/var/www/yourapplication/application.cfg'
from yourapplication import app

應用本身必須像下面這樣初始化自己才會根據(jù)環(huán)境變量搜索配置:

app = Flask(__name__)
app.config.from_object('yourapplication.default_config')
app.config.from_envvar('YOURAPPLICATION_CONFIG')

這個方法在配置管理一節(jié)已作了詳細的介紹。

配置文件

上文已談到,應用會根據(jù) YOURAPPLICATION_CONFIG 環(huán)境變量找到正確的配置文件。 因此我們應當把配置文件放在應用可以找到的地方。在不同的電腦上配置文件是不同的, 所以一般我們不對配置文件作版本處理。

一個流行的方法是在一個獨立的版本控制倉庫為不同的服務器保存不同的配置文件,然后 在所有服務器進行檢出。然后在需要的地方使用配置文件的符號鏈接(例如: /var/www/yourapplication )。

不管如何,我們這里只有一到兩臺服務器,因此我們可以預先手動上傳配置文件。

第一次部署

現(xiàn)在我們可以進行第一次部署了。我已經(jīng)設置好了服務器,因此服務器上應當已經(jīng)有了 virtual 環(huán)境和已激活的 apache 配置。現(xiàn)在我們可以打包應用并部署它了:

$ fab pack deploy

Fabric 現(xiàn)在會連接所有服務器并運行 fabfile 中的所有命令。首先它會打包應用得到一個 tar 壓縮包。然后會執(zhí)行分發(fā),把源代碼上傳到所有服務器并安裝。感謝 setup.py 文件,所需要的依賴庫會自動安裝到 virtual 環(huán)境。

下一步

在前文的基礎上,還有更多的方法可以全部署工作更加輕松:

  • 創(chuàng)建一個初始化新服務器的 bootstrap 命令。它可以初始化一個新的 virtual 環(huán)境、正確設置 apache 等等。
  • 把配置文件放入一個獨立的版本庫中,把活動配置的符號鏈接放在適當?shù)牡胤健?/li>
  • 還可以把應用代碼放在一個版本庫中,在服務器上檢出最新版本后安裝。這樣你可以方便的回滾到老版本。
  • 掛接測試功能,方便部署到外部服務器進行測試。 使用 Fabric 是一件有趣的事情。你會發(fā)現(xiàn)在電腦上打出 fab deploy 是非常神奇的。 你可以看到你的應用被部署到一個又一個服務器上。

在 Flask 中使用 SQLite 3

在 Flask 中,你可以方便的按需打開數(shù)據(jù)庫連接,并且在環(huán)境解散時關閉這個連接( 通常是請求結束的時候)。

以下是一個在 Flask 中使用 SQLite 3 的例子:

import sqlite3
from flask import g

DATABASE = '/path/to/database.db'

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = connect_to_database()
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

為了使用數(shù)據(jù)庫,所有應用都必須準備好一個處于激活狀態(tài)的環(huán)境。使用 get_db 函數(shù)可以得到數(shù)據(jù)庫連接。當環(huán)境解散時,數(shù)據(jù)庫連接會被切斷。

注意:如果你使用的是 Flask 0.9 或者以前的版本,那么你必須使用 flask._app_ctx_stack.top ,而不是 g 。因為 flask.g 對象是綁定到請求的,而不是應用環(huán)境。

示例:

@app.route('/')
def index():
    cur = get_db().cursor()
    ...

Note 請記住,解散請求和應用環(huán)境的函數(shù)是一定會被執(zhí)行的。即使請求前處理器執(zhí)行失敗或根本沒有執(zhí)行,解散函數(shù)也會被執(zhí)行。因此,我們必須保證在關閉數(shù)據(jù)庫連接之前 數(shù)據(jù)庫連接是存在的。

按需連接

上述方式(在第一次使用時連接數(shù)據(jù)庫)的優(yōu)點是只有在真正需要時才打開數(shù)據(jù)庫連接。 如果你想要在一個請求環(huán)境之外使用數(shù)據(jù)庫連接,那么你可以手動在 Python 解釋器打開應用環(huán)境:

with app.app_context():
    # now you can use get_db()

簡化查詢

現(xiàn)在,在每個請求處理函數(shù)中可以通過訪問 g.db 來得到當前打開的數(shù)據(jù)庫連接。為了簡化 SQLite 的使用,這里有一個有用的行工廠函數(shù)。該函數(shù)會轉換每次從數(shù)據(jù)庫返回的 結果。例如,為了得到字典類型而不是元組類型的返回結果,可以這樣:

def make_dicts(cursor, row):
    return dict((cur.description[idx][0], value)
                for idx, value in enumerate(row))

db.row_factory = make_dicts

或者更簡單的:

db.row_factory = sqlite3.Row

此外,把得到游標,執(zhí)行查詢和獲得結果組合成一個查詢函數(shù)不失為一個好辦法:

def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

上述的方便的小函數(shù)與行工廠聯(lián)合使用與使用原始的數(shù)據(jù)庫游標和連接相比要方便多了。

可以這樣使用上述函數(shù):

for user in query_db('select * from users'):
    print user['username'], 'has the id', user['user_id']

只需要得到單一結果的用法:

user = query_db('select * from users where username = ?',
                [the_username], one=True)
if user is None:
    print 'No such user'
else:
    print the_username, 'has the id', user['user_id']

如果要給 SQL 語句傳遞參數(shù),請在語句中使用問號來代替參數(shù),并把參數(shù)放在一個列表中 一起傳遞。不要用字符串格式化的方式直接把參數(shù)加入 SQL 語句中,這樣會給應用帶來 SQL 注入的風險。

初始化模式

關系數(shù)據(jù)庫是需要模式的,因此一個應用常常需要一個 schema.sql 文件來創(chuàng)建 數(shù)據(jù)庫。因此我們需要使用一個函數(shù)來其于模式創(chuàng)建數(shù)據(jù)庫。下面這個函數(shù)可以完成這個任務:

def init_db():
    with app.app_context():
        db = get_db()
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())
        db.commit()

可以使用上述函數(shù)在 Python 解釋器中創(chuàng)建數(shù)據(jù)庫:

>>> from yourapplication import init_db
>>> init_db()

在 Flask 中使用 SQLAlchemy

許多人喜歡使用 SQLAlchemy 來訪問數(shù)據(jù)庫。建議在你的 Flask 應用中使用包來代替 模塊,并把模型放入一個獨立的模塊中(參見大型應用 )。雖然這不是必須的,但是很有用。

有四種 SQLAlchemy 的常用方法,下面一一道來:

Flask-SQLAlchemy 擴展

因為 SQLAlchemy 是一個常用的數(shù)據(jù)庫抽象層,并且需要一定的配置才能使用,因此我們?yōu)槟阕隽艘粋€處理 SQLAlchemy 的擴展。如果你需要快速的開始使用 SQLAlchemy ,那么推薦你使用這個擴展。

你可以從 PyPI 下載 Flask-SQLAlchemy 。

聲明

SQLAlchemy 中的聲明擴展是使用 SQLAlchemy 的最新方法,它允許你像 Django 一樣, 在一個地方定義表和模型然后到處使用。除了以下內(nèi)容,我建議你閱讀聲明的官方文檔。

以下是示例 database.py 模塊:

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()

def init_db():
    # 在這里導入定義模型所需要的所有模塊,這樣它們就會正確的注冊在
    # 元數(shù)據(jù)上。否則你就必須在調(diào)用 init_db() 之前導入它們。
    import yourapplication.models
    Base.metadata.create_all(bind=engine)

要定義模型的話,只要繼承上面創(chuàng)建的 Base 類就可以了。你可能會奇怪這里為什么不用理會線程(就像我們在 SQLite3 的例子中一樣使用 g 對象)。 原因是 SQLAlchemy 已經(jīng)用 scoped_session 為我們做好了此 類工作。

如果要在應用中以聲明方式使用 SQLAlchemy ,那么只要把下列代碼加入應用模塊就可以了。 Flask 會自動在請求結束時或者應用關閉時刪除數(shù)據(jù)庫會話:

from yourapplication.database import db_session

@app.teardown_appcontext
def shutdown_session(exception=None):
    db_session.remove()

以下是一個示例模型(放入 models.py 中):

from sqlalchemy import Column, Integer, String
from yourapplication.database import Base

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    email = Column(String(120), unique=True)

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email

    def __repr__(self):
        return '<User %r>' % (self.name)

可以使用 init_db 函數(shù)來創(chuàng)建數(shù)據(jù)庫:

>>> from yourapplication.database import init_db
>>> init_db()

在數(shù)據(jù)庫中插入條目示例:

>>> from yourapplication.database import db_session
>>> from yourapplication.models import User
>>> u = User('admin', 'admin@localhost')
>>> db_session.add(u)
>>> db_session.commit()

查詢很簡單:

>>> User.query.all()
[<User u'admin'>]
>>> User.query.filter(User.name == 'admin').first()
<User u'admin'>

人工對象關系映射

人工對象關系映射相較于上面的聲明方式有優(yōu)點也有缺點。主要區(qū)別是人工對象關系映射 分別定義表和類并映射它們。這種方式更靈活,但是要多些代碼。通常,這種方式與聲明 方式一樣運行,因此請確保把你的應用在包中分為多個模塊。

示例 database.py 模塊:

from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
metadata = MetaData()
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
def init_db():
    metadata.create_all(bind=engine)

就像聲明方法一樣,你需要在請求后或者應用環(huán)境解散后關閉會話。把以下代碼放入你的應用模塊:

from yourapplication.database import db_session

@app.teardown_appcontext
def shutdown_session(exception=None):
    db_session.remove()

以下是一個示例表和模型(放入 models.py 中):

from sqlalchemy import Table, Column, Integer, String
from sqlalchemy.orm import mapper
from yourapplication.database import metadata, db_session

class User(object):
    query = db_session.query_property()

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email

    def __repr__(self):
        return '<User %r>' % (self.name)

users = Table('users', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50), unique=True),
    Column('email', String(120), unique=True)
)
mapper(User, users)

查詢和插入與聲明方式的一樣。

SQL 抽象層

如果你只需要使用數(shù)據(jù)庫系統(tǒng)(和 SQL )抽象層,那么基本上只要使用引擎:

from sqlalchemy import create_engine, MetaData

engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
metadata = MetaData(bind=engine)

然后你要么像前文中一樣在代碼中聲明表,要么自動載入它們:

users = Table('users', metadata, autoload=True)

可以使用 insert 方法插入數(shù)據(jù)。為了使用事務,我們必須先得到一個連接:

>>> con = engine.connect()
>>> con.execute(users.insert(), name='admin', email='admin@localhost')

SQLAlchemy 會自動提交。

可以直接使用引擎或連接來查詢數(shù)據(jù)庫:

>>> users.select(users.c.id == 1).execute().first()
(1, u'admin', u'admin@localhost')

查詢結果也是類字典元組:

>>> r = users.select(users.c.id == 1).execute().first()
>>> r['name']
u'admin'

你也可以把 SQL 語句作為字符串傳遞給 execute() 方法:

>>> engine.execute('select * from users where id = :1', [1]).first()
(1, u'admin', u'admin@localhost')

關于 SQLAlchemy 的更多信息請移步其官方網(wǎng)站

上傳文件

是的,這里要談的是一個老問題:文件上傳。文件上傳的基本原理實際上很簡單,基本上是:

  1. 一個帶有 enctype=multipart/form-data 的
    標記,標記中含有一個 。
  2. 應用通過請求對象的 files 字典來訪問文件。
  3. 使用文件的 save() 方法把文件永久 地保存在文件系統(tǒng)中。

簡介

讓我們從一個基本的應用開始,這個應用上傳文件到一個指定目錄,并把文件顯示給用戶。 以下是應用的引導代碼:

import os
from flask import Flask, request, redirect, url_for
from werkzeug import secure_filename

UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

首先我們導入了一堆東西,大多數(shù)是淺顯易懂的。werkzeug.secure_filename() 會在稍后解釋。UPLOAD_FOLDER 是上傳文件要儲存的目錄,ALLOWED_EXTENSIONS 是允許上傳的文件擴展名的集合。接著我們給應用手動添加了一個 URL 規(guī)則。一般現(xiàn)在不會做這個,但是為什么這么做了呢?原因是我們需要服務器(或我們的開發(fā)服務器)為我們提供 服務。因此我們只生成這些文件的 URL 的規(guī)則。

為什么要限制文件件的擴展名呢?如果直接向客戶端發(fā)送數(shù)據(jù),那么你可能不會想讓用戶上傳任意文件。否則,你必須確保用戶不能上傳 HTML 文件,因為 HTML 可能引起 XSS 問題(參見跨站腳本攻擊(XSS) )。如果服務器可以執(zhí)行 PHP 文件,那么還必須確保不允許上傳 .php 文件。但是誰又會在服務器上安裝 PHP 呢,對不? :)

下一個函數(shù)檢查擴展名是否合法,上傳文件,把用戶重定向到已上傳文件的 URL:

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('uploaded_file',
                                    filename=filename))
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form action="" method=post enctype=multipart/form-data>
      <p><input type=file name=file>
         <input type=submit value=Upload>
    </form>
    '''

那么 secure_filename() 函數(shù)到底是有什么用?有一條原則是“ 永遠不要信任用戶輸入”。這條原則同樣適用于已上傳文件的文件名。所有提交的表單數(shù)據(jù) 可能是偽造的,文件名也可以是危險的。此時要謹記:在把文件保存到文件系統(tǒng)之前總是要 使用這個函數(shù)對文件名進行安檢。

進一步說明

你可以會好奇 secure_filename() 做了哪些工作,如果不使用 它會有什么后果。假設有人把下面的信息作為 filename 傳遞給你的應用:

filename = "../../../../home/username/.bashrc"

假設 ../ 的個數(shù)是正確的,你會把它和 UPLOAD_FOLDER 結合在一起,那么用戶 就可能有能力修改一個服務器上的文件,這個文件本來是用戶無權修改的。這需要了解 應用是如何運行的,但是請相信我,黑客都是病態(tài)的 :)

現(xiàn)在來看看函數(shù)是如何工作的:

>>> secure_filename('../../../../home/username/.bashrc')
'home_username_.bashrc'

現(xiàn)在還剩下一件事:為已上傳的文件提供服務。 Flask 0.5 版本開始我們可以使用一個 函數(shù)來完成這個任務:

from flask import send_from_directory

@app.route('/uploads/<filename>')
def uploade
下一篇:部署方式