LoginSignup
0
0

Flask - 個人的NOTE / 公式DOC>Tutorial (AppSetup~BluePrint) 1/3

Last updated at Posted at 2024-05-19

公式のTutorialを読んで新しく学んだことをメモ書きしてます。
初心者には重要なポイントや公式でわかりづらいとこも補完しました。
読みづらいのは個人用につき勘弁いただきたい

Application setup

Flaskのインスタンスの生成はスクリプトの最上部にグローバルインスタンスとして書くこともできるが、アプリが複雑化するとこの方法はトラブルの種になりやすい。

そこでFlaskのインスタンスは関数の中で生成して実装させる。この様な関数をApplication Factoryと呼ぶ

Application Factory

  • __init__.py スクリプトとは
    1. application factoryとしての役割
    2. Pythonに所属フォルダをpackage(他のスクリプトからのimport可能)として認識させる
__init__.py
import os
from flask import Flask

def create_app(test_config=None):
    # appインスタンスの生成と設定
    app = Flask(__name__, instance_relative_config=True) # check, instance_relative_config
    app.config.from_mapping(
        SECRET_KEY = 'dev',
        DATABASE = os.path.join(app.instance_path, 'flaskr.sqlite')
    )
    
    if test_config is None:
        # test状態ではなく、configインスタンスが存在する場合はconfigインスタンスを読み込む
        app.config.from_pyfile('config.py', silent=True) # check, from_pyfile
    else:
        # test_configが渡されていたら読み込み
        app.config.from_mapping(test_config)

    # フォルダの存在を確認する
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # hello出力
    @app.route('/hello')
    def hello():
        return 'Hello, World!!'
    return app

create_app関数の処理

Flask(__name__, instance_relative_config=True)

  • __name__ : Pythonモジュールの現在の名前を示します。path設定においても現在地を指定する必要があります
  • instance_relative_config=True :
  • configrationファイルがinstance folderに対して相対的であることを伝える
    • Instance folder
      • flaskrフォルダの外側にあり、バージョン管理システム(git)に反映されるべきでは無いデータを保持することを目的としたフォルダ
      • ex. 中身 => conficration secrets, database files

TODO : Instance_relative_configの目的をもう少し咀嚼して理解する

  • app.config.from_mapping() : デフォルトのアプリ設定を行います

    • SECRET_KEY : Flaskと拡張機能で利用される。データを安全に維持するvalが開発時は楽。デプロイ時はランダムで複雑な値に変更
    • DATABASE : SQLiteのファイルが存在するpath, app.instance_pathの下にある(Flaskがインスタンスフォルダーとして選んだpath)
  • app.config.from_pyfile()

    • config.pyファイルがある場合、取得した設定をデフォルトの設定に上書きする関数
    • test_configを使うことで、後から開発するtestコードを独立して実装することができる
  • osmakedirs()

    • app.instance_pathの存在を保証?する
    • 今回SQLiteファイルを使うのでフォルダを動的に作らせる必要がある
  • @app.route()

    • URL hello/responseを作る関数繋ぐためのルートを作成する

Run The Application

  • Debug modeでflaskを起動
  • flask-tutorialのディレクトリの中にて実行
    flask --app flaskr run --debug
実行
~/De/d/flask_d/t/flask-tutorial > flask --app flaskr run --debug
 * Serving Flask app 'flaskr'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 571-032-974

image.png

  • Debug modeとは
    • ページに何かしらの例外処理を出した時、コードを変更する時その度にデバッガー画面を表示する

Define and Access the Database

  • Pythonはsqlite3モジュールにSQLiteのサポートを組み込んでいる
  • 一方、同時に書き込みリクエストが送られると順番に実行するから動作が遅くなる。
  • 小規模なアプリだと気にならないが、大きくなるとデータベースを変えたくなるかもしれない

Databaseへの接続

  • Databaseの種類に限らず、まずはconnectionを作ることが1番最初に行う事である
  • どんなqueryや命令もconnectionを通じて行われて、処理完了後にcloseする
    -  WEBアプリケーションだと、connectionはrequestオブジェクトに紐づけられて,requestオブジェクトの操作において何回か作られる、responseオブジェクトの生成前にcloseされる
db.py
import sqlite3
import click
from flask import current_app, g

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('pop', None)

    if db is not None:
        db.close()

特殊なオブジェクト

gオブジェクト
  • requestの処理の間に複数の関数からアクセスされるデータを保持するオブジェクトである
