Flask Web開発学習稿(三)


第六章電子メール
特定のイベントが発生したときにユーザーに注意する必要がある場合、smtplibをパッケージしたFlask-Mail拡張機能は、Flaskとpip install flask-mail Flask-Mailを統合してSMTPサーバに接続することができ、構成しない場合、Flask-Mailはlocalhostのポート25に接続されます.
コンフィギュレーション
デフォルト
説明
MAIL_SERVER
localhost
Emailサーバのipアドレスまたはホスト名
MAIL_PORT
25
Emailサーバポート
MAIL_USE_TLS
False
トランスポート・レイヤ・セキュリティ・プロトコルの有効化
MAIL_USE_SSL
False
コンドーム接続プロトコルの有効化
MAIL_USERNAME
None
Emailユーザー名
MAIL_PASSWORD
None
Emailパスワード
外部SMTPサーバの使用がより便利
from flask.ext.mail import Mail

app.config['MAIL_SERVER'] = 'mail.xxx.com'
app.config['MAIL_PORT'] = '587'
app.config['MAIL_USE_TLS'] = 'True'
app.config['MAIL_USERNAME'] = 'username'
app.config['MAIL_PASSWORD'] = 'pwd'
mail = Mail(app)

アカウントとパスワードをプログラムに書くのは安全ではありません.機密情報を保護するために、スクリプトを環境変数からインポートする必要があります.
app.config['MAIL_USERNAME'] = os.environ.get('MALI_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MALI_PASSWORD')

環境変数の設定方法
# Linux    Mac OS X
export MALI_USERNAME=<YOU_USERNAME>
export MALI_PASSWORD=<YOU_PASSWORD>
# Windows 
set MALI_USERNAME=<YOU_USERNAME>
set MALI_PASSWORD=<YOU_PASSWORD>

プログラム内のEメール送信機能の統合
from flask.ext.mail import Message

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <[email protected]>'

def send_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    mail.send(msg)

この2つのプログラム固有の構成項目は、メールトピックの接頭辞と送信者のアドレスをそれぞれ定義するsend_email()関数のパラメータが、それぞれ受信者アドレス、トピック、メール本文のテンプレートとキーワードパラメータリストでテンプレートを指定する際に拡張子を含めることはできません.これにより、2つのテンプレートを使用して、純粋なテキスト本文とリッチテキスト本文の呼び出し元をそれぞれレンダリングし、テンプレートで使用するためにキーワードパラメータをrender_template()関数に渡し、電子メール本文を生成します.次に、ビュー関数を変更します.
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
#...
@app.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 app.config['FLASKY_ADMIN']:
                send_email(app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True

        session['name'] = form.name.data
        form.name.data = ''

        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False))

純粋なテキストとHTML版のメール本文をレンダリングするための2つのテンプレートファイルを作成します.この2つのテンプレートファイルは、通常のテンプレートと区別するためにtmplatesフォルダの下のmailサブフォルダに保存されます.電子メールのテンプレートにユーザーがテンプレートパラメータを持つ必要があるためsend_を呼び出すmail()関数をキーワードパラメータとしてユーザーに渡すというプログラムは、メールを送信する際に一時的にブロックされ、電子メールを非同期で送信して不要な遅延を解消します.
from threading import Thread

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

def send_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr

多くのFlask拡張では、アクティブ化されたプログラムコンテキストと要求コンテキストがすでに存在すると仮定しています.Flask-Mailのsend()関数はcurrent_appを使用します.そのため、プログラムコンテキストをアクティブ化する必要がありますが、異なるスレッドでmail.send()関数を実行する場合、プログラムコンテキストはapp.app_context()を使用して手動で作成します.Eメールを大量に送信する必要がある場合、Celeryタスクキューの使用が適切
第七章大型プログラムの構造
多くの機能の学習を完了しましたが、プログラムが大きくなるにつれて、大規模なプログラムの構造をどのように組織するかを学びます.
7.1プロジェクト構造
├─Flsky
│  │─app  # Flask   | ├─static| |─templates| |─main| | │-__init__.py
│  │  | |-errors.py
│  │  | |-forms.py
│  │  | |-views.py
│  │  |-__init__.py
│  │  |-email.py
│  │  |-models.py
| |-migrations #        
| |-tests #     
| | |-__init__.py
| | |-test*.py|-config.py #     |-manage.py #                |-requirements.txt #        
| └─ venv # python    

7.2構成オプション
これからは、以前のように簡単な辞書構造ではなく、構成クラスを使用します.
#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
}

