どこにでもいるSEの備忘録

たぶん動くと思うからリリースしようぜ

Flaskのチュートリアルをやってみる

f:id:nogawanogawa:20190326203754p:plain
以前こんなんをやっていました。

nogawanogawa.hatenablog.com

nogawanogawa.hatenablog.com

やってて思うのは、意外とFlaskって紹介記事が少ないんです。特に日本語。

ということで、正攻法で勉強していくしかないと思います。 今回は公式のチュートリアルをなぞって、正しい書き方を勉強したいと思います

公式HPはこちら。

flask.pocoo.org

やるとこ

ここだけ見ていきます。

ここさえ写経すればあとは必要に応じて参照すれば問題ないでしょう。 全部なぞるわけではないですが、なんとなく一通り読んでいきます。

Quickstart

HelloWorld的な何か

from flask import Flask
app = Flask(__name__)

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

このシンプルなコードでやっているのは下記の4つです。

  1. まず、 Flask クラスをインポートしました。このクラスのインスタンスWSGIアプリケーションです。 まず、このアプリケーションのモジュール名について述べます。 もしあなたが使うモジュールが一つだけなら、 name を使わなければなりません。 それがアプリケーションとして起動したときとモジュールとしてインポートされたときで名前が異なるからです (アプリケーションとして起動したときは 'main' 、インポートされたときはそのインポート名)。 もっと詳しく知りたければ、 Flask のドキュメントを参照してください。
  2. 次にインスタンスを生成します。モジュールやパッケージの名前は要りません。 このインスタンスはFlaskがテンプレートファイルやスタティックファイルなどをどこから探すのかを 認識するために必要です。
  3. route() デコレータを使用し、ファンクションを起動するURLをFlaskに教えます。
  4. そのファンクション名は特定のファンクションに対してURLを生成するためにも使われ、 ファンクションはユーザーのブラウザ上で表示したいメッセージを返します。

(参考:https://a2c.bitbucket.io/flask/quickstart.html)

と、こんな感じです。 Flask1.0系だと実行の方法が以前と違うので、こんな感じで実行する必要があります。

$ export FLASK_APP=hello.py
$ flask run
 * Running on http://127.0.0.1:5000/ 

これだと外部からはアクセスできないのでご注意を。 外部PCからアクセス可能にするには--host=0.0.0.0のオプションをつけます。

$ export FLASK_APP=hello.py
$ flask run --host=0.0.0.0
 * Running on http://127.0.0.1:5000/ 

これでOSにパブリックIPを参照するように通知します。

routing

エンドポイントを指定して、エンドポイントに対する処理を記述します。

@app.route('/')
def index():
    return 'Index Page'

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

@app.route()でエンドポイントを指定し、その直下のメソッドにエンドポイントアクセス時の処理を記述します。

変数ルール

エンドポイントの値の動的読み取りが可能になっている。

@app.route('/user/<username>')
def show_user_profile(username):
    # show the user profile for that user
    pass

@app.route('/post/<int:post_id>')
def show_post(post_id):
    # show the post with the given id, the id is an integer
    pass

この場合だと、エンドポイント末尾にusernameに指定した場合に、usernameを変数としてメソッドの引数にすることが可能になっている。 下の例では、型も指定している。このようにurlの一部を変数として利用できる。

リダイレクト

エンドポイントの末尾に"/"(スラッシュ)を入れるかどうかで挙動が変わる。

@app.route('/projects/')
def projects():
    pass

@app.route('/about')
def about():
    pass

上の例では、スラッシュなしでアクセスすると、強引にスラッシュありにリダイレクトされる。 下の例だと、ファイル同じように認識されるため、スラッシュ付きでアクセスしようとすると404エラーになる。

HTTP メソッド

HTTPにはGETやPOSTといったメソッドが分かれています。 methodにそれらを指定することが可能です。

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        do_the_login()
    else:
        show_the_login_form()

このように、request.methodでメソッドの種類を取得可能です。

リクエストオブジェクト

リクエストにくっついて来るオブジェクトを使用するときには requestをimportします。

from flask import request

使い方はこんな感じ。

@app.route('/login', methods=['POST', 'GET'])
def login():
    error = None
    if request.method == 'POST':
        if valid_login(request.form['username'],
                       request.form['password']):
            return log_the_user_in(request.form['username'])
        else:
            error = 'Invalid username/password'
    # this is executed if the request method was GET or the
    # credentials were invalid

requestに格納されているオブジェクトに.formでアクセスしています。

session

セッション情報は session というオブジェクトをimportすることで使用可能です。

from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)

