小規模なFlaskのプロジェクトでモジュールを分割する

# はじめに
Blueprintを使うほどでもない規模やAWS Lambda用のプロジェクトで、共通処理のみを別モジュールに切り出したいことがあるかとおもいます。
そんなときに使える手法まとめます。

## 検証環境
- CentOS7
- Python3.6.6
- Flask1.0.2

# 実装例
各手法を適用した例です。

## Flaskメインモジュール
Flaskのメイン処理を記述するモジュールです。

```ruby:api_flask_example.py
from flask import Flask, request, jsonify, make_response
import logging
import api_filter

app = Flask(__name__)
logger = app.logger
logger.setLevel(logging.INFO)

'''''''''''''''''
GET request
'''''''''''''''''
@app.route('/flask-example', methods=['GET'])
def get():
    result = api_filter.make_result(request.method)
    return make_response(jsonify(result))

'''''''''''''''''
POST request
'''''''''''''''''
@app.route('/flask-example', methods=['POST'])
def post():
    result = api_filter.make_result(request.method)
    return make_response(jsonify(result))

'''''''''''''''''
Exception
'''''''''''''''''
@app.route('/flask-exception', methods=['GET'])
def flask_exception():
    raise Exception

'''''''''''''''''
共通処理の設定
'''''''''''''''''
# request前後に実行する処理
app.before_request(api_filter.before_request)
app.after_request(api_filter.after_request)

# エラー発生時
app.register_error_handler(Exception, api_filter.exception_handler)
app.register_error_handler(404, api_filter.not_found_handler)

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

## 共通モジュール
共通処理を記述するモジュールです。

```ruby:api_filter.py
from flask import request, jsonify, current_app, logging
import traceback
from http import HTTPStatus

def make_result(method):
    result = {
            "result": "hello! Flask"
            , "method": method
            , "app_name": current_app.name
        }
    return result

def before_request():
    '''
    リクエストの前処理
    '''
    logger = logging.create_logger(current_app)
    logger.info('start ' + current_app.name + ' :: ' + str(request.json))

def after_request(response):
    '''
    リクエストの後処理
    '''
    logger = logging.create_logger(current_app)
    logger.info('end ' + current_app.name + ' :: httpStatusCode=' +
                str(response._status_code) + ', response=' + str(response.response))
    return response

def exception_handler(ex):
    '''
    共通例外処理
    '''
    logger = logging.create_logger(current_app)

    # 予期せぬ例外発生時
    logger.error(traceback.format_exc())
    response = {"message":"undefined error occured."}

    return (jsonify(response), HTTPStatus.INTERNAL_SERVER_ERROR)

def not_found_handler(ex):
    '''
    存在しないURLへのアクセス
    '''
    logger = logging.create_logger(current_app)

    logger.info('not found ' + request.url)
    response = {"message":"resource not found."}

    return (jsonify(response), HTTPStatus.NOT_FOUND)
```

# 手法


## current_app
`app = Flask(__name__)` はメインモジュールに記述するので共通モジュールでは使えませんが、appの要素を使いたいときがあるかとおもいます。
そんなときには `current_app`を使います。

```ruby:api_flask_example.py
@app.route('/flask-example', methods=['GET'])
def get():
    result = api_filter.make_result(request.method)
    return make_response(jsonify(result))
```

```ruby:api_filter.py
def make_result(method):
    result = {
            "result": "hello! Flask"
            , "method": method
            , "app_name": current_app.name # アプリケーション名。特に指定がなければ Flask(__name__) を実行したモジュール名が使われる
        }
    return result
