【Python】超簡単！Flaskフレームワークで作るREST APIを試してみた

#はじめに
社内のオンライン開発合宿というイベント向けに、アプリケーションを作る一環として、
（コロナ禍もあってオンライン...）

- `Python`を開発で使ってみたい
- `Flask`が軽量フレームワークで`Django`よりも学習コストが低くく、とっかかりやすそう

という理由で、バックエンドのアーキテクチャに組み込んで、試してみました◎

▼DBマイグレーションとORMの話はこちら

https://qiita.com/ichi_zamurai/items/9e449603e39e8054e301

#Flaskとは
![スクリーンショット 2021-05-14 8.59.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/a81ec913-25d8-6120-d771-510a099f1f42.png)

https://flask.palletsprojects.com/en/2.0.x/
>Flask（フラスク）は、プログラミング言語Python用の、軽量なウェブアプリケーションフレームワークである。標準で提供する機能を最小限に保っているため、自身を「マイクロフレームワーク」と呼んでいる。Werkzeug WSGIツールキットとJinja2テンプレートエンジンを基に作られている。BSDライセンスで公開されている。
参照元：[wikipedia](https://ja.wikipedia.org/wiki/Flask)


ふむふむ。
つまり、**`Flask`自体の機能は最小限だから、自分の好きなように後でカスタマイズしてね**
ってことか。

###Flaskの特徴
- 標準で提供する機能は最小限
- 拡張ライブラリは第三者によって提供
    - オブジェクトリレーショナルマッパ
    - フォーム値の検証
    - ファイルのアップロード
    - ユーザログイン
    - 種々のオープンな認証技術　等々


#自分のFlaskアプリで使うライブラリの準備
`Flask`自体と、それにまつわる拡張ライブラリをインストールしていきます。
※[#] 以降に2021/05/14時点のversionを記載
※`Flask-Cors`がpython3.7までのサポートとなっていたので、Python3.7で開発してます。

```bash
$ pip install Flask # 1.1.2
$ pip install Flask-Cors # 3.0.10
$ pip install python-dotenv # 0.17.1
$ pip install PyMySql # 1.0.2
$ pip install Flask-SQLAlchemy # 2.5.1
$ pip install flask-marshmallow # 0.14.0
$ pip install marshmallow-sqlalchemy # 0.25.0
```
https://qiita.com/ichi_zamurai/items/dcbac977c1ff367b9094


#自分のFlaskアプリのファイル構成
```
src
├── .env             #環境変数の登録
├── main.py          #アプリ起動
├── router.py        #APIエンドポイントの集約。ルーター目的
├── db.py            #dbインスタンスの初期化
├── settings.py      #[.env]から環境変数の読込と設定
├── logger.py        #ロギング用のデコレータ
├── auth.py          #APIの実行認可、認証目的
├── controller
│   └── user_controller.py #コントローラー
├── service
│   └── user_service.py #ビジネスロジック
├── model
│   └── users.py         #モデル
├── config
│   ├── logging.json    #ロギングの設定ファイル
│   └── config.py       #DBの各種設定
└── logs
    └── application.log #ログが出力されるファイル

```

#Flask(Python)で書いたコード
各ファイル自体の目的を明確にして、その用途にあった必要なコードのみの記述を意識しました。

##今回の目的：FlaskのRest APIで主にやること
DBに保存したユーザー情報を、APIを叩いて取得するまでの実践。

##`main.py`でアプリ起動

- DB関連の初期化
- アプリ起動
- アプリに必要なインスタンスを`Blueprint`機能を使って登録（router）

（`__init__.py`とか作ってファイルを分けた方がいいかも？）

```python:main.py
#!/usr/bin/python3
from flask import Flask
from flask_cors import CORS
import router
from config import config
import db

def create_app():
    # Generate Flask App Instance
    app = Flask(__name__)

    # Read DB setting & Initialize
    app.config.from_object(config.Config)
    db.init_db(app)
    db.init_ma(app)

    # Register Router Instance
    app.register_blueprint(router.router)

    # Additional Configuration 
    app.config['JSON_AS_ASCII'] = False #日本語文字化け対策
    app.config["JSON_SORT_KEYS"] = False #ソートをそのまま
    CORS(
        app,
        resources = {
            r"/api/*": {"origins": ["http://localhost", "http://localhost:4200"]}
        }
    )
    return app

app = create_app()
if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True, port=8080, threaded=True, use_reloader=False)
```

##`router.py`でAPIエンドポイントの管理
コントローラーへのルーティング設定を管理しています。

```python:router.py
from flask import Blueprint
from controller import user_controller
from logging import config
from json import load
import auth
import logger

# Generate Router Instance
router = Blueprint('router', __name__)

# Read Logging Configuration
with open("./config/logging.json", "r", encoding="utf-8") as f:
  config.dictConfig(load(f))

@router.route("/", methods=['GET'])
@logger.http_request_logging
@auth.requires_auth
def hello_world():
    return "Hello World!!"

@router.route("/api/v1/users/getUserList", methods=['GET'])
@logger.http_request_logging
@auth.requires_auth
def api_v1_users_get_user_list():
    return user_controller.get_user()

@router.after_request
def after_request(response):
  # response.headers.add('Access-Control-Allow-Origin', '*')
  response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  return response
```

##`controller`でシンプルに結果を返す
クライアント側にビジネスロジックで生成物をレスポンスする。

```python:user_controller.py
from service.user_service import get_user_logic

def get_user():
    return get_user_logic()
```

##`service`でビジネスロジックを管理
DBから抽出したRawデータを加工する目的。

```python:user_service.py
from flask import make_response, jsonify
from model.users import User, UserSchema

def get_user_logic():
    users = User.get_user_list()
    user_schema = UserSchema(many=True)
    return make_response(jsonify({
        'code': 200,
        'users': user_schema.dump(users)
    }))
```

##`model`でDBに問い合わせ
Userモデルの定義と、CRUD系のメソッドを用意します。

```python:users.py
from db import db, ma
from sqlalchemy.dialects.mysql import TIMESTAMP as Timestamp
from sqlalchemy.sql.functions import current_timestamp

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, autoincrement=True, primary_key=True)
    name = db.Column(db.String(225), nullable=False)
    created_at = db.Column(Timestamp, server_default=current_timestamp(), nullable=False)
    updated_at = db.Column(Timestamp, server_default=current_timestamp(), nullable=False)

    # Contructor
    def __init__(self, id, name, created_at, updated_at):
        self.id = id
        self.name = name
        self.created_at = created_at
        self.updated_at = updated_at

    def __repr__(self):
         return '<User %r>' % self.name

    def get_user_list():
        # SELECT * FROM users
        user_list = db.session.query(User).all()
        if user_list == None:
            return []
        else:
            return user_list

    def get_user_by_id(id):
        return db.session.query(User)\
            .filter(User.id == id)\
            .one()

    def create_user(user):
        record = User(
            name = user['name'],
        )
        # INSERT INTO users(name) VALUES(...)
        db.session.add(record)
        db.session.commit()
        return user

# Difinition of User Schema with Marshmallow
# refer: https://flask-marshmallow.readthedocs.io/en/latest/
class UserSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
      model = User
```


##`db.py`でdbインスタンスの初期化
`Marshmallow`という`SQLAlchemy`で受け取ったデータをJSONに変換してくれるライブラリも起動して、`Flask`アプリの`app`に関連づけます。

```python:db.py
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
ma = Marshmallow()

def init_db(app):
  db.init_app(app)

def init_ma(app):
  ma.init_app(app)
```

##`config.py`でdb情報の設定を管理
`settings.py`で設定したコンスタントを使って各種プロパティを指定。

```python:config.py
import settings

class SystemConfig:
    # Flask
    DEBUG = True

    # SQLAlchemy
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{db}?charset=utf8mb4'.format(**{
        'user': settings.MYSQL_USER,
        'password': settings.MYSQL_PASSWORD,
        'host': settings.MYSQL_HOST,
        'db': settings.MYSQL_DATABASE
    })
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ECHO = False

Config = SystemConfig
```

##`settings.py`で環境変数を定数にセット
```python:settings.py
# coding: UTF-8
import os
from os.path import join, dirname
from dotenv import load_dotenv

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

# Set Environment Variables to Constant
AP = os.environ.get("API_KEY") 
MYSQL_ROOT_PASSWORD = os.environ.get("MYSQL_ROOT_PASSWORD")
MYSQL_HOST = os.environ.get("MYSQL_HOST")
MYSQL_DATABASE = os.environ.get("MYSQL_DATABASE")
MYSQL_USER = os.environ.get("MYSQL_USER")
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD")
```

##`auth.py`でAPIに対する認証・認可周りを管理
ここで、APIの共通の認証処理ができるようになるデコレータ用の関数です。
Httpヘッダーの`Authorization`を使う場合に、何かしらできるみたい。

```python:auth.py
from flask import request
from functools import wraps

def requires_auth(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        auth = request.headers.get("Authorization", None)
        # ここで 認証処理 (...まだ書いていない)
        return func(*args, **kwargs)
    return wrapper
```

##`logger.py`で共通のロギング関数デコレータを用意
APIを叩いた時に、ログを残せるようにしてます。

```python:logger.py
from flask import request, current_app
from functools import wraps

def http_request_logging(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        logger = current_app.logger
        try:
            logger.info('%s - %s - %s - %s', request.remote_addr, request.method, request.url, request.query_string)
        except Exception as e:
            logger.exception(e)
            pass
        return f(*args, **kwargs)
    return decorated_function
```

##`logging.json`の設定内容
一例です。この設定を元に、loggerが頑張ってログ出力してくれます。
`console`とファイル出力ができるようにしてます。

```json:logging.json
{
    "version": 1,
    "formatters": {
        "default": {
            "format": "[%(asctime)s] [%(levelname)s] : %(message)s"
        }
    },
    "loggers": {
        "file": {
            "handlers": ["file"],
            "level": "WARN",
            "qualname": "file",
            "propagate": "no"
        },
        "wsgi": {
            "handlers": ["wsgi"],
            "level": "WARN",
            "qualname": "wsgi",
            "propagate": "no"
        }
    },
    "handlers": {
        "file": {
            "class": "logging.handlers.TimedRotatingFileHandler",
            "formatter": "default",
            "filename": "./logs/application.log"
        },
        "wsgi": {
            "class": "logging.StreamHandler",
            "stream": "ext://flask.logging.wsgi_errors_stream",
            "formatter": "default"
        }
    },
    "root": {
        "level": "WARN",
        "handlers": ["file", "wsgi"]
    }
}
```

##`application.log`に出力される内容
```text:application.log
[2021-05-14 11:06:28,844] [INFO] :  * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
[2021-05-14 11:06:28,845] [INFO] :  * Restarting with stat
[2021-05-14 11:06:29,080] [WARNING] :  * Debugger is active!
[2021-05-14 11:06:29,090] [INFO] :  * Debugger PIN: 143-258-047

```

#Flask REST API の実践と動作検証！！
`main.py`がある階層に移動して、アプリ起動！！

```bash
$ cd /Users/username/src/github.com/flask-challenge/api/src
$ python main.py
 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
[2021-05-14 14:37:17,400] [INFO] :  * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
[2021-05-14 14:37:17,402] [INFO] :  * Restarting with stat
[2021-05-14 14:37:17,694] [WARNING] :  * Debugger is active!
[2021-05-14 14:37:17,704] [INFO] :  * Debugger PIN: 143-258-047
```
アプリ起動できましたね〜

###それでは、用意したAPIを叩いてみましょう！！
![スクリーンショット 2021-05-14 12.08.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/89b2571e-d1a5-a63a-e61b-222b3f566f88.png)

###レスポンスが返ってきた！！！！
嬉しす✨



#Flaskでハマったポイント
- `router`を独立したファイルにさせたいと思い、`Blueprint`の機能にたどり着いた。
    - ただ、`Blueprint`の機能が最初わからず、調べてもFlaskの`template(html)`と関連した記事が結構出て戸惑った
    -  =>なんやかんやで、`Flask`アプリ(appのこと)に、他でBlueprintとして起動したインスタンスを登録できるシンプルな機能だった。(便利)
- `flask_cors`の互換性が盲点だった。エラると嫌だからバージョンを`Python3.7`に下げた
- `logger`や`auth`の実装をする時に、デコレータというPythonの概念でつまずいた。
    - 2時間くらいデコレータのソースを見つめて、動画とか見て感覚を養ったら想像できるようになった。
    - 結局は、**元の関数の処理に、前処理として何か追加できるようになる**よってこと。関数の装備品。
- `Marshmallow`の英語の発音が難しすぎた
    - `SQLAlchemy`と合わせて使うようで、結構調べる時間が必要だった


#FlaskとPython触った感想とまとめ
軽量フレームワークというだけあって、最初の導入はめちゃくちゃ楽だった。
`import Flask`でアプリ起動すれば、すぐ使えるこの手軽さはやばい。
`API`とかすぐ作れちゃうなこれ。

色々ライブラリも揃ってて組み合わせするともっと楽しそうだこれ。

しかもPython自体もすげえ感覚的にかける言語で、`Javascript`にも似てるし、かなりストレスフレー！！
勉強にもなったし、最初のとっかかりにはとてもよかった。

業界で屈指のPythonフレームワークである[Django](https://www.djangoproject.com/)も、これから経験積んで色んな開発に携われるようになりたいでごわす。


こちらも参考になれば嬉しいです✨

https://qiita.com/ichi_zamurai/items/9e449603e39e8054e301


以上、ありがとうございました！