構成クラスは、プログラムインスタンスであるinit_app()メソッドを定義することができ、このメソッドでは、現在の環境の構成の初期化を実行することができ、ベースクラスConfigのinit_app()メソッドが空になります.
7.3パッケージ
パッケージは、プログラムのすべてのコード、テンプレート、および静的ファイルを保存するために使用されます.
7.3.1プログラムファクトリ関数の使用
作成プロセスを明示的に呼び出すことができるファクトリ関数に移動すると、プログラムのファクトリ関数はappパッケージのコンストラクションファイルで定義されたコンストラクションファイルに使用中のFlask拡張子が多すぎて、まだ初期化に必要なプログラムインスタンスがないため、初期化拡張がなく、拡張クラスを作成する際にコンストラクション関数にパラメータ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

7.3.2ブルーブックでプログラムの機能を実現する
単一スクリプトプログラムでは、プログラムインスタンスはグローバルな役割ドメインに存在し、ルーティングはappを直接使用することができる.route修飾器定義.しかし、現在、アプリケーション・インスタンスは実行時に作成され、app.routecreate_app()以降にのみ存在します.この場合、定義ルーティングが遅すぎて、ブルーブックに登録されているルーティングはスリープ状態にあり、ブルーブックがプログラムに登録されるまで、ルーティングは本当にプログラムの一部になります.グローバルな役割ドメインにあるブルーブックを使用する場合、ルーティングを定義する方法は、単一のスクリプトプログラムとほぼ同じです.柔軟性を最大化するために、パッケージにはブルーブックを保存するためのサブパッケージが作成されています.
app/main/__init__.py
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors

ブルーブッククラスオブジェクトをインスタンス化することでブルーブックを作成できます.コンストラクション関数には、ブルーブックの名前とブルーブックが存在するモジュールまたはパッケージの2つのパラメータがあります.多くの場合、Pythonの__name__変数は、2番目のパラメータに必要な値アプリケーションのルーティングがapp/main/views.pyモジュールに、エラー処理がapp/main/errors.pyモジュールに配置されます.これらのモジュールをインポートすると,ルーティングとエラー処理がブルーブックに関連付けられる.viewsのため、ルーティングおよびエラー処理モジュールはapp/__init__.pyの下部に導入されることに注意する.pyとerrors.pyはmain blueprintをインポートするので、ループ依存を避けるために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/error.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()の使い方が異なる
  • Flaskは、ブルーブックのすべてのエンドポイントにネーミングスペースを追加します.これにより、競合することなく、異なるブルーブックで同じエンドポイント名を使用してビュー関数を定義できます.ネーミングスペースは、ブルーブックの名前(ブルーブックコンストラクタの最初のパラメータ)であるため、ビュー関数index()に登録されているエンドポイント名はmain.inedxであり、そのURLはurl_for('main.index')を用いてプログラムを完全に修正するためのページを取得し、フォームオブジェクトもブルーブックに移動し、app/main/forms.pyモジュールに保存する
    7.4起動スクリプト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変数を読み出し、変数が定義されていない場合はデフォルト値を使用します.次に、Flask-ScriptFlask-Migrate、およびPython Shellに定義されたコンテキストを初期化する.
    7.5需要書類
    pipは、次のコマンドを使用して、このファイルpip freeze >requirements.txtを自動的に生成することができ、別の環境でこれらの依存をインストールする準備ができたら、次のコマンドpip install -r requirements.txtを実行します.
    7.6ユニットテスト
    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'])

    テストは典型的なユニットテストの書き方に従って構築され、実行中のプログラムと同様に、まずテスト構成を使用してプログラムを作成し、コンテキストをアクティブにします.setUp()およびtearDown()メソッドは、各テストメソッドの実行前後に実行され、test_で始まるメソッドは、テストメソッドとして実行されます.setUp()メソッドは、テストに必要な環境を作成します.彼はまず、アプリケーションインスタンスをテストとして使用する山下文環境を作成します.これにより、テストがcurrent_appに取得され、新しいデータベースが作成されます.データベースおよびアプリケーション・インスタンスは、tearDown()メソッドで最後に破棄されます.
    最初のテストでは、アプリケーション・インスタンスが存在することを確認し、2番目のテスト・アプリケーション・インスタンスがテスト構成で実行されます.テストフォルダに正しいパッケージ構造があることを確認するために、tests/__init__.pyファイルを追加する必要があります.これにより、ユニットテストパッケージはテストフォルダ内のモジュールをすべてスキャンすることができます.
    ユニットテストを実行するためにmanage.pyにカスタムコマンドを追加し、
    @manager.command
    def test():
        """Run the unit tests."""
        import unittest
        tests = unittest.TestLoader().discover('tests')
        unittest.TextTestRunner(verbosity=2).run(tests)

    運転方法は以下の通りです.
    (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.7データベースの作成
    環境変数からデータベースのURLを読み込むことを優先します.また、次のコマンドを使用してデータテーブルを作成したり、最新のリビジョンpython mange.py db upgradeにアップグレードしたりするためのデフォルトのSQLiteデータベースも用意されています.