```

```ruby:実行結果
$ curl http://192.168.33.10:5000/flask-example | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    72  100    72    0     0  21667      0 --:--:-- --:--:-- --:--:-- 24000
{
  "app_name": "api_flask_example",
  "method": "GET",
  "result": "hello! Flask"
}
```

`current_app`はApplicationContextがpushされている間のみ使えます。

## before_request, after_request
リクエストの前後に実施したい処理がメインモジュールにあれば`@before_request`, `@after_request`デコレータを使いますが、別モジュールに分かれているときは文で記述し、引数に実行したいメソッドを指定します。

```ruby:api_flask_example.py
app.before_request(api_filter.before_request)
app.after_request(api_filter.after_request)
```

```ruby:api_filter.py
def before_request():
    logger = logging.create_logger(current_app)
    logger.info('start ' + current_app.name + ' :: ' + str(request.json))

def after_request(response):
    logger = logging.create_logger(current_app)
    logger.info('end ' + current_app.name + ' :: httpStatusCode=' +
                str(response._status_code) + ', response=' + str(response.response))
    return response
```

```ruby:実行結果
$ curl -X POST -H "Content-Type: application/json" -d '{"request":"flask"}' http://192.168.33.10:5000/flask-example | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    92  100    73  100    19  17124   4456 --:--:-- --:--:-- --:--:-- 18250
{
  "app_name": "api_flask_example",
  "method": "POST",
  "result": "hello! Flask"
}
```

```ruby:ログ
[2018-12-15 05:38:34,172] INFO in api_filter: start api_flask_example :: {'request': 'flask'}
[2018-12-15 05:38:34,173] INFO in api_filter: end api_flask_example :: httpStatusCode=200, response=[b'{"app_name":"api_flask_example","method":"POST","result":"hello! Flask"}\n']
192.168.33.10 - - [15/Dec/2018 05:38:34] "POST /flask-example HTTP/1.1" 200 -
```

ログについては後述します。

## ログ
メインモジュール内であれば `app.logger`でロガーを使用できますが、別モジュールではloggerとcurrent_appで改めてロガーを取得する必要があります。

```ruby:api_filter.py
def before_request():
    logger = logging.create_logger(current_app)
    logger.info('start ' + current_app.name + ' :: ' + str(request.json))
```

## 例外処理
例外発生時に実行したい処理がメインモジュールにあれば`@exception_handler`デコレータで登録しますが、別モジュールに分かれているときは`register_error_handler`で登録します。

```ruby:api_flask_example.py
@app.route('/flask-exception', methods=['GET'])
def flask_exception():
    raise Exception

app.register_error_handler(Exception, api_filter.exception_handler) # 何らかのExceptionが発生した
app.register_error_handler(404, api_filter.not_found_handler) # 404 not found
```

```ruby:api_filter.py
def exception_handler(ex):
    logger = logging.create_logger(current_app)

    # 予期せぬ例外発生時
    logger.error(traceback.format_exc())
    response = {"message":"undefined error occured."}

    return (jsonify(response), HTTPStatus.INTERNAL_SERVER_ERROR)

def not_found_handler(ex):
    logger = logging.create_logger(current_app)

    logger.info('not found ' + request.url)
    response = {"message":"resource not found."}

    return (jsonify(response), HTTPStatus.NOT_FOUND)
```

```ruby:実行結果
curl http://192.168.33.10:5000/flask-exception | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    39  100    39    0     0   4703      0 --:--:-- --:--:-- --:--:--  4875
{
  "message": "undefined error occured."
}
```

```ruby:ログ
[2018-12-15 05:45:50,188] INFO in api_filter: start api_flask_example :: None
[2018-12-15 05:45:50,192] ERROR in api_filter: Traceback (most recent call last):
  File "/home/vagrant/venv36/lib64/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/vagrant/venv36/lib64/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/vagrant/workspaces/py-example/src/functions/api_flask_example.py", line 30, in flask_exception
    raise Exception
Exception

[2018-12-15 05:45:50,193] INFO in api_filter: end api_flask_example :: httpStatusCode=HTTPStatus.INTERNAL_SERVER_ERROR, response=[b'{"message":"undefined error occured."}\n']
192.168.33.10 - - [15/Dec/2018 05:45:50] "GET /flask-exception HTTP/1.1" 500 -
```