@app.route('/')
def index():
    if 'username' in session:
        return 'Logged in as %s' % escape(session['username'])
    return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        session['username'] = request.form['username']
        return redirect(url_for('index'))
    return '''
        <form action="" method="post">
            <p><input type=text name=username>
            <p><input type=submit value=Login>
        </form>
    '''

@app.route('/logout')
def logout():
    # remove the username from the session if its there
    session.pop('username', None)
    return redirect(url_for('index'))

# set the secret key.  keep this really secret:
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'

Tutorial

チュートリアルでは、”Flaskr”というアプリケーションを作成します。 日本語のチュートリアルと最新(1.0.2)の英語版のチュートリアルで内容がかなり違うのでご注意ください今回は最新のチュートリアルをやっていきます。

最新の完全なコードはこちらだそうです。

github.com

ディレクトリ階層

最終的にできるディレクトリ階層はこんな感じです。

/home/user/Projects/flask-tutorial
├── flaskr/
│   ├── __init__.py
│   ├── db.py
│   ├── schema.sql
│   ├── auth.py
│   ├── blog.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── auth/
│   │   │   ├── login.html
│   │   │   └── register.html
│   │   └── blog/
│   │       ├── create.html
│   │       ├── index.html
│   │       └── update.html
│   └── static/
│       └── style.css
├── tests/
│   ├── conftest.py
│   ├── data.sql
│   ├── test_factory.py
│   ├── test_db.py
│   ├── test_auth.py
│   └── test_blog.py
├── venv/
├── setup.py
└── MANIFEST.in

セットアップ

では、まずflask-tutorialというプロジェクトディレクトリの中に、'flaskr'というディレクトリを作成します。

次に、flaskr/init.pyにアプリケーションファクトリPythonにパッケージの範囲を通知する旨を記述します。

以下のようにコーディングします。

import os

from flask import Flask


def create_app(test_config=None):
    # create and configure the app
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
    )

    if test_config is None:
        # load the instance config, if it exists, when not testing
        app.config.from_pyfile('config.py', silent=True)
    else:
        # load the test config if passed in
        app.config.from_mapping(test_config)

    # ensure the instance folder exists
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # a simple page that says hello
    @app.route('/hello')
    def hello():
        return 'Hello, World!'

    return app

create_appがアプリケーションファクトリ関数です。

  1. app = Flask(name, instance_relative_config=True)
  2. app.config.from_mapping()
    • いくつかのデフォルトの設定を読み込みます。
  3. app.config.from_pyfile()
    • config.pyから取得した設定値をオーバーライドします。
  4. os.makedirs()
  5. @app.route()
    • エンドポイントを設定します

アプリケーションを実行してみます。

export FLASK_APP=flaskr
export FLASK_ENV=development
flask run

 * Serving Flask app "flaskr" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 273-900-170

http://127.0.0.1:5000/helloにアクセスすると、HelloWorldが見れるのでは無いでしょうか?

データベース接続

こんどはDB(SQLite3)に接続します。

flaskr/db.pyを作成して、以下のように記述します。

import sqlite3

import click
from flask import current_app, g
from flask.cli import with_appcontext


def get_db():
    if 'db' not in g:
        g.db = sqlite3.connect(
            current_app.config['DATABASE'],
            detect_types=sqlite3.PARSE_DECLTYPES
        )
        g.db.row_factory = sqlite3.Row

    return g.db


def close_db(e=None):
    db = g.pop('db', None)

    if db is not None:
        db.close()

gは特殊なオブジェクトで、リクエストごとに固有なオブジェクトとなっています。 ここでは、同じリクエストの中でDBアクセスが複数発生する際にコネクションを一度だけ取得するようにしています。

current_appも特殊なオブジェクトで、Flaskアプリケーションがリクエストをコントロールするために使用されます。 リクエストを排他制御するために使用されます。

DBの初期化にはflaskr/schema.sqlを作成します。

DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;

CREATE TABLE user (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL
);

CREATE TABLE post (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  author_id INTEGER NOT NULL,
  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  FOREIGN KEY (author_id) REFERENCES user (id)
);

flaskr/db.pyに下記を追加します。

def init_db():
    db = get_db()

    with current_app.open_resource('schema.sql') as f:
        db.executescript(f.read().decode('utf8'))


@click.command('init-db')
@with_appcontext
def init_db_command():
    """Clear the existing data and create new tables."""
    init_db()
    click.echo('Initialized the database.')

close_db とinit_db_command関数をアプリケーションインスタンスに登録します。 flaskr/db.pyに下記を追加します。

def init_app(app):
    app.teardown_appcontext(close_db)
    app.cli.add_command(init_db_command)

最後にflaskr/init.pyに追記します。

from . import db
    db.init_app(app)

    return app

DBの初期化は下記のようにコマンドを実行します。

$flask init-db

Initialized the database.
flask-tutorial 

Blueprints と Views

Blueprintは関連するViewとコードを連携する手法です。

flaskr/auth.pyに下記のように記述します。

import functools

from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

これによって、authという名のBlueprintが作成されました。 flaskr/init.pyに登録します。

def create_app():
    app = ...
    # existing code omitted

    from . import auth
    app.register_blueprint(auth.bp)

    return app

次に、登録画面を作ります。

flaskr/auth.pyに下記を追記します。

@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None

        if not username:
            error = 'Username is required.'
        elif not password:
            error = 'Password is required.'
        elif db.execute(
            'SELECT id FROM user WHERE username = ?', (username,)
        ).fetchone() is not None:
            error = 'User {} is already registered.'.format(username)

        if error is None:
            db.execute(
                'INSERT INTO user (username, password) VALUES (?, ?)',
                (username, generate_password_hash(password))
            )
            db.commit()
            return redirect(url_for('auth.login'))

        flash(error)

    return render_template('auth/register.html')

次に、Loginに関する挙動を登録します。 再びflaskr/auth.pyに下記を追記します。

@bp.route('/login', methods=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):
            error = 'Incorrect password.'

        if error is None:
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

最後にログアウトの挙動を記載します。 flaskr/auth.pyに下記を追記します。

@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

登録・ログイン周りができたところで、ログイン制約を付けます。

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view

テンプレートの使用

ベースレイアウトはこんな感じです。

flaskr/templates/base.html

<!doctype html>
<title>{% block title %}{% endblock %} - Flaskr</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
  <h1>Flaskr</h1>
  <ul>
    {% if g.user %}
      <li><span>{{ g.user['username'] }}</span>
      <li><a href="{{ url_for('auth.logout') }}">Log Out</a>
    {% else %}
      <li><a href="{{ url_for('auth.register') }}">Register</a>
      <li><a href="{{ url_for('auth.login') }}">Log In</a>
    {% endif %}
  </ul>
</nav>
<section class="content">
  <header>
    {% block header %}{% endblock %}
  </header>
  {% for message in get_flashed_messages() %}
    <div class="flash">{{ message }}</div>
  {% endfor %}
  {% block content %}{% endblock %}
</section>

その他画面は下記のようになります。

flaskr/templates/auth/register.html

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Register">
  </form>
{% endblock %}

flaskr/templates/auth/login.html

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Log In">
  </form>
{% endblock %}

静的ファイル

最後にスタイルシートを作成します。 flaskr/static/style.cssに下記を記述します。

html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul  { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }

実行

実行してみます。

flask-tutorial $flask run

http://127.0.0.1:5000/auth/loginにアクセスすればこんな感じのログイン画面が出るかと思います。

f:id:nogawanogawa:20190428185858p:plain

公式では、こんな感じに書いていくみたいですね。

この先もうちょっと続きますが、興味あるところは見れたので今回はこのへんで。。。

感想

駆け足でサラッと流し読みしましたが、基本的な部分は大体抑えられたんではなかろうか?と思ってます。 今まで書いていたのは若干古い書き方のようなので、これからはお行儀の良いFlaskコードを書いていくようにしたいと思います。

そうはいってもシンプルなので、すぐ書けますね。 これぐらい簡単だと使いやすくていいですね。