Flask - 個人的NOTE / 公式DOC>QuickStart

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


- PIN : 
```
~/De/d/flask_d/tutorial main !101 ?1 > python app.py                   13s py myproject py base
 * Serving Flask app 'app'
 * 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 all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5001
 * Running on http://192.168.2.102:5001
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 656-369-823

```
```
Flaskの「PIN」は、デバッグモードが有効になっている際に、デバッガーのセキュリティを強化するために使用される「Personal Identification Number」の略です。Flaskアプリケーションでデバッグモードが有効になると、エラーが発生した際にWebページ上にデバッグ情報が表示されますが、このデバッグインターフェースにはアプリケーションのコードを直接実行する機能が含まれています。

セキュリティ上の理由から、デバッガーにアクセスする際にはPINが要求されます。これにより、不正なユーザーがデバッグ情報を悪用してアプリケーションに介入するのを防ぐことが目的です。このPINは通常、Flaskアプリケーションが起動するたびに生成され、コンソールに表示されます。開発者はこのPINを使用してデバッガーに安全にアクセスできます。
```
- HTML escape
jinjaが自動で実装してくれるが、手動で実装する必要ある?
```
When returning HTML (the default response type in Flask), any user-provided values rendered in the output must be escaped to protect from injection attacks. HTML templates rendered with Jinja, introduced later, will do this automatically.

escape(), shown here, can be used manually. It is omitted in most examples for brevity, but you should always be aware of how you’re using untrusted data.
```

- Variable Rules
```
from markupsafe import escape

@app.route('/user/<username>')
def show_user_profile(username):
    # show the user profile for that user
    return f'User {escape(username)}'

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

@app.route('/path/<path:subpath>')
def show_subpath(subpath):
    # show the subpath after /path/
    return f'Subpath {escape(subpath)}'
Converter types:

string (default) accepts any text without a slash

int:accepts positive integers

float:
accepts positive floating point values

path:like string but also accepts slashes

uuid:accepts UUID strings
```

- Unique URLs / Redirection Behavior
```
@app.route('/projects/') # /projectという末端の/がなくてもこのルーティングにアクセスされる
def projects():
    return 'The project page'

@app.route('/about') #/about/と末端の/を入れると404 Not Foundエラーになる
def about():
    return 'The about page'
```
- url_for関数
```
from flask import Flask, url_for

app = Flask(__name__) 

@app.route('/profile/<name>')
def urlbulding(name):
    return f'{name} \'s friend'

with app.test_request_context():
    print(url_for('urlbulding',name='Yamada Takayuki'))

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=5001)

結果 : /profile/Yamada%20Takayuki
```
この %２０はURLにおける空白のエンコード結果
他にも&とか?も変更されるらしい

- GETとPOSTメソッドの使い分け
```
from flask import request

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

@app.post('/login')
def login_post():
    return do_the_login()
```
※HTTPrequestには種類があって、HEADメソッドとOPTIONSメソッドというものがありFlaskは自動でサポートしているらしいが読んでる時点ではあまりイメージできない
If GET is present, Flask automatically adds support for the HEAD method and handles HEAD requests according to the HTTP RFC. Likewise, OPTIONS is automatically implemented for you.

- static files
staticフォルダのPathへのURLの生成はurl_forが使える
```
url_for('static', filename='style.css')
```
理想的にはこのような静的ファイルはWEBサーバーから供給されるべきだが、開発中に扱うものとしてはstaticフォルダを利用できる。=> アプリサーバーとWEBサーバーを設定する必要がある


