Flaskで任意のデータを引数として受け取る方法


## 1. はじめに
[前回](https://qiita.com/5zm/items/4b99d557fafb7927ff1a)はFlaskの拡張ポイントを利用して、全てのURLにPrefixを追加する方法について説明しました。今回も同様にFlaskの拡張ポイントを利用して、`@app.route()`関数デコレータが付与された関数の引数として、任意のデータを受け取る方法について説明したいと思います。

### 1.1. （おさらい）FlaskのURLパス変数について
FlaskではURLの一部の値をパス変数として、引数で受け取ることができます。その際、Converterを利用してデータ型の変換を行うことも可能です。

http://flask.pocoo.org/docs/0.12/quickstart/#variable-rules

> **Variable Rules**
> To add variable parts to a URL you can mark these special sections as `<variable_name>`. Such a part is then passed as a keyword argument to your function. Optionally a converter can be used by specifying a rule with `<converter:variable_name>`. Here are some nice examples:
> 
```python:公式ドキュメントのサンプルより引用
@app.route('/user/<username>')
def show_user_profile(username):
    # show the user profile for that user
    return 'User %s' % 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 'Post %d' % post_id
```

* Flaskがデフォルトで用意しているConverterの一覧（公式ドキュメントより引用）

| データ型 | 説明 |
|:-------:|:-----|
| string | accepts any text without a slash (the default) |
| int | accepts integers |
| float | like int but for floating point values |
| path | like the default but also accepts slashes | 
| any | matches one of the items provided |
| uuid | accepts UUID strings |

**Flaskのデフォルトの仕様では、パス変数を定義したら、必ず引数として定義しなければなりません。
その反対も同様で、引数として定義したものは、パス変数として定義されている必要があります。**

### 1.2. 検証環境
* Python 2.7.13
* Flask 0.12.2

## 2. 任意のデータを引数として受け取るには`url_value_preprocessor`を定義
任意のデータを引数として受け取るには`url_value_preprocessor`を定義します。
`url_value_preprocessor`についてFlaskの公式ドキュメントを見てましょう。
http://flask.pocoo.org/docs/0.12/api/#flask.Flask.url_value_preprocessor

> **url_value_preprocessor(f)**
>  Registers a function as URL value preprocessor for all view functions of the application. It’s called before the view functions are called and can modify the url values provided.

**`url_value_preprocessor`とは？**

* 全てのView Function（`@app.route()`関数デコレータが付与された関数）で利用される。
* 「URL Value（パス変数）を処理する関数」を登録する機能である。
* View Functionの実行前に呼び出される。
* 引数として渡されるURL Value（パス変数）を操作（追加、変更、削除）することができる。

**つまり独自の`url_value_preprocessor`を定義することで、任意のデータを引数として受け取ることができるようになります。**

### 2.1. ソースコード
`url_value_preprocessor`のサンプルとして、URLパスには含まれていないユーザ情報をURL Value（パス変数）に追加する処理を実装してみます。なお、ユーザ情報の取得は本来であればデータベースやKVS、外部の連携システム等から取得すると思いますが、今回は適当なダミーデータとします。要件に応じて自由に実装してください。

URL Value（パス変数）は辞書型（キー：引数名、値：引数として受け取りたいもの）のため、いろいろなデータ型の情報を追加することができます。
今回は独自に定義したクラス（User_Info）のオブジェクトを追加し、引数として受け取ることを試してみます。
クラスを定義するのが面倒な場合、クラスではなく辞書型のデータを利用することも可能です。

```python:url_value_preprocessor_app.py
# -*- coding: utf-8 -*-
import os
import datetime
from flask import Flask, make_response, jsonify
from werkzeug.routing import Rule

# flask
app = Flask(__name__)

# original data class for use as an argument ★ポイント１
class User_Info():
    userid = "dummy"
    roll = "staff"
    registered_at = None

    def __str__(self):
        return "User_Info=[userid={0}, roll={1}, registered_at={2}]" \
            .format(self.userid, self.roll, self.registered_at)

# original url_value_preprocessor ★ポイント２
@app.url_value_preprocessor
def add_user_info(endpoint, values):
    # this fuction is called by all request
    # if specific request only, judge by the value of endpoint
    print "url_value_preprocessor={0}, endpoint={1}".format("add_user_info", endpoint)

    if not endpoint is None:
        # retrieve user infomation from database, kvs, or other system
        # ex. return type is dictionary
        # user_info = {'userid':'0123456789', 'roll':'employee', 'registered_at':'2017-09-01T07:32:45Z'}
        # ex. return type is object of original class
        user_info = User_Info()
        user_info.userid = "0123456789"
        user_info.roll = "employee"
        user_info.registered_at = datetime.datetime(2017, 9, 1, 7, 32, 45)

        # add original data to url_values
        values["userInfo"] = user_info

# rest api ★ポイント３
@app.route('/hello', methods=['GET'])
def hello_world(userInfo):
    # if forget url_value_preprocessor, an error occurs
    # 'TypeError: hello_world() takes exactly 1 argument (0 given)'
    print userInfo
    return make_response(jsonify({'result':'hello world!'}))

@app.route('/goodbye', methods=['GET'])
def goodbye():
    # an error occurs because not define argument
    # 'TypeError: goodbye() takes no arguments (1 given)'
    return make_response(jsonify({'result':'goodbye!'}))

# main
if __name__ == "__main__":
    # 設定されている全ての url_value_preprocessor を表示
    print app.url_value_preprocessors
    app.run(host='localhost', port=3000)
```

### 2.2. 動作確認

```console:引数にuserInfoを定義した関数（hello_world）を呼び出した場合
C:\demo>python url_value_preprocessor_app.py
{None: [<function add_user_info at 0x000000000312B358>]}
 * Running on http://localhost:3000/ (Press CTRL+C to quit)
url_value_preprocessor=add_user_info, endpoint=hello_world
User_Info=[userid=0123456789, roll=employee, registered_at=2017-09-01 07:32:45]
127.0.0.1 - - [05/Oct/2017 21:26:57] "GET /hello HTTP/1.1" 200 -
```

標準出力の結果（`{None: [<function add_user_info at 0x000000000312B358>]}`）から`url_value_preprocessor`として、今回実装した`add_user_info`関数が設定されていることが分かります。

`/hello`にアクセスすると、最初に`url_value_preprocessor`である`add_user_info`関数が実行され、任意のデータ（今回は`userInfo`）をURL Value（パス変数）に登録します。
次に`hello_world`関数が実行されますが、この時点で`userInfo`がURL Value（パス変数）に存在するため、引数として受け取ることができるようになります。

```console:引数にuserInfoを定義していない関数（goodbye）を呼び出した場合
url_value_preprocessor=add_user_info, endpoint=goodbye
[2017-10-05 21:27:20,566] ERROR in app: Exception on /goodbye [GET]
Traceback (most recent call last):
  File "C:\Python27\lib\site-packages\flask\app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "C:\Python27\lib\site-packages\flask\app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "C:\Python27\lib\site-packages\flask\app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "C:\Python27\lib\site-packages\flask\app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "C:\Python27\lib\site-packages\flask\app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
TypeError: goodbye() takes no arguments (1 given)
127.0.0.1 - - [05/Oct/2017 21:27:20] "GET /goodbye HTTP/1.1" 500 -
```

`/goodbye`にアクセスすると、引数にuserInfoを定義していない関数（goodbye）が呼ばれるため、想定通りエラーになります。
エラーにならないようにするには、以下のどちらかを行う必要があります。

* 引数に`userInfo`を定義して受け取る
* `add_user_info`関数で、`endpoint`の値で判断して処理をスキップする

```console:存在しないパス（/hello2）にアクセスした場合
url_value_preprocessor=add_user_info, endpoint=None
127.0.0.1 - - [05/Oct/2017 21:27:57] "GET /hello2 HTTP/1.1" 404 -
```

存在しないパスのため`endpoint`は`None`になりますが、この場合でも`url_value_preprocessor`は処理が実行されます。つまり、全てのリクエストが処理対象になるのが確認できたかと思います。
**`url_value_preprocessor`を定義する際は、この前提を意識して設計や実装を行う必要があります。**

## 3. さいごに
今回はFlaskの拡張ポイント（`url_value_preprocessor`）を利用して、任意のデータを引数として受け取る方法について説明しました。
Javaでspring mvcを利用したことがある方には
`org.springframework.web.method.support.HandlerMethodArgumentResolver`と同様の機能であることが分かるかと思います。
開発言語は違ってもWebアプリケーション用のフレームワークでは、同じような機能が実装されている可能性があると思って調べてみるのもよいかもしれません。