tachitomonn’s blog

IT技術関連の学習メモがメインでたまに趣味のこととか

Python Flask Tutorial で読書ログアプリーケーションを作ってみた 前編

公式チュートリアルを写経に利用しつつ、ただの写経はつまらないので読書ログアプリケーションを作ってみた。

Tutorial — Flask Documentation (1.1.x)

プロジェクトのレイアウト

プロジェクト用のディレクトリを作成したら、その中に venv の仮想環境用ディレクトリを作成します。
作成した仮想環境に Flask をインストールする。プロジェクト用のディレクトリには以下が含まれることになる。

  • アプリケーションのコード、ファイルを含むアプリケーション本体のパッケージディレクト
  • テスト用モジュールを含むディレクト
  • Flask とその他プロジェクトに必要なモジュールやパッケージをインストールする仮想環境ディレクト
  • プロジェクト配布用の setup.py
  • git のようなバージョンコントロール用の設定ファイル

バージョンコントロールの対象外にすべきファイル群は無視するように設定します( git なら.gitignore に記述する)

アプリケーションのセットアップ

Flask のアプリケーションは Flask クラスのインスタンスでした。
Flask のアプリケーションの最も単純なものはコードのトップレベルのグローバルな Flask インスタンスをつくったもの。
でもプロジェクトが大きくなると管理が煩雑になるのでグローバルな Flask インスタンスの代わりに関数の中でインスタンスを生成するようにします。
この関数は application factory として知られている。
プロジェクト用のディレクトリ中にアプリケーション本体のパッケージ用ディレクトリをつくり、 __init__.py を作成して入れておきます。__init__.py には application factory を含むこととディレクトリをパッケージとして扱わせる役割がある。

__init__.py

#! /usr/bin/env python
# -*- conding: utf8 -*-

import os

from flask import Flask

def create_app(test_config=None):
    # アプリケーションを生成&デフォルト設定
    app = Flask(__name__, instance_relative_config=True) #設定ファイルはパッケージ外のインスタンスフォルダにあることを明示
    # デフォルト設定
    app.config.from_mapping(
            SECRET_KEY = "dev",
            DATABASE = os.path.join(app.instance_path, "readinglog.sqlite"),
            )
    if test_config is None:
        # テストでなく、設定が存在すればインスタンス設定の読み込み
        app.config.from_pyfile("config.py", silent=True)
    else:
        # テスト設定が渡された場合はテスト設定の読み込み
        app.config.from_mapping(test_config)
    # インスタンスフォルダの存在チェック(自動的には作られない)
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass
    # Hello, World!
    @app.route("/hello")
    def hello():
        return "Hello World!"
    return app

ここまででアプリケーションを動かしてみます。

> set FLASK_APP=readinglog
> set FLASK_ENV=development
> flask run

http://127.0.0.1:5000/hello にブラウザでアクセスして Hello World! が表示されれば OK!

データベースの定義とアクセス

アプリケーションのユーザーとポストされたデータの保存には SQLite を使います。 SQLite はお手軽だけどある程度の規模のアプリケーションを作るなら MySQL とか別の DB を使おうね。
パッケージ内に db.py をつくります。

db.py

#! /usr/bin/env python
# -*- coding: utf8 -*-

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 はリクエスト毎にユニークな特殊なオブジェクトです。同一のリクエスト内で二回目の get_db が呼ばれても新たなコネクションは作らず保存されたコネクションが再利用されるようにしている。
current_app も特殊なオブジェクトでリクエストをハンドリングする Flask アプリケーションを示している。
続いてアプリケーションに必要なテーブルを作成するクエリを記述する schema.sql をパッケージ内につくります。

schema.sql

DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS book_shelf;

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

CREATE TABLE book_shelf (
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	owner_id INTEGER NOT NULL,
	created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	isbn BIGINT(13) NOT NULL,
	status INTEGER NOT NULL DEFAULT 0,
	evaluation INTEGER DEFAULT NULL,
	finished TIMESTAMP DEFAULT NULL,
	FOREIGN KEY (owner_id) REFERENCES user (id)
);

さらに db.py にテーブル作成クエリを発行する関数を追加します。

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.")

open_resource() でアプリケーションパッケージに関連するファイルを開く(ファイルのパスを把握していなくても良いので楽)。click.command() は init-db というコマンドをコマンドラインの有効なコマンドとして定義している。

続いて close_db と init_db_command 関数をアプリケーションのインスタンスと紐づけていきます。
db.py に init_app 関数を実装します。

db.py

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

app.teardown_appcontext() はレスポンスを返した後に呼ばれる関数を登録することができる。
app.cli.add_command() は新規の flask コマンドとして追加することができる。

init_app 関数を application factory から呼ぶようにします。
__init__.py の app を返す直前に追加します。

__init__.py

    from . import db
    db.init_app(app)
    return app

init_db がアプリケーションに登録されたので flask コマンドとして呼んでみます。

 > flask init-db

プロジェクトディレクトリ内の instance ディレクトリ内に readinglog.sqlite ファイルが生成されるはず。