current_appオブジェクト
  • requestを処理するアプリケーションオブジェクト(app)を指定できるオブジェクト。
  • 使う理由 :
    • アプリケーションファクトリ(create_app関数)を用いている場合、appオブジェクトはコードを書いているときは存在しない。
    • get_db関数はappオブジェクトが生成された後に、requestを処理するときに呼び出される。この現在の状態のappオブジェクトにアクセスするためにcurrent_appオブジェクトを利用する
      • current_app.configから取得できる(例では current_app.config['DATABASE]で作成されたappオブジェクトのDATABASEへのpathを取得している)
current_app & sqlite
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

sqlite3.connect()

  • current_app.config['DATABASE']の記載先のファイルへのPATHを示す。
  • このファイルはデータベースを初期化するまでは存在しない

g.db.row_factory = sqlite3.Row

  • connectionオブジェクトが辞書型の様な振る舞いをする行(sqlite3.Row)として戻り値を返す様に設定している

何故 gオブジェクトのdb値をsqlite.connect(~)の戻り値に置換するのか :

  • gオブジェクトはリクエスト内部でのグローバル変数(ローカルコンテキストオブジェクト)である
  • データベースに関連する処理をする際にg.dbを参照すれば処理の度に回データベースにアクセスする必要がなくなり、リソースの節約とコードの可読性が上がるというメリットがある
  • gオブジェクトのデータはrequest終了時に自動的にクリーンアップされて確実に閉じることができる

close_db(e=None)の処理内容

  • 引数 e = None
    • Pythonの記法として、eにエラーオブジェクトが入ったら、エラーオブジェクトが入力される。
    • Noneはもしeに何も入らなかった場合用のデフォルト値である
    • デフォルト値を指定する事で、キーワード関数を入れなくても実行できる汎用性の高い関数として使うことができる
  • シグナルやフックといった、Request終了時や、アプリケーションのシャットダウン時の特定の関数を自動的に呼び出す機能があり、そのときエラー情報を渡す事ができる様に設計されている
  • @teardown_appontextデコレーターとの併用
from flask import Flask, g

app = Flask(__name__)

@app.teardown_appcontext
def close_db(e=None):
    db = g.pop('db', None)
    if db is not None:
        db.close()
  • teardown_appcontextデコレーター:
    • アプリケーションコンテキストが破棄されるタイミングでclose_dbを呼び出すというもの
    • もし、エラーが発生していたらclose_dbにエラーオブジェクトを渡す事ができる

import click :
click はFlaskにおいてコマンドラインインターフェース(CLI)を作成するためのパッケージである、Flaskでは内部的にClickを使ってflaskコマンドを提供している。

Create the tables

  • Pythonのスクリプト db.py から user, postテーブルを作成するクエリを実行する
  • まず、クエリを持った次のsqlファイルを 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)
);
  • 次にPythonスクリプトを改修する(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')
def init_db_command():
    """Clear the existing data and create new tables."""
    init_db()
    click.echo('Initialized the database.')

with句とは何か
ファイルのリソースを読み込んだ際、必ずclose関数でリソースを解放する必要がある。
try/finallyで書いていたものをwith句で作成したブロックで表現することができる。
参照 : https://blog.mtb-production.info/entry/2018/04/10/183000

filename = 'example.txt'

# with句を使った例
with open(filename, 'r') as f:
    content = f.read()
    print(content)

# try / finallyを使った例
f = open(filename, 'r')
try:
    content = f.read()
    print(content)
finally:
    f.close()
  • open_resource()関数

    • flaskrパッケージに関連するファイルを開く関数です
    • 開発者はアプリケーションをデプロイするときに各ファイルの場所を知る必要がないので便利な処理
    • get_db関数でdatabase connection オブジェクトを取得し、ファイルに書かれたSQLを実行するために利用する
  • click.command()

    • init-dbというコマンドラインを定義する。これはinit_db関数を実行し、成功メッセージを呼び出すというもの
    • => 詳細 ; Command Line Interface https://flask.palletsprojects.com/en/3.0.x/cli/

Reister with the Application

  • close_db, init_db_command関数はアプリケーションインスタンス(app)に登録されないと、アプリケーションに利用されていない。
  • 今回は application factory functionを利用しているので、関数を書き込むときにはインスタンスは利用できない。その代わりにアプリケーションの取得と関数の登録を行う関数を作成する
#------------------------------
        #appインスタンスを取得してappにclose_db,init_db_command関数を登録する関数
#------------------------------
def init_app(app):
    app.teardown_appcontext(close_db) # アプリが終了する際に実行する関数
    app.cli.add_command(init_db_command) # コマンドラインにinit_dbを追加する関数
  • app.teardown_appcontext(close_db)
    • Flaskへresponseオブジェクトを戻して、処理を完了させるときに呼び出す関数を指定している
  • app.cli.add_command()
    • flaskコマンドに新しいコマンド(init_db_command)を追加する

init.pyへの追加処理

  • appインスタンスを生成したときに, db.pyで定義した init_db関数を呼び出させるために以下の処理を追加する
app.py
def create_app(test_config=None):
    # appインスタンスの生成と設定
    app = Flask(__name__, instance_relative_config=True) # check, instance_relative_config
    app.config.from_mapping(
        SECRET_KEY = 'dev',
        DATABASE = os.path.join(app.instance_path, 'flaskr.sqlite')
    )
    
    if test_config is None:
        # test状態ではなく、configインスタンスが存在する場合はconfigインスタンスを読み込む
        app.config.from_pyfile('config.py', silent=True) # check, from_pyfile
    else:
        # test_configが渡されていたら読み込み
        app.config.from_mapping(test_config)

    # フォルダの存在を確認する
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # hello出力
    @app.route('/hello')
    def hello():
        return 'Hello, World!!'
    
    #---------------------------------------------
    #追加した処理
    #init_appにappインスタンスを渡す。 (アプリケーション終了時の関数を登録 + コマンドラインの追加)
    from . import db
    db.init_app(app)
    #---------------------------------------------
    return app
    

次のコマンドを実行するとinit_db関数が実行される
flask --app flaskr init-db

結果

  1. flaskrフォルダの同階層にinstanceフォルダが作成される
  2. instanceフォルダにflaskr.sqlファイルが生成される

上記の処理結果は以下の設定によるものである

__init__.pyの一部
    # appインスタンスの生成と設定
    app = Flask(__name__, instance_relative_config=True) # check, instance_relative_config
    app.config.from_mapping(
        SECRET_KEY = 'dev',
        DATABASE = os.path.join(app.instance_path, 'flaskr.sqlite')
    )
    

flaskrフォルダの同階層にinstanceフォルダが作成される

instance_relative_config=True :
- appインスタンスの生成にて指定するこのパラメータは、app.instance_pathで指定するインスタンスディレクトリの位置を設定します
    - Trueの時(今回) : アプリケーションフォルダの外側に設定します
    - Falseの時 : アプリケーションフォルダ内に設定します
FYI : インスタンスフォルダとは
  • Flask0.8から導入した概念

    • 元々、Flaskはアプリケーションのフォルダからの相対パスを参照できるようにしていた
      • 問題 : アプリケーションがパッケージ化(外部に配布可能・再利用可能)される際に重要なリソースファイルや設定ファイルもパッケージに含まれている必要がありました。
    • リソースファイルや設定ファイルをパッケージに含むことはセキュリティ的にリスクが伴う。
      • : APIキーやデータベースパスワードなどの機密情報がパッケージ内に含まれてしまうと、不特定多数の人に知られてしまう可能性がある
  • インスタンスフォルダ

    • パッケージ外で重要なファイル(リソースファイルや設定ファイル)を管理できる概念
      • アプリケーションコードとは別に配置され、デプロイ固有の設定や実行時に変化するデータを安全に管理するための場所
    • バージョン管理システムには含めないことが推奨される
      • 機密情報や環境ごとの設定ファイルが誤って公開されるリスクを減らす

instanceフォルダにflaskr.sqlファイルが生成される

以下の処理においてファイルが生成される

os.path.join(app.instance_path, 'flaskr.sqlite')

app.instance_pathに従い、Flaskrのプロジェクトフォルダと同階層に配置される

Blueprints and Views

概要

Flaskはrequestに含まれるURLとview functionを照合させるためのパターンを持っている
View functionはFlaskがresponseとして変換されたデータを発信する
逆にURLをそのView functionの名称や引数などに合わせて生成することも可能である。

view function
アプリケーションへリクエストに対するresponseを作成する機能の事

Create a Blueprint

Blueprintとは関連し合うviewやその他のコードのグループである。
アプリケーションに直接それらを登録するのではなく、blueprintに登録することが勧められる。
blueprintがファクトリー関数で利用可能になったとき,アプリケーションへ登録する

今回の開発における2つのblueprint

・Authentication functions
・Blog posts functions

これらのblueprintは異なるモジュールの中に入れられる。開発の順番については、Blog posts functionsは、authentication系(loginなど)の情報が必要なので、まずはauthentication functionsに着手

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を使用している。

  • __name__
    アプリケーションオブジェクトの様にblueprintもどこで自身が定義されているかを知る必要があるため付与された。(中身はモジュール名 auth になる)

  • url_prefix
    このブループリントに統合されたすべてのURLの頭にurl_prefixの値が追加されます

ファクトリーメソッドへblueprintとimportし、登録する

__init__.py
    # blueprintのimportと登録
    app = ~
    from . import auth
    app.register_blueprint(auth.bp)

Authenticationブループリントへは新規ユーザの作成と、ログイン/ログアウトのview functionを追加していく

Blueprintの実装(View function/ バックエンド部分)

アカウント登録処理 auth.py

1 - /auth/registerへのGET request
上記のURLが送られたらユーザへ、入力用のHTMLを返す
2 - POST request
入力値の検証とその後のハンドリングを設定(エラーメッセージ or ログイン画面へ遷移)
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')

@bp.rout('/register',method=('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'
        if error is None:
            try:
                db.execute(
                    "INSERT INTO user (username,password) VALUE (?,?)",
                    (username, generate_password_hash(password))
                )
                db.commit()
            except db.IntegrityError:
                error = f"User {username} is already registered."
            else:
                return redirect(url_for("auth.login"))
            
            flash(error)
        return render_template('auth/register.html')

個人的note :
・条件分岐 elif という書き方
・同一パッケージからの複数importの書式で ()が使える

db.IntegrityErrorとは、Primary keyに重複がある時のエラー?

コードの解説

🙆@bp.route
URL(/register)とViewFunction(register関数)を紐づける
🙆if request.method='POST'
requestのメソッドを判定している。ユーザがフォーム入力した場合はPOST requestになる。
🙆request.form
辞書型のデータになっている。
🙆POSTメソッドの時の処理
db.execute
このメソッドは、?のPlaceholderを持ったSQL文と、Placeholderに置換するタプル型()の値を利用する
この記述方法はSQLインジェクション攻撃への対策になる
generate_password_hash()
パスワードはセキュリティの観点から直接データベースに保存ではなく、ハッシュ化して保存する
db.commit()
db.execute()に入力した値は、元々の入力値を変更しているからdb.commit()して変更を保存させる必要がある
sqlite3.IntegrityError
同じusernameがあるときに発生するエラー、ユーザに対してはわかりやすい形で伝えるべき
🙆データベースに保存後の処理
redirect()とurl_for()
url_forで関数の名称に基づいたURLを自動生成すれば、あとでURLを変えたくなったときに便利なのでredirectで直接URLを指定するよりもこちらを優先したほうがいい
🙆flash関数
templateをレンダリングするときに受け取るメッセージを保存することができる
🙆GETメソッドの時の処理
ユーザが直接 ```auth/register``` へ移動してくる。or 判定エラーの時
HTMLページは表示されるべき、 ```render_template()``` を使ってHTMLを含むtemplateをレンダリングする

ログイン処理 login()

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,password)
        ).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')

コードの解説

register viewとほとんど同じだが以下の点で違いがある

🙆fetchone()
fetchone()はクエリから1行持ってくる。結果がなかったらNone
*fetchall()
結果のすべての行リストを戻す
🙆check_password_hash()関数
入力されたパスワードを、保存時と同じロジックでハッシュ化しセキュアに照合する。
🙆session
sessionは辞書型のデータであり、requestを跨って保存される
ログインが成功した時、sessionにuserのidが保存される
データはCookieに載せてブラウザに返し、ブラウザからの後続のrequestに載って返ってくる
Flaskは安全にデータに署名して、改竄されない様にする

セッションについての詳細 :
https://flask.palletsprojects.com/en/3.0.x/api/#flask.session

bp.before_app_request()

どのURLでrequestされても、一律にユーザーのIDをセッションから確認し、g.userに保存する。

@login_requiredとの違い :
before_app_requestデコレーターはあらゆるRequestが来た時にまず最初に実行する共通の前処理である。login_requiredはビュー別に表示していいものかどうかを柔軟に判定する。もし、before_app_requestでlogin_requiredを設定するとすべてのリクエストに対してログイン判断が行われてしまう。

auth.py
@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user=None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id=?',
            (user_id,)
        ).fetchone()