- Rendering templates
    - FlaskはTLS(Thread Local Strage)という機能を使った並列処理を実現している
    - これらの並列処理はスレッドという単位で管理される。
    - Session, Request, gオブジェクトなどは一見グローバルオブジェクトに見えるが、それぞれスレッド単位でのローカルオブジェクトになっている。
    - この様なローカルオブジェクトをFlaskではLocal context objectと呼んでいる
    - 細く
        - アプリケーションが処理されるまでのフローや学習についてはこちらでChatgptに確認している
        - https://chatgpt.com/c/8e785a5d-dc2e-424c-bbe8-e15781396d49
    - Unitテストをする時、Requestオブジェクトに依存したコード処理のデバッグには以下のtest_request_context関数を実行させる、これはRequestオブジェクトをユーザが作成する方法
        - メリット
            - test_request_context()を使用する主なメリットは、単体テストの柔軟性と制御の向上です。実際にflask runを用いてアプリケーションを実行し、HTTPリクエストを送る方法も有効ですが、以下の点でtest_request_context()が有利です：

                - 速度と単純化：実際のサーバーを起動する必要がなく、テスト環境の立ち上げとシャットダウンが速くなります。
                - 環境の分離：実サーバーを使うと、他のテストや環境設定に影響されることがありますが、test_request_context()を使うと、テストごとに独立したリクエストコンテキストを生成でき、テストの隔離が保証されます。
                - 細かい制御：リクエストのパラメータやHTTPメソッドを細かく設定できるため、様々なシナリオのテストが容易になります。

- The Request Obj
    - Request objの詳細はAPIセクション :https://flask.palletsprojects.com/en/3.0.x/api/#flask.Request
    - 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'
    # the code below is executed if the request method
    # was GET or the credentials were invalid
    return render_template('login.html', error=error)
```

- ユーザがURLを変更した時、request.formに正しい値が含まれなかったらKeyerrorもしくはエラー400が発生する。
- これはユーザーフレンドリーではないのでハンドリングしたい
- ハンドリングする方法としては、URLのパラメータをrequest.args.get関数を使ってkeyにデフォルト値を入れてあげるか、KeyErrorのエラーハンドリングを実装することが進められている。
```
searchword = request.args.get('key', '')
```

Keyerrorに対する、エラーハンドリングの仕方その1
```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/search')
def search():
    # 'query' パラメータがない場合はデフォルトで None を返す
    search_query = request.args.get('query')
    if search_query is None:
        return jsonify(error="Query parameter 'query' is required"), 400

    result = f"Results for {search_query}"
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)
```
Keyerrorに対する、エラーハンドリングの仕方その2
```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/search')
def search():
    try:
        # クエリパラメータから 'query' キーの値を取得
        search_query = request.args['query']
        # 何らかの検索処理を想定
        result = f"Results for {search_query}"
        return jsonify(result=result)
    except KeyError:
        # 'query' キーが存在しない場合の処理
        return jsonify(error="Query parameter 'query' is required"), 400

if __name__ == '__main__':
    app.run(debug=True)
```

- File Uploads
    - HTMLには``` enctype="multipart/form-data" ```を指定しないとブラウザがファイルをトランスミット(transmit)しない。
        - MIME(Multipurpose Internet Mail Extensions)
            - HTTP通信においてはMIMEタイプという概念を用いて、そのリクエストがどんなデータ形式なのかを指定するHTML
            - 具体的にはHTTP通信のHTTPヘッダー部分のContent-Typeフィールドに記載される情報
```html
<form
  action="http://localhost:8000/"
  method="post"
  enctype="multipart/form-data">
  <label>Name: <input name="myTextField" value="Test" /></label>
  <label><input type="checkbox" name="myCheckBox" /> Check</label>
  <label>
    Upload file: <input type="file" name="myFile" value="test.txt" />
  </label>
  <button>Send the file</button>
</form>
                
```
```http
POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------8721656041911415653955004498
Content-Length: 465

-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myTextField"

Test
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myCheckBox"

on
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myFile"; filename="test.txt"
Content-Type: text/plain

Simple file.
-----------------------------8721656041911415653955004498--
```

- ファイルの保存コード
```python
from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/uploaded_file.txt')
```

クライアントから渡されたファイルの名称を安全に利用
- werkzeugのsecure_filenameを利用

```python
from werkzeug.utils import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['the_file']
        file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
