50
86

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-14

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

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

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

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

#Flaskとは
スクリーンショット 2021-05-14 8.59.09.png

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

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

###Flaskの特徴

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

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

$ 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

#自分の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とか作ってファイルを分けた方がいいかも?)

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エンドポイントの管理
コントローラーへのルーティング設定を管理しています。

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でシンプルに結果を返す
クライアント側にビジネスロジックで生成物をレスポンスする。

user_controller.py
from service.user_service import get_user_logic

def get_user():
    return get_user_logic()

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

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系のメソッドを用意します。

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に関連づけます。

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で設定したコンスタントを使って各種プロパティを指定。

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で環境変数を定数にセット

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を使う場合に、何かしらできるみたい。

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を叩いた時に、ログを残せるようにしてます。

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とファイル出力ができるようにしてます。

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に出力される内容

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がある階層に移動して、アプリ起動!!

$ 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

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

#Flaskでハマったポイント

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

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

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

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

業界で屈指のPythonフレームワークであるDjangoも、これから経験積んで色んな開発に携われるようになりたいでごわす。

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

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

50
86
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
50
86

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?