@bp.before_app_request
どんなURLをrequestされても、そのview function前に実行する関数を設定する
load_logged_in_user()
1-セッションにuserのIDが保存されているかを確認する
2-そのユーザーの情報をデータベースから取得して g.userに保存しておく(gオブジェクトはrequestの間持続する)
3-ユーザ情報がなければ g.userはNoneになる

ログアウト処理 logout()

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

セッションからUser IDを削除すれば load_logged_in_user()はユーザーの情報を読み込むことができない。

他のViewで認証が必要な場合

  • blogテーブルに置いて、CRUD操作をする時はユーザにログインしてもらう必要がある
  • デコレーターが適用されたViewそれぞれにおいてその判定ができる、
auth.py
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
  • このデコレーターは、それぞれのViewを包む(Wrap)した新しいView関数を返す
  • この新しいView関数はユーザーが読み込まれたかを確認して、読み込まれていなかったらログインページへ移動させる
  • もしユーザーが読み込まれたら、通常通り処理される

エンドポイントとURL

url_for()
1-Viewの名前と引数に応じてURLを生成する
エンドポイント
1-Viewに紐づけられた名前はエンドポイントという、デフォルトはView関数の名前と同じである(例. logout()
2-Blueprintを使っていると、関数の頭にblueprintの名前が auth.login の様に付けられる
3-そのため、auth blueprintのlogin関数へのURLは url_for(auth.login)で生成されるのだ
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0