```

- Cookies
    - Cookieの値の設定はresponseオブジェクトのset_cookie()メソッドで実現できる
    - responseオブジェクトはmake_responseメソッドで作成することができる
    - responseオブジェクトを作成する前にCookieを設定したい => Deferred Request Callbacks https://flask.palletsprojects.com/en/3.0.x/patterns/deferredcallbacks/

```python
from flask import make_response

@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'the username')
    return resp
```
- Requestに含まれる、Cookieの値を取得

```python
from flask import request

@app.route('/')
def index():
    username = request.cookies.get('username')
    # use cookies.get(key) instead of cookies[key] to not get a
    # KeyError if the cookie is missing.

```

- Cookieの値は直接的に扱わない(ここではおそらくID/PASSなどの情報)ことを薦めており、その代わりにセッションオブジェクトを使うことができる

- Redirects and Errors => 詳細　：https://flask.palletsprojects.com/en/3.0.x/errorhandling/
    - エラーハンドリングの方法 @app.errorhandler(~~~)
    - @app.errorhandlerで修飾されたメソッドにはエラーオブジェクト(error)が渡される。
        - errorオブジェクトをprint出力してみた

※エラーはabortメソッドでエラーコードを指定して実行することができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/d24ec5e6-6587-c507-d73b-356c2b696d89.png)

```python
from flask import Flask, abort, redirect, url_for, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return redirect(url_for('err_page'))

@app.route('/err_page/')
def err_page():
    abort(401)
    return redirect(url_for('index'))

@app.route('/err_page2/')
def err_page2():
    abort(404)
    return redirect(url_for('index'))

@app.errorhandler(401)
def err_401(error):
    return render_template('error_401.html',msg=error)


@app.errorhandler(404)
def err_404(error):
    return render_template('error_404.html',msg=error)

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0',port=5001)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/8362cfe3-981f-9b2f-a5e2-fc2b1317c668.png)

- About Response
    - view functionからの戻り値は自動でRespnseオブジェクトに変換されている
        - *view function : 特定のエンドポイントに対するリクエストに応答して実行される関数のこと(@app.route(~)で修飾されるあらゆる関数のこと)

    - view functionの戻り値のデータ型 => Responseオブジェクト内での変換結果
        - String
            - body : string
            - status : 200 ok
            - mimetype : text/html
        - Response obj
            - Response objをそのまま返す
        - イテレーター / ジェネレーター
            - ストリーミングレスポンスとして扱われる
            - イテレータ
                - for文の中で使われるオブジェクト。リストやタプル,辞書型のデータの値を順番に取ることができるメソッドを持つオブジェクト
            - ジェネレーター
                - イテレータの一種。関数の形で定義される。その中ではyieldステートメントを用いて一時的に値を関数の中に保存しておき、イテレーターの__next__()メソッドを実行するたびに次の値を作り出す

```python
def simple_generator():
    yield 1
    yield 2
    yield 3

gen = simple_generator()  # ジェネレータオブジェクトを作成
print(next(gen))  # 1を出力
print(next(gen))  # 2を出力
print(next(gen))  # 3を出力
```

- リスト / 辞書型
    - jsonify()を使ってjson形式のresponseオブジェクトが生成される
- タプル型 : 
    - HTTP ボディ, ステータス,　ヘッダーの順番で指定してResponseオブジェクトを上書きすることができる

```python
from flask import Flask, make_response

app = Flask(__name__)

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

@app.route('/not_found')
def not_found():
    return 'This page does not exist', 404

@app.route('/custom_header')
def custom_header():
    headers = {'Content-Type': 'application/json', 'X-Header': 'Value'}
    return '{"key": "value"}', 200, headers

if __name__ == '__main__':
    app.run(debug=True)

# この形がタプル型
#return 'Hello, World!', 200
#return 'This page does not exist', 404
#return '{"key": "value"}', 200, headers

```

- Sessions 
    - flaskのデフォルトのセッションの方式はClient Side Session
    - Client side session
        - ユーザのCookie情報にsession情報が入っている
        - 署名や暗号化といった機能を使ってセキュリティを担保している
```cl: Flask.secret_keyをOSのランダム生成機能を使って作成
$ python -c 'import secrets; print(secrets.token_hex())'
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
```
a
        - Serverside sessionもFlask拡張機能でサポートしているらしいです。


- Message Flashing
    - 一時的にメッセージを表示する機能
        - 一時的にと言っても自動で消えるわけでは無い、ブラウザの更新をしたときに消える
        - 自動で消すためにはjavascriptで実装する必要がある。
    - HTMLにも反映しないと使えない
    - categoryという機能を使って、flashさせるメッセージの表記方法を変更させることができる
    - またcategoryに特定の値が出たときだけflashとして扱うフィルタ的な機能も実装可能

```python: app.py
from flask import Flask, flash, redirect, render_template, \
request, url_for

app = Flask(__name__)
app.secret_key = 'test'

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/login', methods=['GET','POST'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] != 'admin' or \
            request.form['password'] != 'secret':
            error = 'invalid credentials'
        else:
            # username, passwordがそれぞれadmin , secretならば画面の上部にflashメッセージが表示される
            # 第二引数はcategoryと呼ばれる任意の文字列, categoryの値をhtmlのclass名にするなどしてフォーマットを動的に指定できる
            flash('You were successfully logged in','error') 
            return redirect(url_for('index'))
    return render_template('login.html',error=error)

if __name__=='__main__':
    app.run(debug=True, host='0.0.0.0',port='5001')
```

```html: index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Application</title>
</head>
<body>
    <!-- with句で変数を指定することができる。ここではmessages変数を定義 -->
    <!-- get_flashed_messages(), キーワード引数はwith_categories, category_filter -->
        <!-- with_categories : html内でcategoryを利用するかどうかを指定 -->
        <!-- category_filter : htmlに反映する特定のcategoryを指定 
            ex. get_flashed_messages(category_filter=["error"]) -->
    {% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        <ul class="flashes">
            {% for category,message in messages %}
                <li class="{{ category }}">{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}
    {% endwith %}
    {% block body %}{% endblock %}
</body>
</html>
```
```html: index.html
{% extends "layout.html" %}
{% block body %}
    <h1>overview</h1>
    <p>Do you want to <a href="{{url_for('login')}}">LOG IN?</a></p>
{% endblock %}
```
```html:login.html
{% extends "layout.html" %}
{% block body %}
<h1>login</h1>
    {% if error %}
        <p class="error"><strong>Error: </strong></p>{{ error }}
    {% endif %}
<!-- dt, ddはなくても使える-->
<form method="post">
    <dl>
        <dt>Username:
            <dd><input type="text" name="username" value="{{ request.form.username }}">
        <dt>Password:
            <dd><input type="password" name="password">
    </dl>
    <p><input type="submit" value="login"></p>
</form>
{% endblock %}

```

実行の様子

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/67af0389-00e2-a135-6e45-74ff9b2cbbee.png)

ID pass入力画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/4ab8382e-e62c-100c-35f5-13fe42baa66d.png)

正しいID PASSを入力した時 : 
You were successfully logged inというflashで作成した文字列がでる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/cfc6d480-b70b-aa2f-d513-753107f2fec0.png)

更新後 : flashで作成したメッセージが消えている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/52cbb1a4-98e1-defe-247d-f976c9c1bf56.png)

- Logging
    - HTTP requestで受け取るユーザの値が改ざんや、間違った入力がされた時基本的には400エラーを出せば良いが、一方でエラーが実行されない場合もある。その時にLoggerライブラリを利用できる。

```python
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')

```

- Hooking in WSGI Middleware
    - WSGIミドルウェアとは?
        - Pythonで書かれたアプリケーションとウェブサーバーの間に存在するコンポーネント
        - 両者のRequestとResponseに対して追加処理や変更を行う
    
    - 実はそもそもFlaskもWSGIミドルウェアとして機能している
        - Werkzeugの提供するProxyfixでラッパーすることで以下の機能を追加できる
            - ロギング機能を追加(request, responseのやり取りを記録)
            - 認証機能を追加(requestをフィルタリングする)
            - リバースプロキシのサポート
                - WEBサーバーがNginxやApacheの時、クライアントのIPアドレス/スキーム(http/https)が伝わらない時があることを避ける
             - セキュリティの向上
                 - クエアイアンとIPアドレス, ホスト情報
             - アプリの処理の信頼性の向上
                 - プロキシやロードバランサー(Nginx)で追加されるヘッダー情報を正しく処理できる
```python: wsgiミドルウェアをラッパー
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)

# ProxyFixミドルウェアを追加
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1)

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

if __name__ == '__main__':
    app.run()
```

- Flask拡張機能(ex flask_SQLalchemy) 
    - 探し方 - Pyplで検索 => https://pypi.org/search/?c=Framework+%3A%3A+Flask
        - 基本的にはFlask-Foo, Foo-Flaskという形で作成されるらしい

    - 使い方
        - extentionパッケージをインスタンス化し、Flaskインスタンスの初期設定時に設定することが一般的
```python: how to use extension
from flask_foo import Foo

foo = Foo()

app = Flask(__name__)
app.config.update(
    FOO_BAR='baz',
    FOO_SPAM='eggs',
)

foo.init_app(app)    
```

- Deploying to Production
    - 必ず運用時には本番環境を利用せよ
        - アプリ開発時 : テストサーバー(デバッガー, ホットリローダー)
        - 本番 : 専用のWSGIサーバーかプラットフォームを使用

            - 「本番」=>数百万人のユーザー向けでも、一人のユーザー向けでも等しく「開発状態では無い」という意味
            - 開発用のサーバーはローカルの開発環境でだけ使われる様に設計されていて、セキュア&安定&効率的に作られていない
    - ### 自分でサーバーを構築する場合
        - #### Client側サーバー(スマホ, PC) 
            - : WEBサーバーヘHTTP Requestを送信しHTTP Responseを受け取る
        - #### WEBサーバー (Nginx, Apache)
            - : Requestを受け取り、静的コンテンツ(HTML/CSS/JS/画像)だけほしいなら直接参照し、アプリ処理が必要ならバックエンドに(WSGIサーバーに)転送する
            - #### WSGI ミドルウェア(ProxyFix)
                - ： WEBサーバーとWSGIサーバーのやり取りに対して追加機能(リバースプロキシのサポート, 認証, ロギング)を付与
        - #### WSGIサーバー(Gunicorn, uWSGI)
            - : アプリケーションを実行するためのサーバー。WSGIサーバーはHTTPサーバーとしての役割を内包しているが、HTTP専用のサーバーを建てた方が安全かつ効率的である
            - #### HTTPサーバー
                - : HTTP通信のやり取りをするサーバー
                - : WEBサーバー > HTTPサーバー > WSGIサーバーの様な位置にHTTPサーバーを置く構造をリバースプロキシと呼ぶ
    - ### 既存のサービス(Paas)
        - PythonAnywhere
        - Google App Engine
        - Google Cloud Run
        - AWS Elastic Beanstalk
        - Microsoft Azure
        - => ネットワーク, ドメインをメンテナンスしなくても良い
        - => 上記はFlask / WSGI / pythonの導入のチュートリアルがある
        - => このページではプロキシサーバー(HTTPサーバー)の数を指定しておいて、信頼できるRequestだけ取得できる様にする方法を記載している : https://flask.palletsprojects.com/en/3.0.x/deploying/proxy_fix/